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 `Default`), 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 all constraint applications for one type so the
1687    /// latest `-> suggest` command wins. Measure scalars stay raw until unit factors
1688    /// are resolved; callers convert via [`value_kind_from_raw_suggestion`].
1689    pub fn apply_constraint(
1690        &mut self,
1691        type_name: &str,
1692        command: TypeConstraintCommand,
1693        args: &[CommandArg],
1694        declared_suggestion: &mut Option<RawSuggestion>,
1695    ) -> Result<(), String> {
1696        if command == TypeConstraintCommand::Trait
1697            && !matches!(&self, TypeSpecification::Measure { .. })
1698        {
1699            return Err("trait command is only valid on measure types".to_string());
1700        }
1701        match self {
1702            TypeSpecification::Boolean { help } => match command {
1703                TypeConstraintCommand::Help => {
1704                    apply_type_help_command(help, args)?;
1705                }
1706                TypeConstraintCommand::Suggest => {
1707                    let lit = require_literal(args, "suggest")?;
1708                    reject_calendar_for_suggestion(
1709                        lit,
1710                        type_name,
1711                        SuggestionExpectation::Boolean,
1712                        None,
1713                    )?;
1714                    match lit {
1715                        crate::literals::Value::Boolean(bv) => {
1716                            *declared_suggestion =
1717                                Some(RawSuggestion::Value(ValueKind::Boolean(bool::from(bv))));
1718                        }
1719                        _ => {
1720                            return Err(
1721                                "Please provide true or false, for example `-> suggest true`."
1722                                    .to_string(),
1723                            );
1724                        }
1725                    }
1726                }
1727                other => {
1728                    return Err(format!(
1729                        "Invalid command '{}' for boolean type. Valid commands: help, suggest",
1730                        other
1731                    ));
1732                }
1733            },
1734            TypeSpecification::Measure {
1735                decimals,
1736                minimum,
1737                maximum,
1738                units,
1739                traits,
1740                help,
1741                ..
1742            } => match command {
1743                TypeConstraintCommand::Decimals => {
1744                    let d = require_decimal_literal(args, "decimals")?;
1745                    *decimals = Some(decimal_to_u8(d, "decimals")?);
1746                }
1747                TypeConstraintCommand::Unit => {
1748                    let (unit_name, value, derived_measure_factors) = match args {
1749                        [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
1750                            (name.clone(), *v, Vec::new())
1751                        }
1752                        [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Expr(
1753                            prefix,
1754                            factors,
1755                        ))] => {
1756                            let raw: Vec<(String, i32)> = factors
1757                                .iter()
1758                                .map(|f| (f.measure_ref.clone(), f.exp))
1759                                .collect();
1760                            (name.clone(), *prefix, raw)
1761                        }
1762                        _ => {
1763                            return Err(
1764                                "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')"
1765                                    .to_string(),
1766                            );
1767                        }
1768                    };
1769                    if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
1770                        let new_factor = crate::computation::rational::decimal_to_rational(value)
1771                            .map_err(|failure| failure.to_string())?;
1772                        if existing.factor != new_factor
1773                            || existing.derived_measure_factors != derived_measure_factors
1774                        {
1775                            return Err(format!(
1776                                "Unit '{unit_name}' is already defined in this type's inherited units; \
1777                                 cannot change factor or decomposition. Add a new unit name instead."
1778                            ));
1779                        }
1780                    } else {
1781                        units.0.push(MeasureUnit::from_decimal_factor(
1782                            unit_name,
1783                            value,
1784                            derived_measure_factors,
1785                        )?);
1786                    }
1787                }
1788                TypeConstraintCommand::Trait => {
1789                    let measure_trait = parse_measure_trait(args)?;
1790                    if traits.contains(&measure_trait) {
1791                        return Err(format!(
1792                            "Duplicate trait '{}' for measure type.",
1793                            measure_trait_name(measure_trait)
1794                        ));
1795                    }
1796                    if measure_trait == MeasureTrait::Duration {
1797                        validate_duration_trait_requirements(units)?;
1798                    }
1799                    if measure_trait == MeasureTrait::Calendar {
1800                        validate_calendar_trait_requirements(units)?;
1801                    }
1802                    traits.push(measure_trait);
1803                }
1804                TypeConstraintCommand::Minimum => {
1805                    *minimum = Some(parse_measure_declared_bound(
1806                        args, "minimum", units, type_name,
1807                    )?);
1808                }
1809                TypeConstraintCommand::Maximum => {
1810                    *maximum = Some(parse_measure_declared_bound(
1811                        args, "maximum", units, type_name,
1812                    )?);
1813                }
1814                TypeConstraintCommand::Help => {
1815                    apply_type_help_command(help, args)?;
1816                }
1817                TypeConstraintCommand::Suggest => {
1818                    let lit = require_literal(args, "suggest")?;
1819                    if !traits.contains(&MeasureTrait::Calendar) {
1820                        reject_calendar_for_suggestion(
1821                            lit,
1822                            type_name,
1823                            SuggestionExpectation::MeasureUnits,
1824                            Some(units),
1825                        )?;
1826                    }
1827                    match lit {
1828                        crate::literals::Value::NumberWithUnit(_, _) => {
1829                            let (magnitude, unit_name) =
1830                                parse_measure_declared_bound(args, "suggest", units, type_name)?;
1831                            *declared_suggestion = Some(RawSuggestion::Measure {
1832                                magnitude,
1833                                unit_name,
1834                            });
1835                        }
1836                        _ => {
1837                            return Err(measure_suggestion_wrong_shape_error(type_name, traits));
1838                        }
1839                    }
1840                }
1841                _ => {
1842                    return Err(format!(
1843                        "Invalid command '{}' for measure type. Valid commands: unit, trait, minimum, maximum, decimals, help, suggest",
1844                        command
1845                    ));
1846                }
1847            },
1848            TypeSpecification::Number {
1849                decimals,
1850                minimum,
1851                maximum,
1852                help,
1853            } => match command {
1854                TypeConstraintCommand::Decimals => {
1855                    let d = require_decimal_literal(args, "decimals")?;
1856                    *decimals = Some(decimal_to_u8(d, "decimals")?);
1857                }
1858                TypeConstraintCommand::Unit => {
1859                    return Err(
1860                        "Invalid command 'unit' for number type. Number types are dimensionless and cannot have units. Use 'measure' type instead.".to_string()
1861                    );
1862                }
1863                TypeConstraintCommand::Minimum => {
1864                    *minimum = Some(require_decimal_literal(args, "minimum")?);
1865                }
1866                TypeConstraintCommand::Maximum => {
1867                    *maximum = Some(require_decimal_literal(args, "maximum")?);
1868                }
1869                TypeConstraintCommand::Help => {
1870                    apply_type_help_command(help, args)?;
1871                }
1872                TypeConstraintCommand::Suggest => {
1873                    let lit = require_literal(args, "suggest")?;
1874                    reject_calendar_for_suggestion(
1875                        lit,
1876                        type_name,
1877                        SuggestionExpectation::Number,
1878                        None,
1879                    )?;
1880                    match lit {
1881                        crate::literals::Value::Number(d) => {
1882                            *declared_suggestion = Some(RawSuggestion::Value(ValueKind::Number(
1883                                lift_parser_decimal(*d)?,
1884                            )));
1885                        }
1886                        _ => {
1887                            return Err(
1888                                "Please provide a number, for example `-> suggest 42`.".to_string()
1889                            );
1890                        }
1891                    }
1892                }
1893                _ => {
1894                    return Err(format!(
1895                        "Invalid command '{}' for number type. Valid commands: minimum, maximum, decimals, help, suggest",
1896                        command
1897                    ));
1898                }
1899            },
1900            TypeSpecification::NumberRange {
1901                lower,
1902                upper,
1903                minimum,
1904                maximum,
1905                help,
1906            } => match command {
1907                TypeConstraintCommand::Lower => {
1908                    *lower = Some(require_decimal_literal(args, "lower")?);
1909                }
1910                TypeConstraintCommand::Upper => {
1911                    *upper = Some(require_decimal_literal(args, "upper")?);
1912                }
1913                TypeConstraintCommand::Minimum => {
1914                    let width = require_decimal_literal(args, "minimum")?;
1915                    reject_negative_width_magnitude(&width, "minimum")?;
1916                    *minimum = Some(width);
1917                }
1918                TypeConstraintCommand::Maximum => {
1919                    let width = require_decimal_literal(args, "maximum")?;
1920                    reject_negative_width_magnitude(&width, "maximum")?;
1921                    *maximum = Some(width);
1922                }
1923                TypeConstraintCommand::Help => {
1924                    apply_type_help_command(help, args)?;
1925                }
1926                TypeConstraintCommand::Suggest => {
1927                    let (left, right) = require_suggestion_range_endpoints(
1928                        args,
1929                        type_name,
1930                        SuggestionExpectation::NumberRange,
1931                        None,
1932                    )?;
1933                    let left = literal_value_from_parser_value(left)?;
1934                    let right = literal_value_from_parser_value(right)?;
1935                    if !left.lemma_type.is_number() || !right.lemma_type.is_number() {
1936                        return Err(
1937                            "Please provide a number range, for example `-> suggest 10...100`."
1938                                .to_string(),
1939                        );
1940                    }
1941                    *declared_suggestion = Some(RawSuggestion::Value(ValueKind::Range(
1942                        Box::new(left),
1943                        Box::new(right),
1944                    )));
1945                }
1946                _ => {
1947                    return Err(format!(
1948                        "Invalid command '{}' for number range type. Valid commands: lower, upper, minimum, maximum, help, suggest",
1949                        command
1950                    ));
1951                }
1952            },
1953            TypeSpecification::Ratio {
1954                decimals,
1955                minimum,
1956                maximum,
1957                units,
1958                help,
1959            } => match command {
1960                TypeConstraintCommand::Decimals => {
1961                    let d = require_decimal_literal(args, "decimals")?;
1962                    *decimals = Some(decimal_to_u8(d, "decimals")?);
1963                }
1964                TypeConstraintCommand::Unit => {
1965                    let (unit_name, value_dec) = match args {
1966                        [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
1967                            (name.clone(), *v)
1968                        }
1969                        _ => {
1970                            return Err(
1971                                "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."
1972                                    .to_string(),
1973                            );
1974                        }
1975                    };
1976                    let value = crate::computation::rational::decimal_to_rational(value_dec)
1977                        .map_err(|failure| {
1978                            format!(
1979                                "ratio unit value is not exactly representable as a rational: {}",
1980                                failure
1981                            )
1982                        })?;
1983                    if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
1984                        if existing.value != value {
1985                            return Err(format!(
1986                                "Unit '{unit_name}' is already defined in this type's inherited units; \
1987                                 cannot change factor. Add a new unit name instead."
1988                            ));
1989                        }
1990                    } else {
1991                        units.0.push(RatioUnit {
1992                            name: unit_name,
1993                            value,
1994                            minimum: None,
1995                            maximum: None,
1996                            suggestion_magnitude: None,
1997                        });
1998                    }
1999                }
2000                TypeConstraintCommand::Minimum => {
2001                    let canonical = ratio_bound_to_canonical_rational(args, "minimum", units)?;
2002                    sync_ratio_units_from_canonical(
2003                        units,
2004                        &canonical,
2005                        UnitConstraintField::Minimum,
2006                    )?;
2007                    *minimum = Some(canonical);
2008                }
2009                TypeConstraintCommand::Maximum => {
2010                    let canonical = ratio_bound_to_canonical_rational(args, "maximum", units)?;
2011                    sync_ratio_units_from_canonical(
2012                        units,
2013                        &canonical,
2014                        UnitConstraintField::Maximum,
2015                    )?;
2016                    *maximum = Some(canonical);
2017                }
2018                TypeConstraintCommand::Help => {
2019                    apply_type_help_command(help, args)?;
2020                }
2021                TypeConstraintCommand::Suggest => {
2022                    let lit = require_literal(args, "suggest")?;
2023                    reject_calendar_for_suggestion(
2024                        lit,
2025                        type_name,
2026                        SuggestionExpectation::Ratio,
2027                        None,
2028                    )?;
2029                    let default = match lit {
2030                        crate::literals::Value::NumberWithUnit(_, _) => {
2031                            let element_spec = TypeSpecification::Ratio {
2032                                decimals: *decimals,
2033                                minimum: minimum.clone(),
2034                                maximum: maximum.clone(),
2035                                units: units.clone(),
2036                                help: help.clone(),
2037                            };
2038                            parser_value_to_value_kind(lit, &element_spec)?
2039                        }
2040                        other => {
2041                            return Err(format!(
2042                                "suggest requires a ratio literal with a unit, got {}. Please provide a ratio value with a unit, for example `-> suggest 25%`.",
2043                                value_kind_name(other)
2044                            ));
2045                        }
2046                    };
2047                    sync_ratio_suggestion_units(units, &default)?;
2048                    *declared_suggestion = Some(RawSuggestion::Value(default));
2049                }
2050                _ => {
2051                    return Err(format!(
2052                        "Invalid command '{}' for ratio type. Valid commands: unit, minimum, maximum, decimals, help, suggest",
2053                        command
2054                    ));
2055                }
2056            },
2057            TypeSpecification::RatioRange {
2058                lower,
2059                upper,
2060                minimum,
2061                maximum,
2062                units,
2063                help,
2064            } => match command {
2065                TypeConstraintCommand::Unit => {
2066                    let (unit_name, value_dec) = match args {
2067                        [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
2068                            (name.clone(), *v)
2069                        }
2070                        _ => {
2071                            return Err(
2072                                "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."
2073                                    .to_string(),
2074                            );
2075                        }
2076                    };
2077                    let value = crate::computation::rational::decimal_to_rational(value_dec)
2078                        .map_err(|e| {
2079                            format!(
2080                                "ratio unit value is not exactly representable as a rational: {e}"
2081                            )
2082                        })?;
2083                    if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
2084                        if existing.value != value {
2085                            return Err(format!(
2086                                "Unit '{unit_name}' is already defined in this type's inherited units; \
2087                                 cannot change factor. Add a new unit name instead."
2088                            ));
2089                        }
2090                    } else {
2091                        units.0.push(RatioUnit {
2092                            name: unit_name,
2093                            value,
2094                            minimum: None,
2095                            maximum: None,
2096                            suggestion_magnitude: None,
2097                        });
2098                    }
2099                }
2100                TypeConstraintCommand::Lower => {
2101                    *lower = Some(ratio_bound_to_canonical_rational(args, "lower", units)?);
2102                }
2103                TypeConstraintCommand::Upper => {
2104                    *upper = Some(ratio_bound_to_canonical_rational(args, "upper", units)?);
2105                }
2106                TypeConstraintCommand::Minimum => {
2107                    let width = ratio_bound_to_canonical_rational(args, "minimum", units)?;
2108                    reject_negative_width_magnitude(&width, "minimum")?;
2109                    *minimum = Some(width);
2110                }
2111                TypeConstraintCommand::Maximum => {
2112                    let width = ratio_bound_to_canonical_rational(args, "maximum", units)?;
2113                    reject_negative_width_magnitude(&width, "maximum")?;
2114                    *maximum = Some(width);
2115                }
2116                TypeConstraintCommand::Help => {
2117                    apply_type_help_command(help, args)?;
2118                }
2119                TypeConstraintCommand::Suggest => {
2120                    let (left, right) = require_suggestion_range_endpoints(
2121                        args,
2122                        type_name,
2123                        SuggestionExpectation::RatioRange,
2124                        None,
2125                    )?;
2126                    let element_spec = TypeSpecification::RatioRange {
2127                        lower: lower.clone(),
2128                        upper: upper.clone(),
2129                        minimum: minimum.clone(),
2130                        maximum: maximum.clone(),
2131                        units: units.clone(),
2132                        help: help.clone(),
2133                    }
2134                    .element_from_range()
2135                    .expect("BUG: RatioRange must define element_from_range");
2136                    let left = lift_range_endpoint(left, &element_spec)?;
2137                    let right = lift_range_endpoint(right, &element_spec)?;
2138                    if !left.lemma_type.is_ratio() || !right.lemma_type.is_ratio() {
2139                        return Err(
2140                            "Please provide a ratio range, for example `-> suggest 10%...50%`."
2141                                .to_string(),
2142                        );
2143                    }
2144                    *declared_suggestion = Some(RawSuggestion::Value(ValueKind::Range(
2145                        Box::new(left),
2146                        Box::new(right),
2147                    )));
2148                }
2149                _ => {
2150                    return Err(format!(
2151                        "Invalid command '{}' for ratio range type. Valid commands: unit, lower, upper, minimum, maximum, help, suggest",
2152                        command
2153                    ));
2154                }
2155            },
2156            TypeSpecification::Text {
2157                length,
2158                options,
2159                help,
2160            } => match command {
2161                TypeConstraintCommand::Option => {
2162                    if args.len() != 1 {
2163                        return Err("option takes exactly one argument".to_string());
2164                    }
2165                    options.push(option_name(&args[0], "option")?);
2166                }
2167                TypeConstraintCommand::Options => {
2168                    let mut collected = Vec::with_capacity(args.len());
2169                    for arg in args {
2170                        collected.push(option_name(arg, "options")?);
2171                    }
2172                    *options = collected;
2173                }
2174                TypeConstraintCommand::Length => {
2175                    let d = require_decimal_literal(args, "length")?;
2176                    *length = Some(decimal_to_usize(d, "length")?);
2177                }
2178                TypeConstraintCommand::Help => {
2179                    apply_type_help_command(help, args)?;
2180                }
2181                TypeConstraintCommand::Suggest => {
2182                    let lit = require_literal(args, "suggest")?;
2183                    reject_calendar_for_suggestion(
2184                        lit,
2185                        type_name,
2186                        SuggestionExpectation::Text,
2187                        None,
2188                    )?;
2189                    match lit {
2190                        crate::literals::Value::Text(s) => {
2191                            *declared_suggestion =
2192                                Some(RawSuggestion::Value(ValueKind::Text(s.clone())));
2193                        }
2194                        _ => {
2195                            return Err(
2196                                "Please provide a text value in double quotes, for example `-> suggest \"my default value\"`."
2197                                    .to_string(),
2198                            );
2199                        }
2200                    }
2201                }
2202                _ => {
2203                    return Err(format!(
2204                        "Invalid command '{}' for text type. Valid commands: options, length, help, suggest",
2205                        command
2206                    ));
2207                }
2208            },
2209            TypeSpecification::Date {
2210                minimum,
2211                maximum,
2212                help,
2213            } => match command {
2214                TypeConstraintCommand::Minimum => {
2215                    let dt = require_date_literal(args, "minimum")?;
2216                    *minimum = Some(dt);
2217                }
2218                TypeConstraintCommand::Maximum => {
2219                    let dt = require_date_literal(args, "maximum")?;
2220                    *maximum = Some(dt);
2221                }
2222                TypeConstraintCommand::Help => {
2223                    apply_type_help_command(help, args)?;
2224                }
2225                TypeConstraintCommand::Suggest => {
2226                    let lit = require_literal(args, "suggest")?;
2227                    reject_calendar_for_suggestion(
2228                        lit,
2229                        type_name,
2230                        SuggestionExpectation::Date,
2231                        None,
2232                    )?;
2233                    match lit {
2234                        crate::literals::Value::Date(dt) => {
2235                            *declared_suggestion = Some(RawSuggestion::Value(ValueKind::Date(
2236                                date_time_to_semantic(dt),
2237                            )));
2238                        }
2239                        _ => {
2240                            return Err(
2241                                "Please provide a date, for example `-> suggest 2024-06-15`."
2242                                    .to_string(),
2243                            );
2244                        }
2245                    }
2246                }
2247                _ => {
2248                    return Err(format!(
2249                        "Invalid command '{}' for date type. Valid commands: minimum, maximum, help, suggest",
2250                        command
2251                    ));
2252                }
2253            },
2254            TypeSpecification::DateRange {
2255                lower,
2256                upper,
2257                minimum,
2258                maximum,
2259                help,
2260            } => match command {
2261                TypeConstraintCommand::Lower => {
2262                    *lower = Some(require_date_literal(args, "lower")?);
2263                }
2264                TypeConstraintCommand::Upper => {
2265                    *upper = Some(require_date_literal(args, "upper")?);
2266                }
2267                TypeConstraintCommand::Minimum => {
2268                    *minimum = Some(parse_unresolved_width_bound(args, "minimum")?);
2269                }
2270                TypeConstraintCommand::Maximum => {
2271                    *maximum = Some(parse_unresolved_width_bound(args, "maximum")?);
2272                }
2273                TypeConstraintCommand::Help => {
2274                    apply_type_help_command(help, args)?;
2275                }
2276                TypeConstraintCommand::Suggest => {
2277                    let (left, right) = require_suggestion_range_endpoints(
2278                        args,
2279                        type_name,
2280                        SuggestionExpectation::DateRange,
2281                        None,
2282                    )?;
2283                    let left = literal_value_from_parser_value(left)?;
2284                    let right = literal_value_from_parser_value(right)?;
2285                    if !left.lemma_type.is_date() || !right.lemma_type.is_date() {
2286                        return Err(
2287                            "Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
2288                                .to_string(),
2289                        );
2290                    }
2291                    *declared_suggestion = Some(RawSuggestion::Value(ValueKind::Range(
2292                        Box::new(left),
2293                        Box::new(right),
2294                    )));
2295                }
2296                _ => {
2297                    return Err(format!(
2298                        "Invalid command '{}' for date range type. Valid commands: lower, upper, minimum, maximum, help, suggest",
2299                        command
2300                    ));
2301                }
2302            },
2303            TypeSpecification::Time {
2304                minimum,
2305                maximum,
2306                help,
2307            } => match command {
2308                TypeConstraintCommand::Minimum => {
2309                    let t = require_time_literal(args, "minimum")?;
2310                    *minimum = Some(t);
2311                }
2312                TypeConstraintCommand::Maximum => {
2313                    let t = require_time_literal(args, "maximum")?;
2314                    *maximum = Some(t);
2315                }
2316                TypeConstraintCommand::Help => {
2317                    apply_type_help_command(help, args)?;
2318                }
2319                TypeConstraintCommand::Suggest => {
2320                    let lit = require_literal(args, "suggest")?;
2321                    reject_calendar_for_suggestion(
2322                        lit,
2323                        type_name,
2324                        SuggestionExpectation::Time,
2325                        None,
2326                    )?;
2327                    match lit {
2328                        crate::literals::Value::Time(t) => {
2329                            *declared_suggestion =
2330                                Some(RawSuggestion::Value(ValueKind::Time(time_to_semantic(t))));
2331                        }
2332                        _ => {
2333                            return Err(
2334                                "Please provide a time, for example `-> suggest 09:00:00`."
2335                                    .to_string(),
2336                            );
2337                        }
2338                    }
2339                }
2340                _ => {
2341                    return Err(format!(
2342                        "Invalid command '{}' for time type. Valid commands: minimum, maximum, help, suggest",
2343                        command
2344                    ));
2345                }
2346            },
2347            TypeSpecification::TimeRange {
2348                lower,
2349                upper,
2350                minimum,
2351                maximum,
2352                help,
2353            } => match command {
2354                TypeConstraintCommand::Lower => {
2355                    *lower = Some(require_time_literal(args, "lower")?);
2356                }
2357                TypeConstraintCommand::Upper => {
2358                    *upper = Some(require_time_literal(args, "upper")?);
2359                }
2360                TypeConstraintCommand::Minimum => {
2361                    *minimum = Some(parse_unresolved_width_bound(args, "minimum")?);
2362                }
2363                TypeConstraintCommand::Maximum => {
2364                    *maximum = Some(parse_unresolved_width_bound(args, "maximum")?);
2365                }
2366                TypeConstraintCommand::Help => {
2367                    apply_type_help_command(help, args)?;
2368                }
2369                TypeConstraintCommand::Suggest => {
2370                    let (left, right) = require_suggestion_range_endpoints(
2371                        args,
2372                        type_name,
2373                        SuggestionExpectation::TimeRange,
2374                        None,
2375                    )?;
2376                    let left = literal_value_from_parser_value(left)?;
2377                    let right = literal_value_from_parser_value(right)?;
2378                    if !left.lemma_type.is_time() || !right.lemma_type.is_time() {
2379                        return Err(
2380                            "Please provide a time range, for example `-> suggest 09:00...17:00`."
2381                                .to_string(),
2382                        );
2383                    }
2384                    *declared_suggestion = Some(RawSuggestion::Value(ValueKind::Range(
2385                        Box::new(left),
2386                        Box::new(right),
2387                    )));
2388                }
2389                _ => {
2390                    return Err(format!(
2391                        "Invalid command '{}' for time range type. Valid commands: lower, upper, minimum, maximum, help, suggest",
2392                        command
2393                    ));
2394                }
2395            },
2396            TypeSpecification::MeasureRange {
2397                lower,
2398                upper,
2399                minimum,
2400                maximum,
2401                units,
2402                decomposition,
2403                help,
2404            } => match command {
2405                TypeConstraintCommand::Unit => {
2406                    let (unit_name, value, derived_measure_factors) = match args {
2407                        [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
2408                            (name.clone(), *v, Vec::new())
2409                        }
2410                        [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Expr(
2411                            prefix,
2412                            factors,
2413                        ))] => {
2414                            let raw: Vec<(String, i32)> = factors
2415                                .iter()
2416                                .map(|f| (f.measure_ref.clone(), f.exp))
2417                                .collect();
2418                            (name.clone(), *prefix, raw)
2419                        }
2420                        _ => {
2421                            return Err(
2422                                "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')"
2423                                    .to_string(),
2424                            );
2425                        }
2426                    };
2427                    if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
2428                        let new_factor = crate::computation::rational::decimal_to_rational(value)
2429                            .map_err(|failure| failure.to_string())?;
2430                        if existing.factor != new_factor
2431                            || existing.derived_measure_factors != derived_measure_factors
2432                        {
2433                            return Err(format!(
2434                                "Unit '{unit_name}' is already defined in this type's inherited units; \
2435                                 cannot change factor or decomposition. Add a new unit name instead."
2436                            ));
2437                        }
2438                    } else {
2439                        units.0.push(MeasureUnit::from_decimal_factor(
2440                            unit_name,
2441                            value,
2442                            derived_measure_factors,
2443                        )?);
2444                    }
2445                }
2446                TypeConstraintCommand::Lower => {
2447                    *lower = Some(parse_measure_declared_bound(
2448                        args, "lower", units, type_name,
2449                    )?);
2450                }
2451                TypeConstraintCommand::Upper => {
2452                    *upper = Some(parse_measure_declared_bound(
2453                        args, "upper", units, type_name,
2454                    )?);
2455                }
2456                TypeConstraintCommand::Minimum => {
2457                    let width = parse_measure_declared_bound(args, "minimum", units, type_name)?;
2458                    reject_negative_width_magnitude(&width.0, "minimum")?;
2459                    *minimum = Some(width);
2460                }
2461                TypeConstraintCommand::Maximum => {
2462                    let width = parse_measure_declared_bound(args, "maximum", units, type_name)?;
2463                    reject_negative_width_magnitude(&width.0, "maximum")?;
2464                    *maximum = Some(width);
2465                }
2466                TypeConstraintCommand::Help => {
2467                    apply_type_help_command(help, args)?;
2468                }
2469                TypeConstraintCommand::Suggest => {
2470                    let (left, right) = require_suggestion_range_endpoints(
2471                        args,
2472                        type_name,
2473                        SuggestionExpectation::MeasureRange,
2474                        Some(units),
2475                    )?;
2476                    let element_spec = TypeSpecification::MeasureRange {
2477                        lower: lower.clone(),
2478                        upper: upper.clone(),
2479                        minimum: minimum.clone(),
2480                        maximum: maximum.clone(),
2481                        units: units.clone(),
2482                        decomposition: decomposition.clone(),
2483                        help: help.clone(),
2484                    }
2485                    .element_from_range()
2486                    .expect("BUG: MeasureRange must define element_from_range");
2487                    let left = lift_range_endpoint(left, &element_spec)?;
2488                    let right = lift_range_endpoint(right, &element_spec)?;
2489                    if !left.lemma_type.is_measure() || !right.lemma_type.is_measure() {
2490                        return Err(format!(
2491                            "Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
2492                        ));
2493                    }
2494                    *declared_suggestion = Some(RawSuggestion::Value(ValueKind::Range(
2495                        Box::new(left),
2496                        Box::new(right),
2497                    )));
2498                }
2499                _ => {
2500                    return Err(format!(
2501                        "Invalid command '{}' for measure range type. Valid commands: unit, lower, upper, minimum, maximum, help, suggest",
2502                        command
2503                    ));
2504                }
2505            },
2506            TypeSpecification::Veto { .. } => {
2507                return Err(format!(
2508                    "Invalid command '{}' for veto type. Veto is not a user-declarable type and cannot have constraints",
2509                    command
2510                ));
2511            }
2512            TypeSpecification::Undetermined => {
2513                return Err(format!(
2514                    "Invalid command '{}' for undetermined sentinel type. Undetermined is an internal type used during type inference and cannot have constraints",
2515                    command
2516                ));
2517            }
2518        }
2519        Ok(())
2520    }
2521}
2522
2523/// Parse a "number unit" string into a Measure or Ratio value according to the type.
2524/// Caller must have obtained the TypeSpecification via unit_index from the unit in the string.
2525pub fn parse_number_unit(
2526    value_str: &str,
2527    type_spec: &TypeSpecification,
2528) -> Result<crate::parsing::ast::Value, String> {
2529    use crate::literals::{NumberWithUnit, RatioLiteral};
2530    use crate::parsing::ast::Value;
2531
2532    let trimmed = value_str.trim();
2533    match type_spec {
2534        TypeSpecification::Measure { units, .. } => {
2535            if units.is_empty() {
2536                unreachable!(
2537                    "BUG: Measure type has no units; should have been validated during planning"
2538                );
2539            }
2540            match trimmed.parse::<NumberWithUnit>() {
2541                Ok(n) => {
2542                    let unit = units.get(&n.1).map_err(|e| e.to_string())?;
2543                    Ok(Value::NumberWithUnit(n.0, unit.name.clone()))
2544                }
2545                Err(e) => {
2546                    if trimmed.split_whitespace().count() == 1 && !trimmed.is_empty() {
2547                        let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
2548                        let example_unit = units
2549                            .iter()
2550                            .next()
2551                            .expect("BUG: units non-empty after guard")
2552                            .name
2553                            .as_str();
2554                        Err(format!(
2555                            "Measure value must include a unit, for example: '{} {}'. Valid units: {}.",
2556                            trimmed,
2557                            example_unit,
2558                            valid.join(", ")
2559                        ))
2560                    } else {
2561                        Err(e)
2562                    }
2563                }
2564            }
2565        }
2566        TypeSpecification::Ratio { units, .. } => {
2567            if units.is_empty() {
2568                unreachable!(
2569                    "BUG: Ratio type has no units; should have been validated during planning"
2570                );
2571            }
2572            match trimmed.parse::<RatioLiteral>()? {
2573                RatioLiteral::Bare(_) => {
2574                    Err("Ratio value requires a unit (e.g. '50%', '500 basis_points').".to_string())
2575                }
2576                RatioLiteral::Percent(n) => {
2577                    let unit = units.get("percent").map_err(|e| e.to_string())?;
2578                    Ok(Value::NumberWithUnit(n, unit.name.clone()))
2579                }
2580                RatioLiteral::Permille(n) => {
2581                    let unit = units.get("permille").map_err(|e| e.to_string())?;
2582                    Ok(Value::NumberWithUnit(n, unit.name.clone()))
2583                }
2584                RatioLiteral::Named { value, unit } => {
2585                    let resolved = units.get(&unit).map_err(|e| e.to_string())?;
2586                    Ok(Value::NumberWithUnit(value, resolved.name.clone()))
2587                }
2588            }
2589        }
2590        _ => Err("parse_number_unit only accepts Measure or Ratio type".to_string()),
2591    }
2592}
2593
2594/// Parse a string value according to a TypeSpecification.
2595/// Used to parse runtime user input into typed values.
2596pub fn parse_value_from_string(
2597    value_str: &str,
2598    type_spec: &TypeSpecification,
2599    source: &Source,
2600) -> Result<crate::parsing::ast::Value, Error> {
2601    use crate::parsing::ast::Value;
2602
2603    let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
2604
2605    let parse_range_value = |element_spec: TypeSpecification| -> Result<Value, Error> {
2606        let (left_str, right_str) = value_str.split_once("...").ok_or_else(|| {
2607            to_err("Range value must use '...' between the two endpoints".to_string())
2608        })?;
2609        if left_str.trim().is_empty() || right_str.trim().is_empty() {
2610            return Err(to_err(
2611                "Range value must contain a non-empty left and right endpoint".to_string(),
2612            ));
2613        }
2614        let left = parse_value_from_string(left_str.trim(), &element_spec, source)?;
2615        let right = parse_value_from_string(right_str.trim(), &element_spec, source)?;
2616        Ok(Value::Range(Box::new(left), Box::new(right)))
2617    };
2618
2619    match type_spec {
2620        TypeSpecification::Text { .. } => value_str
2621            .parse::<crate::literals::TextLiteral>()
2622            .map(|t| Value::Text(t.0))
2623            .map_err(to_err),
2624        TypeSpecification::Number { .. } => value_str
2625            .parse::<crate::literals::NumberLiteral>()
2626            .map(|n| Value::Number(n.0))
2627            .map_err(to_err),
2628        TypeSpecification::Measure { .. } => {
2629            parse_number_unit(value_str, type_spec).map_err(to_err)
2630        }
2631        TypeSpecification::Boolean { .. } => value_str
2632            .parse::<BooleanValue>()
2633            .map(Value::Boolean)
2634            .map_err(to_err),
2635        TypeSpecification::Date { .. } => {
2636            let date = value_str.parse::<DateTimeValue>().map_err(to_err)?;
2637            Ok(Value::Date(date))
2638        }
2639        TypeSpecification::Time { .. } => {
2640            let time = value_str.parse::<TimeValue>().map_err(to_err)?;
2641            Ok(Value::Time(time))
2642        }
2643        TypeSpecification::Ratio { .. } => {
2644            parse_number_unit(value_str, type_spec).map_err(to_err)
2645        }
2646        TypeSpecification::NumberRange { .. }
2647        | TypeSpecification::MeasureRange { .. }
2648        | TypeSpecification::DateRange { .. }
2649        | TypeSpecification::TimeRange { .. }
2650        | TypeSpecification::RatioRange { .. } => {
2651            let element_spec = range_element_type_specification(type_spec).unwrap_or_else(|| {
2652                unreachable!("BUG: range_element_type_specification missing arm for known range type")
2653            });
2654            parse_range_value(element_spec)
2655        }
2656        TypeSpecification::Veto { .. } => Err(to_err(
2657            "Veto type cannot be parsed from string".to_string(),
2658        )),
2659        TypeSpecification::Undetermined => unreachable!(
2660            "BUG: parse_value_from_string called with Undetermined sentinel type; this type exists only during type inference"
2661        ),
2662    }
2663}
2664
2665// -----------------------------------------------------------------------------
2666// Semantic value types (no parser dependency - used by evaluation, inversion, etc.)
2667// -----------------------------------------------------------------------------
2668
2669#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2670#[serde(rename_all = "snake_case")]
2671pub enum SemanticCalendarUnit {
2672    Month,
2673    Year,
2674}
2675
2676impl fmt::Display for SemanticCalendarUnit {
2677    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2678        let s = match self {
2679            SemanticCalendarUnit::Month => "month",
2680            SemanticCalendarUnit::Year => "year",
2681        };
2682        write!(f, "{}", s)
2683    }
2684}
2685
2686pub fn semantic_calendar_unit_from_unit_name(unit_name: &str) -> SemanticCalendarUnit {
2687    match unit_name {
2688        "month" => SemanticCalendarUnit::Month,
2689        "year" => SemanticCalendarUnit::Year,
2690        other => unreachable!(
2691            "BUG: calendar measure signature unit must be month or year, got '{other}'"
2692        ),
2693    }
2694}
2695
2696pub fn semantic_calendar_unit_from_measure_signature(
2697    signature: &[(String, i32)],
2698) -> SemanticCalendarUnit {
2699    let unit_name = signature
2700        .first()
2701        .map(|(name, _)| name.as_str())
2702        .expect("BUG: calendar measure must carry a unit signature");
2703    semantic_calendar_unit_from_unit_name(unit_name)
2704}
2705
2706mod arc_lemma_type {
2707    use super::LemmaType;
2708    use serde::{Deserialize, Deserializer, Serialize, Serializer};
2709    use std::sync::Arc;
2710
2711    pub fn serialize<S>(value: &Arc<LemmaType>, serializer: S) -> Result<S::Ok, S::Error>
2712    where
2713        S: Serializer,
2714    {
2715        value.as_ref().serialize(serializer)
2716    }
2717
2718    pub fn deserialize<'de, D>(deserializer: D) -> Result<Arc<LemmaType>, D::Error>
2719    where
2720        D: Deserializer<'de>,
2721    {
2722        LemmaType::deserialize(deserializer).map(Arc::new)
2723    }
2724}
2725
2726/// Target type for `as` casts (semantic; used by evaluation/computation).
2727#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2728#[serde(rename_all = "snake_case")]
2729pub enum SemanticConversionTarget {
2730    Type(PrimitiveKind),
2731    /// `number as eur` — construct, convert, relabel, or range-span into `unit_name`.
2732    Unit {
2733        unit_name: String,
2734        /// Measure/ratio type resolved in the spec where the conversion was written.
2735        #[serde(with = "arc_lemma_type")]
2736        owning_type: Arc<LemmaType>,
2737    },
2738}
2739
2740impl std::hash::Hash for SemanticConversionTarget {
2741    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
2742        match self {
2743            Self::Type(kind) => {
2744                0u8.hash(state);
2745                kind.hash(state);
2746            }
2747            Self::Unit {
2748                unit_name,
2749                owning_type,
2750            } => {
2751                1u8.hash(state);
2752                unit_name.hash(state);
2753                owning_type.hash(state);
2754            }
2755        }
2756    }
2757}
2758
2759impl SemanticConversionTarget {}
2760
2761impl fmt::Display for SemanticConversionTarget {
2762    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2763        match self {
2764            SemanticConversionTarget::Type(kind) => write!(f, "{kind}"),
2765            SemanticConversionTarget::Unit { unit_name, .. } => write!(f, "{unit_name}"),
2766        }
2767    }
2768}
2769
2770/// Timezone for semantic date/time values
2771#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2772pub struct SemanticTimezone {
2773    pub offset_hours: i8,
2774    pub offset_minutes: u8,
2775}
2776
2777impl fmt::Display for SemanticTimezone {
2778    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2779        if self.offset_hours == 0 && self.offset_minutes == 0 {
2780            write!(f, "Z")
2781        } else {
2782            let sign = if self.offset_hours >= 0 { "+" } else { "-" };
2783            let hour = self.offset_hours.abs();
2784            write!(f, "{}{:02}:{:02}", sign, hour, self.offset_minutes)
2785        }
2786    }
2787}
2788
2789impl Serialize for SemanticTimezone {
2790    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2791        serializer.serialize_str(&self.to_string())
2792    }
2793}
2794
2795impl<'de> Deserialize<'de> for SemanticTimezone {
2796    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2797        let s = String::deserialize(deserializer)?;
2798        Self::from_str(&s).map_err(serde::de::Error::custom)
2799    }
2800}
2801
2802impl FromStr for SemanticTimezone {
2803    type Err = String;
2804
2805    fn from_str(s: &str) -> Result<Self, Self::Err> {
2806        let tz = TimezoneValue::from_str(s)?;
2807        Ok(Self {
2808            offset_hours: tz.offset_hours,
2809            offset_minutes: tz.offset_minutes,
2810        })
2811    }
2812}
2813
2814/// Time-of-day for semantic values
2815#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2816pub struct SemanticTime {
2817    pub hour: u32,
2818    pub minute: u32,
2819    pub second: u32,
2820    pub microsecond: u32,
2821    pub timezone: Option<SemanticTimezone>,
2822}
2823
2824impl fmt::Display for SemanticTime {
2825    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2826        write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
2827        if self.microsecond != 0 {
2828            write!(f, ".{:06}", self.microsecond)?;
2829        }
2830        if let Some(timezone) = &self.timezone {
2831            write!(f, "{}", timezone)?;
2832        }
2833        Ok(())
2834    }
2835}
2836
2837impl Serialize for SemanticTime {
2838    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2839        serializer.serialize_str(&self.to_string())
2840    }
2841}
2842
2843impl<'de> Deserialize<'de> for SemanticTime {
2844    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2845        let s = String::deserialize(deserializer)?;
2846        Self::from_str(&s).map_err(serde::de::Error::custom)
2847    }
2848}
2849
2850impl FromStr for SemanticTime {
2851    type Err = String;
2852
2853    fn from_str(s: &str) -> Result<Self, Self::Err> {
2854        Ok(time_to_semantic(&TimeValue::from_str(s)?))
2855    }
2856}
2857
2858/// Date-time for semantic values
2859#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2860pub struct SemanticDateTime {
2861    pub year: i32,
2862    pub month: u32,
2863    pub day: u32,
2864    pub hour: u32,
2865    pub minute: u32,
2866    pub second: u32,
2867    pub microsecond: u32,
2868    pub timezone: Option<SemanticTimezone>,
2869}
2870
2871impl fmt::Display for SemanticDateTime {
2872    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2873        let has_time = self.hour != 0
2874            || self.minute != 0
2875            || self.second != 0
2876            || self.microsecond != 0
2877            || self.timezone.is_some();
2878        if !has_time {
2879            write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
2880        } else {
2881            write!(
2882                f,
2883                "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
2884                self.year, self.month, self.day, self.hour, self.minute, self.second
2885            )?;
2886            if self.microsecond != 0 {
2887                write!(f, ".{:06}", self.microsecond)?;
2888            }
2889            if let Some(tz) = &self.timezone {
2890                write!(f, "{}", tz)?;
2891            }
2892            Ok(())
2893        }
2894    }
2895}
2896
2897impl Serialize for SemanticDateTime {
2898    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2899        serializer.serialize_str(&self.to_string())
2900    }
2901}
2902
2903impl<'de> Deserialize<'de> for SemanticDateTime {
2904    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2905        let s = String::deserialize(deserializer)?;
2906        Self::from_str(&s).map_err(serde::de::Error::custom)
2907    }
2908}
2909
2910impl FromStr for SemanticDateTime {
2911    type Err = String;
2912
2913    fn from_str(s: &str) -> Result<Self, Self::Err> {
2914        Ok(date_time_to_semantic(&DateTimeValue::from_str(s)?))
2915    }
2916}
2917
2918/// Default captured during type constraint application, before measure unit factors are final.
2919/// Converted into [`ValueKind`] after `resolve_measure_decompositions` (or immediately for
2920/// reference-local defaults, which run after that pass).
2921#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2922pub enum RawSuggestion {
2923    Value(ValueKind),
2924    Measure {
2925        magnitude: RationalInteger,
2926        unit_name: String,
2927    },
2928}
2929
2930pub fn value_kind_from_raw_suggestion(
2931    raw: RawSuggestion,
2932    specifications: &TypeSpecification,
2933    type_name: &str,
2934) -> Result<ValueKind, String> {
2935    match raw {
2936        RawSuggestion::Value(vk) => Ok(vk),
2937        RawSuggestion::Measure {
2938            magnitude,
2939            unit_name,
2940        } => {
2941            let TypeSpecification::Measure { units, .. } = specifications else {
2942                return Err(format!(
2943                    "BUG: RawSuggestion::Measure for non-measure type '{type_name}'"
2944                ));
2945            };
2946            let canonical = measure_declared_bound_to_canonical(
2947                &magnitude, &unit_name, units, type_name, "suggest",
2948            )?;
2949            Ok(ValueKind::Measure(canonical, vec![(unit_name, 1)]))
2950        }
2951    }
2952}
2953
2954/// Value payload (shape of a literal). No type attached.
2955/// Measure unit is required; Ratio unit is optional (see plan ratio-units-optional.md).
2956#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2957pub enum ValueKind {
2958    Number(RationalInteger),
2959    /// Measure: magnitude in canonical (base-unit) space + unit signature.
2960    ///
2961    /// At bind time the user-facing value is multiplied by `unit.factor` to produce
2962    /// the stored magnitude; API unit maps divide back. The signature carries the
2963    /// declared unit name(s) and is always in canonical form (sorted, no zero exponents).
2964    Measure(RationalInteger, Vec<(String, i32)>),
2965    Text(String),
2966    Date(SemanticDateTime),
2967    Time(SemanticTime),
2968    Boolean(bool),
2969    /// Ratio: value + optional unit
2970    Ratio(RationalInteger, Option<String>),
2971    Range(Box<LiteralValue>, Box<LiteralValue>),
2972}
2973
2974impl ValueKind {
2975    /// Decimal magnitude for numeric variants (number, measure, ratio).
2976    pub fn as_decimal_magnitude(&self) -> Result<Decimal, String> {
2977        match self {
2978            ValueKind::Number(n) | ValueKind::Measure(n, _) | ValueKind::Ratio(n, _) => {
2979                n.try_to_decimal().map_err(|failure| failure.to_string())
2980            }
2981            other => Err(format!("expected numeric value kind, got {other}")),
2982        }
2983    }
2984}
2985
2986fn format_rational_magnitude_for_display(rational: &RationalInteger) -> String {
2987    rational.display_str()
2988}
2989
2990fn format_number_with_unit_for_display(rational: &RationalInteger, unit: &str) -> String {
2991    use crate::parsing::ast::Value;
2992    match rational.try_to_decimal() {
2993        Ok(decimal) => format!("{}", Value::NumberWithUnit(decimal, unit.to_string())),
2994        Err(_) => format!("{} {}", rational.display_str(), unit),
2995    }
2996}
2997
2998impl fmt::Display for ValueKind {
2999    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3000        use crate::computation::rational::checked_mul;
3001        match self {
3002            ValueKind::Number(rational) => {
3003                write!(f, "{}", format_rational_magnitude_for_display(rational))
3004            }
3005            ValueKind::Measure(rational, signature) => {
3006                let unit = signature.first().map(|(n, _)| n.as_str()).unwrap_or("");
3007                write!(f, "{}", format_number_with_unit_for_display(rational, unit))
3008            }
3009            ValueKind::Text(s) => write!(f, "{}", crate::parsing::ast::Value::Text(s.clone())),
3010            ValueKind::Ratio(rational, unit) => match unit.as_deref() {
3011                Some("percent") => {
3012                    let display = match checked_mul(rational, &rational_new(100, 1)) {
3013                        Ok(scaled) => format_number_with_unit_for_display(&scaled, "percent"),
3014                        Err(_) => format!("{} percent", rational.display_str()),
3015                    };
3016                    write!(f, "{}", display)
3017                }
3018                Some("permille") => {
3019                    let display = match checked_mul(rational, &rational_new(1000, 1)) {
3020                        Ok(scaled) => format_number_with_unit_for_display(&scaled, "permille"),
3021                        Err(_) => format!("{} permille", rational.display_str()),
3022                    };
3023                    write!(f, "{}", display)
3024                }
3025                Some(unit_name) => {
3026                    write!(
3027                        f,
3028                        "{}",
3029                        format_number_with_unit_for_display(rational, unit_name)
3030                    )
3031                }
3032                None => write!(f, "{}", format_rational_magnitude_for_display(rational)),
3033            },
3034            ValueKind::Date(dt) => write!(f, "{}", dt),
3035            ValueKind::Time(t) => write!(
3036                f,
3037                "{}",
3038                crate::parsing::ast::Value::Time(crate::parsing::ast::TimeValue {
3039                    hour: t.hour as u8,
3040                    minute: t.minute as u8,
3041                    second: t.second as u8,
3042                    microsecond: t.microsecond,
3043                    timezone: t
3044                        .timezone
3045                        .as_ref()
3046                        .map(|tz| crate::parsing::ast::TimezoneValue {
3047                            offset_hours: tz.offset_hours,
3048                            offset_minutes: tz.offset_minutes,
3049                        }),
3050                })
3051            ),
3052            ValueKind::Boolean(b) => write!(f, "{}", b),
3053            ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
3054        }
3055    }
3056}
3057
3058fn decimal_from_serialized_str(s: &str) -> Result<Decimal, String> {
3059    Decimal::from_str(s.trim()).map_err(|e| format!("invalid decimal '{s}': {e}"))
3060}
3061
3062#[derive(Serialize, Deserialize)]
3063struct SerializedValueUnit {
3064    value: String,
3065    unit: String,
3066}
3067
3068#[derive(Serialize, Deserialize)]
3069struct SerializedRatio {
3070    value: String,
3071    unit: Option<String>,
3072}
3073
3074#[derive(Serialize, Deserialize)]
3075struct SerializedMeasure {
3076    value: String,
3077    signature: Vec<(String, i32)>,
3078}
3079
3080#[derive(Serialize, Deserialize)]
3081struct SerializedRange {
3082    from: ValueKind,
3083    to: ValueKind,
3084}
3085
3086impl Serialize for ValueKind {
3087    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3088        use serde::ser::SerializeMap;
3089        let mut map = serializer.serialize_map(Some(1))?;
3090        match self {
3091            ValueKind::Number(rational) => {
3092                map.serialize_entry(
3093                    "number",
3094                    &crate::literals::rational_to_serialized_str(rational)
3095                        .map_err(serde::ser::Error::custom)?,
3096                )?;
3097            }
3098            ValueKind::Measure(rational, signature) => {
3099                map.serialize_entry(
3100                    "measure",
3101                    &SerializedMeasure {
3102                        value: crate::literals::rational_to_serialized_str(rational)
3103                            .map_err(serde::ser::Error::custom)?,
3104                        signature: signature.clone(),
3105                    },
3106                )?;
3107            }
3108            ValueKind::Text(s) => {
3109                map.serialize_entry("text", s)?;
3110            }
3111            ValueKind::Date(dt) => {
3112                map.serialize_entry("date", dt)?;
3113            }
3114            ValueKind::Time(t) => {
3115                map.serialize_entry("time", t)?;
3116            }
3117            ValueKind::Boolean(b) => {
3118                map.serialize_entry("boolean", b)?;
3119            }
3120            ValueKind::Ratio(rational, unit) => {
3121                map.serialize_entry(
3122                    "ratio",
3123                    &SerializedRatio {
3124                        value: crate::literals::rational_to_serialized_str(rational)
3125                            .map_err(serde::ser::Error::custom)?,
3126                        unit: unit.clone(),
3127                    },
3128                )?;
3129            }
3130            ValueKind::Range(left, right) => {
3131                map.serialize_entry(
3132                    "range",
3133                    &SerializedRange {
3134                        from: left.value.clone(),
3135                        to: right.value.clone(),
3136                    },
3137                )?;
3138            }
3139        }
3140        map.end()
3141    }
3142}
3143
3144impl<'de> Deserialize<'de> for ValueKind {
3145    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3146        let map = <serde_json::Map<String, serde_json::Value>>::deserialize(deserializer)?;
3147        if map.len() != 1 {
3148            return Err(serde::de::Error::custom(format!(
3149                "ValueKind must have exactly one variant key, got {}",
3150                map.len()
3151            )));
3152        }
3153        let (tag, payload) = map.into_iter().next().expect("BUG: len checked");
3154        deserialize_value_kind_variant(&tag, payload).map_err(serde::de::Error::custom)
3155    }
3156}
3157
3158fn deserialize_value_kind_variant(
3159    tag: &str,
3160    payload: serde_json::Value,
3161) -> Result<ValueKind, String> {
3162    match tag {
3163        "number" => {
3164            let s = payload
3165                .as_str()
3166                .ok_or_else(|| "number must be a JSON string".to_string())?;
3167            let decimal = decimal_from_serialized_str(s)?;
3168            Ok(ValueKind::Number(
3169                crate::literals::rational_from_parsed_decimal(decimal)?,
3170            ))
3171        }
3172        "measure" => {
3173            let pair: SerializedMeasure =
3174                serde_json::from_value(payload).map_err(|e| e.to_string())?;
3175            let decimal = decimal_from_serialized_str(&pair.value)?;
3176            Ok(ValueKind::Measure(
3177                crate::literals::rational_from_parsed_decimal(decimal)?,
3178                pair.signature,
3179            ))
3180        }
3181        "ratio" => {
3182            let pair: SerializedRatio =
3183                serde_json::from_value(payload).map_err(|e| e.to_string())?;
3184            let decimal = decimal_from_serialized_str(&pair.value)?;
3185            Ok(ValueKind::Ratio(
3186                crate::literals::rational_from_parsed_decimal(decimal)?,
3187                pair.unit,
3188            ))
3189        }
3190        "calendar" => {
3191            let pair: SerializedValueUnit =
3192                serde_json::from_value(payload).map_err(|e| e.to_string())?;
3193            let unit = match pair.unit.as_str() {
3194                "month" => SemanticCalendarUnit::Month,
3195                "year" => SemanticCalendarUnit::Year,
3196                other => {
3197                    return Err(format!(
3198                        "unknown calendar unit '{other}' (expected 'month' or 'year')"
3199                    ));
3200                }
3201            };
3202            let decimal = decimal_from_serialized_str(&pair.value)?;
3203            Ok(ValueKind::Measure(
3204                crate::literals::rational_from_parsed_decimal(decimal)?,
3205                vec![(unit.to_string(), 1)],
3206            ))
3207        }
3208        "text" => {
3209            let s = payload
3210                .as_str()
3211                .ok_or_else(|| "text must be a JSON string".to_string())?;
3212            Ok(ValueKind::Text(s.to_string()))
3213        }
3214        "date" => {
3215            let dt: SemanticDateTime =
3216                serde_json::from_value(payload).map_err(|e| e.to_string())?;
3217            Ok(ValueKind::Date(dt))
3218        }
3219        "time" => {
3220            let t: SemanticTime = serde_json::from_value(payload).map_err(|e| e.to_string())?;
3221            Ok(ValueKind::Time(t))
3222        }
3223        "boolean" => {
3224            let b = payload
3225                .as_bool()
3226                .ok_or_else(|| "boolean must be a JSON bool".to_string())?;
3227            Ok(ValueKind::Boolean(b))
3228        }
3229        "range" => {
3230            let range: SerializedRange =
3231                serde_json::from_value(payload).map_err(|e| e.to_string())?;
3232            Ok(ValueKind::Range(
3233                Box::new(LiteralValue {
3234                    value: range.from,
3235                    lemma_type: primitive_number_arc().clone(),
3236                }),
3237                Box::new(LiteralValue {
3238                    value: range.to,
3239                    lemma_type: primitive_number_arc().clone(),
3240                }),
3241            ))
3242        }
3243        other => Err(format!("unknown ValueKind variant '{other}'")),
3244    }
3245}
3246
3247// -----------------------------------------------------------------------------
3248// Resolved path types (moved from parsing::ast)
3249// -----------------------------------------------------------------------------
3250
3251/// A single segment in a resolved path traversal
3252///
3253/// Used in both DataPath and RulePath for cross-spec traversal.
3254/// Each segment contains a data name that resolves to another spec.
3255#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3256pub struct PathSegment {
3257    /// The data name in this segment
3258    pub data: String,
3259    /// The spec this data references (resolved during planning)
3260    pub spec: String,
3261}
3262
3263/// Resolved path to a data (created during planning from AST DataReference)
3264///
3265/// Represents a fully resolved path through specs to reach a datum.
3266#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3267pub struct DataPath {
3268    /// Path segments (each is a cross-spec step)
3269    pub segments: Vec<PathSegment>,
3270    /// Final data name
3271    pub data: String,
3272}
3273
3274impl DataPath {
3275    /// Create a data path from segments and data name (matches AST DataReference shape)
3276    pub fn new(segments: Vec<PathSegment>, data: String) -> Self {
3277        Self { segments, data }
3278    }
3279
3280    /// Create a local data path (no cross-spec steps)
3281    pub fn local(data: String) -> Self {
3282        Self {
3283            segments: vec![],
3284            data,
3285        }
3286    }
3287
3288    /// Dot-separated key used for matching user-provided data values (e.g. `"order.payment_method"`).
3289    /// Unlike `Display`, this omits the resolved spec name.
3290    pub fn input_key(&self) -> String {
3291        let mut s = String::new();
3292        for segment in &self.segments {
3293            s.push_str(&segment.data);
3294            s.push('.');
3295        }
3296        s.push_str(&self.data);
3297        s
3298    }
3299}
3300
3301/// Resolved path to a rule (created during planning from a rule reference).
3302///
3303/// Represents a fully resolved path through specs to reach a rule.
3304#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3305pub struct RulePath {
3306    /// Path segments (each is a cross-spec step)
3307    pub segments: Vec<PathSegment>,
3308    /// Final rule name
3309    pub rule: String,
3310}
3311
3312impl RulePath {
3313    /// Create a rule path from segments and rule name.
3314    pub fn new(segments: Vec<PathSegment>, rule: String) -> Self {
3315        Self { segments, rule }
3316    }
3317}
3318
3319// -----------------------------------------------------------------------------
3320// Resolved expression types (created during planning)
3321// -----------------------------------------------------------------------------
3322
3323/// Resolved expression (all references resolved to paths, all literals typed)
3324///
3325/// Created during planning from AST Expression. All unresolved references
3326/// are converted to DataPath/RulePath, and all literals are typed.
3327#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3328pub struct Expression {
3329    pub kind: ExpressionKind,
3330    pub source_location: Option<Source>,
3331}
3332
3333impl Expression {
3334    /// Create an expression with an optional source location.
3335    pub fn with_source(kind: ExpressionKind, source_location: Option<Source>) -> Self {
3336        Self {
3337            kind,
3338            source_location,
3339        }
3340    }
3341
3342    /// Collect all DataPath references from this resolved expression tree
3343    pub fn collect_data_paths(&self, data: &mut std::collections::HashSet<DataPath>) {
3344        self.kind.collect_data_paths(data);
3345    }
3346}
3347
3348/// Resolved expression kind (only resolved variants, no unresolved references)
3349#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3350#[serde(rename_all = "snake_case")]
3351pub enum ExpressionKind {
3352    /// Resolved literal with type (boxed to keep enum small)
3353    Literal(Box<LiteralValue>),
3354    /// Resolved data path
3355    DataPath(DataPath),
3356    /// Resolved rule path
3357    RulePath(RulePath),
3358    LogicalAnd(Arc<Expression>, Arc<Expression>),
3359    Arithmetic(Arc<Expression>, ArithmeticComputation, Arc<Expression>),
3360    Comparison(Arc<Expression>, ComparisonComputation, Arc<Expression>),
3361    UnitConversion(Arc<Expression>, SemanticConversionTarget),
3362    LogicalNegation(Arc<Expression>, NegationType),
3363    MathematicalComputation(MathematicalComputation, Arc<Expression>),
3364    Veto(VetoExpression),
3365    /// The `now` keyword — resolved at evaluation to the effective datetime.
3366    Now,
3367    /// Date-relative sugar: `<date_expr> in past` / `in future`
3368    DateRelative(DateRelativeKind, Arc<Expression>),
3369    /// Calendar-period sugar: `<date_expr> in [past|future] calendar year|month|week`
3370    DateCalendar(DateCalendarKind, CalendarPeriodUnit, Arc<Expression>),
3371    RangeLiteral(Arc<Expression>, Arc<Expression>),
3372    PastFutureRange(DateRelativeKind, Arc<Expression>),
3373    RangeContainment(Arc<Expression>, Arc<Expression>),
3374    /// Whether evaluating the operand produced a veto (no value). Parses as `is veto` syntax.
3375    ResultIsVeto(Arc<Expression>),
3376    /// Unless structure: (condition, result) pairs in source order; last true condition wins.
3377    /// First arm is the default (condition is always-true literal).
3378    Piecewise(Vec<(Arc<Expression>, Arc<Expression>)>),
3379}
3380
3381impl ExpressionKind {
3382    /// Collect all DataPath references from this expression kind
3383    pub(crate) fn collect_data_paths(&self, data: &mut std::collections::HashSet<DataPath>) {
3384        match self {
3385            ExpressionKind::DataPath(fp) => {
3386                data.insert(fp.clone());
3387            }
3388            ExpressionKind::LogicalAnd(left, right) => {
3389                left.collect_data_paths(data);
3390                right.collect_data_paths(data);
3391            }
3392            ExpressionKind::Arithmetic(left, _, right)
3393            | ExpressionKind::Comparison(left, _, right)
3394            | ExpressionKind::RangeLiteral(left, right)
3395            | ExpressionKind::RangeContainment(left, right) => {
3396                left.collect_data_paths(data);
3397                right.collect_data_paths(data);
3398            }
3399            ExpressionKind::UnitConversion(inner, _)
3400            | ExpressionKind::LogicalNegation(inner, _)
3401            | ExpressionKind::MathematicalComputation(_, inner)
3402            | ExpressionKind::PastFutureRange(_, inner) => {
3403                inner.collect_data_paths(data);
3404            }
3405            ExpressionKind::DateRelative(_, date_expr) => {
3406                date_expr.collect_data_paths(data);
3407            }
3408            ExpressionKind::DateCalendar(_, _, date_expr) => {
3409                date_expr.collect_data_paths(data);
3410            }
3411            ExpressionKind::Literal(_)
3412            | ExpressionKind::RulePath(_)
3413            | ExpressionKind::Veto(_)
3414            | ExpressionKind::Now => {}
3415            ExpressionKind::ResultIsVeto(operand) => {
3416                operand.collect_data_paths(data);
3417            }
3418            ExpressionKind::Piecewise(arms) => {
3419                for (condition, result) in arms {
3420                    condition.collect_data_paths(data);
3421                    result.collect_data_paths(data);
3422                }
3423            }
3424        }
3425    }
3426}
3427
3428// -----------------------------------------------------------------------------
3429// Resolved types and values
3430// -----------------------------------------------------------------------------
3431
3432/// Where the custom extension chain is rooted: same spec as this type, or imported from another resolved spec.
3433#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
3434#[serde(tag = "kind", rename_all = "snake_case")]
3435pub enum TypeDefiningSpec {
3436    /// Parent type is defined in the same spec as this type.
3437    Local,
3438    /// Parent type was resolved from types loaded from another spec.
3439    Import,
3440}
3441
3442/// What this type extends (primitive built-in or custom type by name).
3443#[derive(Clone, Debug, Serialize, Deserialize)]
3444#[serde(tag = "kind", rename_all = "snake_case")]
3445pub enum TypeExtends {
3446    /// Extends a primitive built-in type (number, boolean, text, etc.)
3447    Primitive,
3448    /// Extends a custom type: parent is the immediate parent type name; family is the root of the extension chain (topmost custom type name).
3449    /// `defining_spec` records whether the parent chain is local or imported from another spec.
3450    Custom {
3451        parent: String,
3452        family: String,
3453        defining_spec: TypeDefiningSpec,
3454    },
3455}
3456
3457impl PartialEq for TypeExtends {
3458    fn eq(&self, other: &Self) -> bool {
3459        match (self, other) {
3460            (TypeExtends::Primitive, TypeExtends::Primitive) => true,
3461            (
3462                TypeExtends::Custom {
3463                    parent: lp,
3464                    family: lf,
3465                    defining_spec: ld,
3466                },
3467                TypeExtends::Custom {
3468                    parent: rp,
3469                    family: rf,
3470                    defining_spec: rd,
3471                },
3472            ) => lp == rp && lf == rf && ld == rd,
3473            _ => false,
3474        }
3475    }
3476}
3477
3478impl Eq for TypeExtends {}
3479
3480impl std::hash::Hash for TypeExtends {
3481    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
3482        match self {
3483            TypeExtends::Primitive => {
3484                0u8.hash(state);
3485            }
3486            TypeExtends::Custom {
3487                parent,
3488                family,
3489                defining_spec,
3490            } => {
3491                1u8.hash(state);
3492                parent.hash(state);
3493                family.hash(state);
3494                defining_spec.hash(state);
3495            }
3496        }
3497    }
3498}
3499
3500impl TypeExtends {
3501    /// Custom extension in the same spec as the defining type (no cross-spec import for the parent chain).
3502    #[must_use]
3503    pub fn custom_local(parent: String, family: String) -> Self {
3504        TypeExtends::Custom {
3505            parent,
3506            family,
3507            defining_spec: TypeDefiningSpec::Local,
3508        }
3509    }
3510
3511    /// Returns the parent type name if this type extends a custom type.
3512    #[must_use]
3513    pub fn parent_name(&self) -> Option<&str> {
3514        match self {
3515            TypeExtends::Primitive => None,
3516            TypeExtends::Custom { parent, .. } => Some(parent.as_str()),
3517        }
3518    }
3519}
3520
3521/// Resolved type after planning
3522///
3523/// Contains a type specification and optional name. Created during planning
3524/// from TypeSpecification in the AST.
3525#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
3526pub struct LemmaType {
3527    /// Optional type name (e.g., "age", "temperature")
3528    pub name: Option<String>,
3529    /// The type specification (Boolean, Number, Measure, etc.).
3530    /// Serialized as a discriminated union: the variant tag appears as
3531    /// `"kind"` alongside `name` and `extends`, and the variant's fields
3532    /// are flattened to the top level.
3533    #[serde(flatten)]
3534    pub specifications: TypeSpecification,
3535    /// What this type extends (primitive or custom from a spec)
3536    pub extends: TypeExtends,
3537}
3538
3539impl LemmaType {
3540    /// Functional update of the `Measure` payload (units + decomposition).
3541    /// Non-Measure variants pass through unchanged. The transform receives the owned
3542    /// units and decomposition and returns the replacements.
3543    pub fn map_measure<F>(self, f: F) -> Self
3544    where
3545        F: FnOnce(
3546            MeasureUnits,
3547            Option<BaseMeasureVector>,
3548        ) -> (MeasureUnits, Option<BaseMeasureVector>),
3549    {
3550        let LemmaType {
3551            name,
3552            specifications,
3553            extends,
3554        } = self;
3555        let specifications = match specifications {
3556            TypeSpecification::Measure {
3557                minimum,
3558                maximum,
3559                decimals,
3560                units,
3561                traits,
3562                decomposition,
3563                help,
3564            } => {
3565                let (units, decomposition) = f(units, decomposition);
3566                TypeSpecification::Measure {
3567                    minimum,
3568                    maximum,
3569                    decimals,
3570                    units,
3571                    traits,
3572                    decomposition,
3573                    help,
3574                }
3575            }
3576            other => other,
3577        };
3578        LemmaType {
3579            name,
3580            specifications,
3581            extends,
3582        }
3583    }
3584
3585    /// Create a new type with a name
3586    pub fn new(name: String, specifications: TypeSpecification, extends: TypeExtends) -> Self {
3587        Self {
3588            name: Some(name),
3589            specifications,
3590            extends,
3591        }
3592    }
3593
3594    /// Create a type without a name (anonymous/inline type)
3595    pub fn without_name(specifications: TypeSpecification, extends: TypeExtends) -> Self {
3596        Self {
3597            name: None,
3598            specifications,
3599            extends,
3600        }
3601    }
3602
3603    /// Create a primitive type (no name, extends Primitive)
3604    pub fn primitive(specifications: TypeSpecification) -> Self {
3605        Self {
3606            name: None,
3607            specifications,
3608            extends: TypeExtends::Primitive,
3609        }
3610    }
3611
3612    /// Get the type name, or a default based on the type specification
3613    pub fn name(&self) -> String {
3614        self.name
3615            .clone()
3616            .unwrap_or_else(|| self.specifications.to_string())
3617    }
3618
3619    /// Check if this type is boolean
3620    pub fn is_boolean(&self) -> bool {
3621        matches!(&self.specifications, TypeSpecification::Boolean { .. })
3622    }
3623
3624    pub fn matches_primitive_kind(&self, kind: PrimitiveKind) -> bool {
3625        matches!(
3626            (kind, &self.specifications),
3627            (PrimitiveKind::Number, TypeSpecification::Number { .. })
3628                | (PrimitiveKind::Text, TypeSpecification::Text { .. })
3629                | (PrimitiveKind::Boolean, TypeSpecification::Boolean { .. })
3630                | (PrimitiveKind::Date, TypeSpecification::Date { .. })
3631                | (PrimitiveKind::Time, TypeSpecification::Time { .. })
3632                | (PrimitiveKind::Ratio, TypeSpecification::Ratio { .. })
3633                | (PrimitiveKind::Measure, TypeSpecification::Measure { .. })
3634        )
3635    }
3636
3637    /// Check if this type is measure
3638    pub fn is_measure(&self) -> bool {
3639        matches!(&self.specifications, TypeSpecification::Measure { .. })
3640    }
3641
3642    pub fn is_measure_range(&self) -> bool {
3643        matches!(&self.specifications, TypeSpecification::MeasureRange { .. })
3644    }
3645
3646    /// Check if this type is number (dimensionless)
3647    pub fn is_number(&self) -> bool {
3648        matches!(&self.specifications, TypeSpecification::Number { .. })
3649    }
3650
3651    pub fn is_number_range(&self) -> bool {
3652        matches!(&self.specifications, TypeSpecification::NumberRange { .. })
3653    }
3654
3655    /// Check if this type is numeric (either measure or number)
3656    pub fn is_numeric(&self) -> bool {
3657        matches!(
3658            &self.specifications,
3659            TypeSpecification::Measure { .. } | TypeSpecification::Number { .. }
3660        )
3661    }
3662
3663    /// Check if this type is text
3664    pub fn is_text(&self) -> bool {
3665        matches!(&self.specifications, TypeSpecification::Text { .. })
3666    }
3667
3668    /// Check if this type is date
3669    pub fn is_date(&self) -> bool {
3670        matches!(&self.specifications, TypeSpecification::Date { .. })
3671    }
3672
3673    pub fn is_date_range(&self) -> bool {
3674        matches!(&self.specifications, TypeSpecification::DateRange { .. })
3675    }
3676
3677    pub fn is_time_range(&self) -> bool {
3678        matches!(&self.specifications, TypeSpecification::TimeRange { .. })
3679    }
3680
3681    /// Check if this type is time
3682    pub fn is_time(&self) -> bool {
3683        matches!(&self.specifications, TypeSpecification::Time { .. })
3684    }
3685
3686    pub fn has_trait_duration(&self) -> bool {
3687        match &self.specifications {
3688            TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Duration),
3689            _ => false,
3690        }
3691    }
3692
3693    pub fn is_duration_like_measure(&self) -> bool {
3694        if !self.is_measure() {
3695            return false;
3696        }
3697        if self.has_trait_duration() {
3698            return true;
3699        }
3700        self.is_anonymous_measure()
3701            && self
3702                .measure_type_decomposition()
3703                .is_some_and(|d| *d == duration_decomposition())
3704    }
3705
3706    pub fn is_duration_like(&self) -> bool {
3707        self.is_duration_like_measure()
3708    }
3709
3710    pub fn has_trait_calendar(&self) -> bool {
3711        match &self.specifications {
3712            TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Calendar),
3713            _ => false,
3714        }
3715    }
3716
3717    pub fn is_calendar_like_measure(&self) -> bool {
3718        if !self.is_measure() {
3719            return false;
3720        }
3721        if self.has_trait_calendar() {
3722            return true;
3723        }
3724        self.is_anonymous_measure()
3725            && self
3726                .measure_type_decomposition()
3727                .is_some_and(|d| *d == calendar_decomposition())
3728    }
3729
3730    pub fn is_calendar_like(&self) -> bool {
3731        self.is_calendar_like_measure()
3732    }
3733
3734    /// Check if this type is ratio
3735    pub fn is_ratio(&self) -> bool {
3736        matches!(&self.specifications, TypeSpecification::Ratio { .. })
3737    }
3738
3739    pub fn is_ratio_range(&self) -> bool {
3740        matches!(&self.specifications, TypeSpecification::RatioRange { .. })
3741    }
3742
3743    pub fn is_calendar_measure_range(&self) -> bool {
3744        matches!(
3745            &self.specifications,
3746            TypeSpecification::MeasureRange { decomposition: Some(decomposition), .. }
3747                if *decomposition == calendar_decomposition()
3748        )
3749    }
3750
3751    pub fn is_calendar_like_range(&self) -> bool {
3752        self.is_calendar_measure_range()
3753    }
3754
3755    pub fn is_range(&self) -> bool {
3756        matches!(
3757            &self.specifications,
3758            TypeSpecification::DateRange { .. }
3759                | TypeSpecification::TimeRange { .. }
3760                | TypeSpecification::NumberRange { .. }
3761                | TypeSpecification::MeasureRange { .. }
3762                | TypeSpecification::RatioRange { .. }
3763        )
3764    }
3765
3766    /// Check if this type is veto
3767    pub fn vetoed(&self) -> bool {
3768        matches!(&self.specifications, TypeSpecification::Veto { .. })
3769    }
3770
3771    /// True if this type is the undetermined sentinel (type could not be inferred).
3772    pub fn is_undetermined(&self) -> bool {
3773        matches!(&self.specifications, TypeSpecification::Undetermined)
3774    }
3775
3776    /// Check if two types have the same base type specification (ignoring constraints)
3777    pub fn has_same_base_type(&self, other: &LemmaType) -> bool {
3778        use TypeSpecification::*;
3779        matches!(
3780            (&self.specifications, &other.specifications),
3781            (Boolean { .. }, Boolean { .. })
3782                | (Number { .. }, Number { .. })
3783                | (NumberRange { .. }, NumberRange { .. })
3784                | (Measure { .. }, Measure { .. })
3785                | (MeasureRange { .. }, MeasureRange { .. })
3786                | (Text { .. }, Text { .. })
3787                | (Date { .. }, Date { .. })
3788                | (DateRange { .. }, DateRange { .. })
3789                | (Time { .. }, Time { .. })
3790                | (TimeRange { .. }, TimeRange { .. })
3791                | (Ratio { .. }, Ratio { .. })
3792                | (RatioRange { .. }, RatioRange { .. })
3793                | (Veto { .. }, Veto { .. })
3794                | (Undetermined, Undetermined)
3795        )
3796    }
3797
3798    /// 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.
3799    #[must_use]
3800    pub fn measure_family_name(&self) -> Option<&str> {
3801        if !self.is_measure() {
3802            return None;
3803        }
3804        match &self.extends {
3805            TypeExtends::Custom { family, .. } => Some(family.as_str()),
3806            TypeExtends::Primitive => self.name.as_deref(),
3807        }
3808    }
3809
3810    /// Returns true if both types are measure and belong to the same named measure family.
3811    #[must_use]
3812    pub fn same_measure_family(&self, other: &LemmaType) -> bool {
3813        if !self.is_measure() || !other.is_measure() {
3814            return false;
3815        }
3816        match (self.measure_family_name(), other.measure_family_name()) {
3817            (Some(self_family), Some(other_family)) => self_family == other_family,
3818            _ => false,
3819        }
3820    }
3821
3822    #[must_use]
3823    pub fn compatible_with_anonymous_measure(&self, other: &LemmaType) -> bool {
3824        if !self.is_measure() || !other.is_measure() {
3825            return false;
3826        }
3827        if !self.is_anonymous_measure() && !other.is_anonymous_measure() {
3828            return false;
3829        }
3830        match (
3831            self.measure_type_decomposition(),
3832            other.measure_type_decomposition(),
3833        ) {
3834            (Some(a), Some(b)) => a == b,
3835            _ => false,
3836        }
3837    }
3838
3839    /// Create a Veto LemmaType
3840    pub fn veto_type() -> Self {
3841        Self::primitive(TypeSpecification::veto())
3842    }
3843
3844    /// LemmaType sentinel for undetermined type (used during inference when a type cannot be determined).
3845    /// Propagates through expressions and is never present in a validated graph.
3846    pub fn undetermined_type() -> Self {
3847        Self::primitive(TypeSpecification::Undetermined)
3848    }
3849
3850    /// Decimal places for display (Number, Measure, and Ratio). Used by formatters.
3851    /// Ratio: optional, no default; when None display is normalized (no trailing zeros).
3852    pub fn decimal_places(&self) -> Option<u8> {
3853        match &self.specifications {
3854            TypeSpecification::Number { decimals, .. } => *decimals,
3855            TypeSpecification::Measure { decimals, .. } => *decimals,
3856            TypeSpecification::Ratio { decimals, .. } => *decimals,
3857            _ => None,
3858        }
3859    }
3860
3861    /// Convert a rational magnitude to a decimal string for API output.
3862    ///
3863    /// Applies this type's `decimal_places` when set. Returns [`NumericFailure::Overflow`]
3864    /// when |magnitude| > Decimal::MAX (callers map this to a decimal-limit Veto).
3865    pub fn try_rational_as_decimal_string(
3866        &self,
3867        magnitude: &crate::computation::rational::RationalInteger,
3868    ) -> Result<String, crate::computation::rational::NumericFailure> {
3869        let decimal = magnitude.try_to_decimal()?;
3870        Ok(format_decimal_for_api(decimal, self.decimal_places()))
3871    }
3872
3873    /// Convert a canonical measure magnitude in the named declared unit to an API decimal string.
3874    pub fn try_measure_canonical_as_decimal_in_unit(
3875        &self,
3876        canonical_magnitude: &crate::computation::rational::RationalInteger,
3877        unit_name: &str,
3878    ) -> Result<String, crate::computation::rational::NumericFailure> {
3879        use crate::computation::rational::checked_div;
3880        let unit_factor = self.measure_unit_factor(unit_name);
3881        let magnitude_in_unit = checked_div(canonical_magnitude, unit_factor)?;
3882        self.try_rational_as_decimal_string(&magnitude_in_unit)
3883    }
3884
3885    /// Convert a canonical ratio magnitude in the named declared unit to an API decimal string.
3886    pub fn try_ratio_canonical_as_decimal_in_unit(
3887        &self,
3888        canonical_magnitude: &crate::computation::rational::RationalInteger,
3889        unit_name: &str,
3890    ) -> Result<String, crate::computation::rational::NumericFailure> {
3891        use crate::computation::rational::checked_mul;
3892        let units = match &self.specifications {
3893            TypeSpecification::Ratio { units, .. } => units,
3894            _ => unreachable!(
3895                "BUG: try_ratio_canonical_as_decimal_in_unit called on non-ratio type {}",
3896                self.name()
3897            ),
3898        };
3899        let ratio_unit = units
3900            .iter()
3901            .find(|unit| unit.name == unit_name)
3902            .unwrap_or_else(|| {
3903                let valid: Vec<&str> = units.iter().map(|unit| unit.name.as_str()).collect();
3904                unreachable!(
3905                    "BUG: unknown ratio unit '{}' for type {} (valid: {}); planning must reject invalid units",
3906                    unit_name,
3907                    self.name(),
3908                    valid.join(", ")
3909                )
3910            });
3911        let magnitude_in_unit = checked_mul(canonical_magnitude, &ratio_unit.value)?;
3912        self.try_rational_as_decimal_string(&magnitude_in_unit)
3913    }
3914
3915    /// Get an example value string for this type, suitable for UI help text
3916    pub fn example_value(&self) -> &'static str {
3917        match &self.specifications {
3918            TypeSpecification::Text { .. } => "\"hello world\"",
3919            TypeSpecification::Measure { .. } => "12.50 eur",
3920            TypeSpecification::MeasureRange { .. } => "30 kilogram...35 kilogram",
3921            TypeSpecification::Number { .. } => "3.14",
3922            TypeSpecification::NumberRange { .. } => "0...100",
3923            TypeSpecification::Boolean { .. } => "true",
3924            TypeSpecification::Date { .. } => "2023-12-25T14:30:00Z",
3925            TypeSpecification::DateRange { .. } => "2024-01-01...2024-12-31",
3926            TypeSpecification::TimeRange { .. } => "09:00...17:00",
3927            TypeSpecification::Veto { .. } => "veto",
3928            TypeSpecification::Time { .. } => "14:30:00",
3929            TypeSpecification::Ratio { .. } => "50%",
3930            TypeSpecification::RatioRange { .. } => "10%...50%",
3931            TypeSpecification::Undetermined => unreachable!(
3932                "BUG: example_value called on Undetermined sentinel type; this type must never reach user-facing code"
3933            ),
3934        }
3935    }
3936
3937    /// Factor for a unit of this measure type (for unit conversion during evaluation only).
3938    /// Planning must validate conversions first and return Error for invalid units.
3939    /// If called with a non-measure type or unknown unit name, panics (invariant violation).
3940    #[must_use]
3941    /// Returns the resolved `BaseMeasureVector` for Measure types, or `None` if
3942    /// the decomposition pass has not yet resolved this type.
3943    /// Panics if called on non-Measure types.
3944    pub fn measure_type_decomposition(&self) -> Option<&BaseMeasureVector> {
3945        match &self.specifications {
3946            TypeSpecification::Measure { decomposition, .. } => decomposition.as_ref(),
3947            _ => unreachable!(
3948                "BUG: measure_type_decomposition called on non-measure type {}",
3949                self.name()
3950            ),
3951        }
3952    }
3953
3954    /// Returns true if this is an anonymous (no-name) Measure — i.e. an anonymous
3955    /// intermediate produced by cross-axis arithmetic.
3956    pub fn is_anonymous_measure(&self) -> bool {
3957        self.name.is_none() && matches!(&self.specifications, TypeSpecification::Measure { .. })
3958    }
3959
3960    /// Build an anonymous `LemmaType` for a given dimensional decomposition.
3961    /// Used at plan time to represent the inferred type of cross-axis intermediates.
3962    /// Signatures live on the value, not on the type.
3963    pub fn anonymous_for_decomposition(decomposition: BaseMeasureVector) -> Self {
3964        Self {
3965            name: None,
3966            specifications: TypeSpecification::Measure {
3967                minimum: None,
3968                maximum: None,
3969                decimals: None,
3970                units: crate::literals::MeasureUnits::new(),
3971                traits: Vec::new(),
3972                decomposition: Some(decomposition),
3973                help: String::new(),
3974            },
3975            extends: TypeExtends::Primitive,
3976        }
3977    }
3978
3979    /// Declared unit names when the type carries a non-empty unit table (`None` otherwise).
3980    #[must_use]
3981    pub fn measure_unit_names(&self) -> Option<Vec<&str>> {
3982        match &self.specifications {
3983            TypeSpecification::Measure { units, .. } if !units.is_empty() => {
3984                Some(units.iter().map(|unit| unit.name.as_str()).collect())
3985            }
3986            TypeSpecification::MeasureRange { units, .. } if !units.is_empty() => {
3987                Some(units.iter().map(|unit| unit.name.as_str()).collect())
3988            }
3989            _ => None,
3990        }
3991    }
3992
3993    /// Return the conversion factor for a declared unit name on this measure type.
3994    pub fn measure_unit_factor(
3995        &self,
3996        unit_name: &str,
3997    ) -> &crate::computation::rational::RationalInteger {
3998        let units = match &self.specifications {
3999            TypeSpecification::Measure { units, .. } => units,
4000            TypeSpecification::MeasureRange { units, .. } => units,
4001            _ => unreachable!(
4002                "BUG: measure_unit_factor called with non-measure type {}; only call during evaluation after planning validated measure conversion",
4003                self.name()
4004            ),
4005        };
4006        match units.get(unit_name) {
4007            Ok(MeasureUnit { factor, .. }) => factor,
4008            Err(_) => {
4009                let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
4010                unreachable!(
4011                    "BUG: unknown unit '{}' for measure type {} (valid: {}); planning must reject invalid conversions with Error",
4012                    unit_name,
4013                    self.name(),
4014                    valid.join(", ")
4015                );
4016            }
4017        }
4018    }
4019
4020    pub fn ratio_unit_factor(
4021        &self,
4022        unit_name: &str,
4023    ) -> &crate::computation::rational::RationalInteger {
4024        let units = match &self.specifications {
4025            TypeSpecification::Ratio { units, .. } => units,
4026            _ => unreachable!(
4027                "BUG: ratio_unit_factor called with non-ratio type {}; only call during evaluation after planning validated ratio conversion",
4028                self.name()
4029            ),
4030        };
4031        match units.get(unit_name) {
4032            Ok(RatioUnit { value, .. }) => value,
4033            Err(_) => {
4034                let valid: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
4035                unreachable!(
4036                    "BUG: unknown unit '{}' for ratio type {} (valid: {}); planning must reject invalid conversions with Error",
4037                    unit_name,
4038                    self.name(),
4039                    valid.join(", ")
4040                );
4041            }
4042        }
4043    }
4044
4045    /// Convert a measure literal to decimal strings in every declared unit (same path as rule-result `measure` maps).
4046    pub(crate) fn measure_literal_in_all_units(
4047        &self,
4048        literal: &LiteralValue,
4049    ) -> Result<BTreeMap<String, String>, LiteralUnitMapFailure> {
4050        use crate::computation::rational::checked_div;
4051
4052        let unit_names = self
4053            .measure_unit_names()
4054            .expect("BUG: measure literal in all units requires declared units");
4055        let ValueKind::Measure(magnitude, _signature) = &literal.value else {
4056            panic!("BUG: measure_literal_in_all_units called with non-measure value");
4057        };
4058        let mut map = BTreeMap::new();
4059        for unit_name in unit_names {
4060            let unit_factor = self.measure_unit_factor(unit_name);
4061            let magnitude_in_unit = checked_div(magnitude, unit_factor)
4062                .map_err(LiteralUnitMapFailure::UnitConversion)?;
4063            let decimal_string = self
4064                .try_rational_as_decimal_string(&magnitude_in_unit)
4065                .map_err(LiteralUnitMapFailure::Commit)?;
4066            map.insert(unit_name.to_string(), decimal_string);
4067        }
4068        Ok(map)
4069    }
4070
4071    /// Convert a ratio literal to decimal strings in every declared unit (same path as rule-result `ratio` maps).
4072    pub(crate) fn ratio_literal_in_all_units(
4073        &self,
4074        literal: &LiteralValue,
4075    ) -> Result<BTreeMap<String, String>, LiteralUnitMapFailure> {
4076        use crate::computation::rational::checked_mul;
4077
4078        let ratio_api_type = match &self.specifications {
4079            TypeSpecification::Ratio { .. } => self,
4080            TypeSpecification::RatioRange { .. } => {
4081                let element = range_element_type_specification(&self.specifications)
4082                    .expect("BUG: ratio range type must have ratio element specification");
4083                let TypeSpecification::Ratio {
4084                    units, decimals, ..
4085                } = element
4086                else {
4087                    panic!("BUG: ratio range element spec must be Ratio");
4088                };
4089                return LemmaType::primitive(TypeSpecification::Ratio {
4090                    minimum: None,
4091                    maximum: None,
4092                    decimals,
4093                    units,
4094                    help: String::new(),
4095                })
4096                .ratio_literal_in_all_units(literal);
4097            }
4098            _ => {
4099                panic!(
4100                    "BUG: ratio_literal_in_all_units called with non-ratio type {}",
4101                    self.name()
4102                );
4103            }
4104        };
4105        let units = match &ratio_api_type.specifications {
4106            TypeSpecification::Ratio { units, .. } => units,
4107            _ => unreachable!("BUG: ratio API type must be Ratio"),
4108        };
4109        let ValueKind::Ratio(canonical, _) = &literal.value else {
4110            panic!("BUG: ratio_literal_in_all_units called with non-ratio value");
4111        };
4112        if units.is_empty() {
4113            panic!(
4114                "BUG: ratio literal type '{}' must have declared units",
4115                self.name()
4116            );
4117        }
4118        let mut map = BTreeMap::new();
4119        for unit in units.iter() {
4120            let magnitude_in_unit = checked_mul(canonical, &unit.value)
4121                .map_err(LiteralUnitMapFailure::UnitConversion)?;
4122            let decimal_string = ratio_api_type
4123                .try_rational_as_decimal_string(&magnitude_in_unit)
4124                .map_err(LiteralUnitMapFailure::Commit)?;
4125            map.insert(unit.name.clone(), decimal_string);
4126        }
4127        Ok(map)
4128    }
4129}
4130
4131/// Failure while converting a literal to decimal strings across declared units.
4132#[derive(Debug, Clone, PartialEq, Eq)]
4133pub(crate) enum LiteralUnitMapFailure {
4134    Commit(crate::computation::rational::NumericFailure),
4135    UnitConversion(crate::computation::rational::NumericFailure),
4136}
4137
4138/// Literal value with type. The single value type in semantics.
4139#[derive(Clone, Debug, PartialEq, Eq, Hash)]
4140pub struct LiteralValue {
4141    pub value: ValueKind,
4142    pub lemma_type: Arc<LemmaType>,
4143}
4144
4145impl LiteralValue {
4146    fn single_measure_signature_unit_name(signature: &[(String, i32)]) -> Option<&str> {
4147        match signature {
4148            [(unit_name, 1)] => Some(unit_name.as_str()),
4149            _ => None,
4150        }
4151    }
4152}
4153
4154impl Serialize for LiteralValue {
4155    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
4156    where
4157        S: serde::Serializer,
4158    {
4159        use serde::ser::SerializeStruct;
4160        let mut state = serializer.serialize_struct("LiteralValue", 3)?;
4161        state.serialize_field("value", &self.value)?;
4162        state.serialize_field("lemma_type", self.lemma_type.as_ref())?;
4163        state.serialize_field("display_value", &self.display_value())?;
4164        state.end()
4165    }
4166}
4167
4168impl<'de> Deserialize<'de> for LiteralValue {
4169    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4170    where
4171        D: serde::Deserializer<'de>,
4172    {
4173        #[derive(Deserialize)]
4174        struct Raw {
4175            value: ValueKind,
4176            lemma_type: LemmaType,
4177        }
4178        let raw = Raw::deserialize(deserializer)?;
4179        Ok(Self {
4180            value: raw.value,
4181            lemma_type: Arc::new(raw.lemma_type),
4182        })
4183    }
4184}
4185
4186impl LiteralValue {
4187    pub fn text(s: String) -> Self {
4188        Self {
4189            value: ValueKind::Text(s),
4190            lemma_type: primitive_text_arc().clone(),
4191        }
4192    }
4193
4194    pub fn text_with_type(s: String, lemma_type: Arc<LemmaType>) -> Self {
4195        Self {
4196            value: ValueKind::Text(s),
4197            lemma_type,
4198        }
4199    }
4200
4201    pub fn number(n: RationalInteger) -> Self {
4202        Self {
4203            value: ValueKind::Number(n),
4204            lemma_type: primitive_number_arc().clone(),
4205        }
4206    }
4207
4208    pub fn number_from_decimal(decimal: Decimal) -> Self {
4209        Self::number(
4210            crate::literals::rational_from_parsed_decimal(decimal)
4211                .expect("BUG: literal number from decimal must lift at boundary"),
4212        )
4213    }
4214
4215    pub fn number_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
4216        Self {
4217            value: ValueKind::Number(n),
4218            lemma_type,
4219        }
4220    }
4221
4222    pub fn number_with_type_from_decimal(decimal: Decimal, lemma_type: Arc<LemmaType>) -> Self {
4223        Self::number_with_type(
4224            crate::literals::rational_from_parsed_decimal(decimal)
4225                .expect("BUG: literal number from decimal must lift at boundary"),
4226            lemma_type,
4227        )
4228    }
4229
4230    /// Build a Measure literal carrying a single user-typed unit name.
4231    /// The signature is `[(unit_name, 1)]`; the normalize pass expands compound names
4232    /// against `unit_index` so all stored signatures end up in canonical (base-unit) form.
4233    pub fn measure_with_type(n: RationalInteger, unit: String, lemma_type: Arc<LemmaType>) -> Self {
4234        Self {
4235            value: ValueKind::Measure(n, vec![(unit, 1)]),
4236            lemma_type,
4237        }
4238    }
4239
4240    /// Build a Measure literal with an explicit signature (already in canonical form).
4241    /// Used by arithmetic when combining operand signatures yields a multi-term result.
4242    pub fn measure_with_signature(
4243        n: RationalInteger,
4244        signature: Vec<(String, i32)>,
4245        lemma_type: Arc<LemmaType>,
4246    ) -> Self {
4247        Self {
4248            value: ValueKind::Measure(n, signature),
4249            lemma_type,
4250        }
4251    }
4252
4253    /// Number interpreted as a measure value in the given unit (e.g. "3 as usd" where 3 is a number).
4254    /// Creates an anonymous one-unit measure type so computation does not depend on parsing types.
4255    pub fn number_interpreted_as_measure(value: RationalInteger, unit_name: String) -> Self {
4256        Self {
4257            value: ValueKind::Measure(value, vec![(unit_name, 1)]),
4258            lemma_type: Arc::new(anonymous_measure_type()),
4259        }
4260    }
4261
4262    pub fn from_bool(b: bool) -> Self {
4263        Self {
4264            value: ValueKind::Boolean(b),
4265            lemma_type: primitive_boolean_arc().clone(),
4266        }
4267    }
4268
4269    pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
4270        Self::date(date_time_to_semantic(dt))
4271    }
4272
4273    /// Magnitude string for decimal input prompts (number, single-unit measure, ratio with percent/permille scaling).
4274    #[must_use]
4275    pub fn magnitude_suggestion_for_decimal_prompt(&self) -> Option<String> {
4276        match &self.value {
4277            ValueKind::Number(n) => Some(
4278                self.lemma_type
4279                    .try_rational_as_decimal_string(n)
4280                    .expect("BUG: stored number literal must convert to decimal for prompt"),
4281            ),
4282            ValueKind::Measure(n, signature) => {
4283                let unit_name = Self::single_measure_signature_unit_name(signature).expect(
4284                    "BUG: measure prompt requires exactly one signature unit with exponent 1",
4285                );
4286                Some(
4287                    self.lemma_type
4288                        .try_measure_canonical_as_decimal_in_unit(n, unit_name)
4289                        .expect("BUG: stored measure literal must convert to decimal for prompt"),
4290                )
4291            }
4292            ValueKind::Ratio(n, Some(unit_name)) => Some(
4293                self.lemma_type
4294                    .try_ratio_canonical_as_decimal_in_unit(n, unit_name)
4295                    .expect("BUG: stored ratio literal must convert to decimal for prompt"),
4296            ),
4297            ValueKind::Ratio(n, None) => Some(
4298                self.lemma_type
4299                    .try_rational_as_decimal_string(n)
4300                    .expect("BUG: stored bare ratio literal must convert to decimal for prompt"),
4301            ),
4302            _ => None,
4303        }
4304    }
4305
4306    /// Per-unit magnitudes when this literal is a measure with declared units.
4307    #[must_use]
4308    pub fn measure_units(&self) -> Option<BTreeMap<String, String>> {
4309        if !matches!(self.value, ValueKind::Measure(_, _)) {
4310            return None;
4311        }
4312        self.lemma_type.measure_unit_names()?;
4313        self.lemma_type.measure_literal_in_all_units(self).ok()
4314    }
4315
4316    /// Per-unit magnitudes when this literal is a ratio with declared units.
4317    #[must_use]
4318    pub fn ratio_units(&self) -> Option<BTreeMap<String, String>> {
4319        if !matches!(self.value, ValueKind::Ratio(_, _)) {
4320            return None;
4321        }
4322        let has_declared_units = match &self.lemma_type.specifications {
4323            TypeSpecification::Ratio { units, .. } => !units.is_empty(),
4324            TypeSpecification::RatioRange { .. } => true,
4325            _ => return None,
4326        };
4327        if !has_declared_units {
4328            return None;
4329        }
4330        self.lemma_type.ratio_literal_in_all_units(self).ok()
4331    }
4332
4333    /// Magnitude in a declared unit when this literal is measure or ratio.
4334    #[must_use]
4335    pub fn magnitude_in_unit(&self, unit: &str) -> Option<String> {
4336        self.measure_units()
4337            .and_then(|map| map.get(unit).cloned())
4338            .or_else(|| self.ratio_units().and_then(|map| map.get(unit).cloned()))
4339    }
4340
4341    pub fn date(dt: SemanticDateTime) -> Self {
4342        Self {
4343            value: ValueKind::Date(dt),
4344            lemma_type: primitive_date_arc().clone(),
4345        }
4346    }
4347
4348    pub fn date_with_type(dt: SemanticDateTime, lemma_type: Arc<LemmaType>) -> Self {
4349        Self {
4350            value: ValueKind::Date(dt),
4351            lemma_type,
4352        }
4353    }
4354
4355    pub fn time(t: SemanticTime) -> Self {
4356        Self {
4357            value: ValueKind::Time(t),
4358            lemma_type: primitive_time_arc().clone(),
4359        }
4360    }
4361
4362    pub fn time_with_type(t: SemanticTime, lemma_type: Arc<LemmaType>) -> Self {
4363        Self {
4364            value: ValueKind::Time(t),
4365            lemma_type,
4366        }
4367    }
4368
4369    pub fn calendar(
4370        value: RationalInteger,
4371        unit: SemanticCalendarUnit,
4372        lemma_type: Arc<LemmaType>,
4373    ) -> Self {
4374        Self::measure_with_type(value, unit.to_string(), lemma_type)
4375    }
4376
4377    pub fn calendar_from_decimal(
4378        value: Decimal,
4379        unit: SemanticCalendarUnit,
4380        lemma_type: Arc<LemmaType>,
4381    ) -> Self {
4382        Self::calendar(
4383            crate::literals::rational_from_parsed_decimal(value)
4384                .expect("BUG: calendar literal from decimal must lift at boundary"),
4385            unit,
4386            lemma_type,
4387        )
4388    }
4389
4390    pub fn calendar_with_type(
4391        value: RationalInteger,
4392        unit: SemanticCalendarUnit,
4393        lemma_type: Arc<LemmaType>,
4394    ) -> Self {
4395        Self::calendar(value, unit, lemma_type)
4396    }
4397
4398    /// Derive second from a duration measure's canonical magnitude.
4399    pub fn duration_canonical_seconds(&self) -> RationalInteger {
4400        let ValueKind::Measure(magnitude, _) = &self.value else {
4401            unreachable!(
4402                "BUG: duration_canonical_seconds called with {:?}",
4403                self.value
4404            );
4405        };
4406        if !self.lemma_type.is_duration_like_measure() {
4407            unreachable!(
4408                "BUG: duration_canonical_seconds called with type {}",
4409                self.lemma_type.name()
4410            );
4411        }
4412        let factor = self.lemma_type.measure_unit_factor("second");
4413        checked_div(magnitude, factor).expect("BUG: duration unit factor cannot be zero")
4414    }
4415
4416    /// Derive month from a calendar measure's canonical magnitude.
4417    pub fn calendar_canonical_months(&self) -> RationalInteger {
4418        let ValueKind::Measure(magnitude, _) = &self.value else {
4419            unreachable!(
4420                "BUG: calendar_canonical_months called with {:?}",
4421                self.value
4422            );
4423        };
4424        if !self.lemma_type.is_calendar_like() {
4425            unreachable!(
4426                "BUG: calendar_canonical_months called with type {}",
4427                self.lemma_type.name()
4428            );
4429        }
4430        let factor = self.lemma_type.measure_unit_factor("month");
4431        checked_div(magnitude, factor).expect("BUG: calendar unit factor cannot be zero")
4432    }
4433
4434    pub fn ratio(r: RationalInteger, unit: Option<String>) -> Self {
4435        Self {
4436            value: ValueKind::Ratio(r, unit),
4437            lemma_type: primitive_ratio_arc().clone(),
4438        }
4439    }
4440
4441    pub fn ratio_from_decimal(r: Decimal, unit: Option<String>) -> Self {
4442        Self::ratio(
4443            crate::literals::rational_from_parsed_decimal(r)
4444                .expect("BUG: ratio literal from decimal must lift at boundary"),
4445            unit,
4446        )
4447    }
4448
4449    pub fn ratio_with_type(
4450        r: RationalInteger,
4451        unit: Option<String>,
4452        lemma_type: Arc<LemmaType>,
4453    ) -> Self {
4454        Self {
4455            value: ValueKind::Ratio(r, unit),
4456            lemma_type,
4457        }
4458    }
4459
4460    pub fn range(left: LiteralValue, right: LiteralValue) -> Self {
4461        let specifications =
4462            range_type_specification_from_endpoints(&left.lemma_type, &right.lemma_type)
4463                .unwrap_or_else(|| {
4464                    unreachable!(
4465                "BUG: attempted to construct a range literal from incompatible endpoint types"
4466            )
4467                });
4468
4469        Self {
4470            value: ValueKind::Range(Box::new(left), Box::new(right)),
4471            lemma_type: Arc::new(LemmaType::primitive(specifications)),
4472        }
4473    }
4474
4475    /// Get a display string for this value (for UI/output)
4476    pub fn display_value(&self) -> String {
4477        format!("{}", self)
4478    }
4479
4480    /// Approximate byte size for resource limit checks (string representation length)
4481    pub fn byte_size(&self) -> usize {
4482        format!("{}", self).len()
4483    }
4484
4485    /// Get the resolved type of this literal
4486    pub fn get_type(&self) -> &LemmaType {
4487        &self.lemma_type
4488    }
4489}
4490
4491/// What a [`DataDefinition::Reference`] copies its value from: either another data path
4492/// or a rule whose result becomes this data's value.
4493#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
4494#[serde(rename_all = "snake_case", tag = "kind")]
4495pub enum ReferenceTarget {
4496    Data(DataPath),
4497    Rule(RulePath),
4498}
4499
4500/// Resolved data value for the execution plan: aligned with [`DataValue`] but with source per variant.
4501#[derive(Clone, Debug, Serialize, Deserialize)]
4502#[serde(rename_all = "snake_case")]
4503pub enum DataDefinition {
4504    /// Value-holding data: current literal (spec or `with` binding); type is on the value.
4505    Value { value: LiteralValue, source: Source },
4506    /// Type-only data: type known, value to be supplied (e.g. via run data).
4507    /// `declared_suggestion` carries the `-> suggest ...` payload for this binding or
4508    /// the suggestion inherited from the parent type chain, if any; value-promoting code
4509    /// uses it instead of re-deriving suggestions from [`TypeSpecification`].
4510    /// The evaluator never commits a suggestion — unbound stays MissingData.
4511    TypeDeclaration {
4512        resolved_type: Arc<LemmaType>,
4513        declared_suggestion: Option<ValueKind>,
4514        source: Source,
4515    },
4516    /// Import (`uses`): alias for another spec; nested members are flattened onto the plan.
4517    Import { target_name: String, source: Source },
4518    /// Value-copy reference to another data or a rule result.
4519    ///
4520    /// `resolved_type` is the merged type that the copied value must satisfy at
4521    /// evaluation time. Merging folds together: (1) the LHS's own declared type,
4522    /// if any; (2) the target's type (data declared type or rule return type);
4523    /// (3) any `local_constraints` written after the `->` on the reference itself.
4524    /// Merging happens in a dedicated pass once all data and rule types are
4525    /// known; before that pass, `resolved_type` holds a provisional value and
4526    /// must not be consumed for type checking.
4527    ///
4528    /// `local_constraints` preserves the raw constraint list from the reference's
4529    /// `-> ...` tail (e.g. `minimum 5` in `data license2: law.other -> minimum 5`)
4530    /// for that merging pass. It is `None` when the reference has no trailing
4531    /// constraints.
4532    ///
4533    /// `local_suggestion` carries any `suggest <value>` constraint from the
4534    /// reference's `-> ...` tail. The reference-merge pass extracts it from the
4535    /// constraint list during type resolution. It is a UI hint only
4536    /// ([`Self::suggestion`] / show); the evaluator never commits it — unbound
4537    /// stays MissingData.
4538    ///
4539    /// The reference itself is evaluated by copying the target's value (data path)
4540    /// or the target rule's result in topological order; caller values in
4541    /// [`crate::evaluation::run_data::RunData`] override the reference.
4542    Reference {
4543        target: ReferenceTarget,
4544        resolved_type: Arc<LemmaType>,
4545        local_constraints: Option<Vec<Constraint>>,
4546        local_suggestion: Option<ValueKind>,
4547        source: Source,
4548    },
4549}
4550
4551impl DataDefinition {
4552    /// Declared lemma type for value, type-declaration, and reference data; `None` for imports.
4553    pub fn lemma_type(&self) -> Option<&LemmaType> {
4554        match self {
4555            DataDefinition::Value { value, .. } => Some(value.lemma_type.as_ref()),
4556            DataDefinition::TypeDeclaration { resolved_type, .. } => Some(resolved_type.as_ref()),
4557            DataDefinition::Reference { resolved_type, .. } => Some(resolved_type.as_ref()),
4558            DataDefinition::Import { .. } => None,
4559        }
4560    }
4561
4562    /// Alias for [`Self::lemma_type`] (historical name).
4563    #[inline]
4564    pub fn schema_type(&self) -> Option<&LemmaType> {
4565        self.lemma_type()
4566    }
4567
4568    /// Returns the literal value when the data already holds one. A `Reference`'s
4569    /// value is produced by the evaluator at runtime, so at plan-time it has no
4570    /// value yet.
4571    pub fn value(&self) -> Option<&LiteralValue> {
4572        match self {
4573            DataDefinition::Value { value, .. } => Some(value),
4574            DataDefinition::TypeDeclaration { .. }
4575            | DataDefinition::Import { .. }
4576            | DataDefinition::Reference { .. } => None,
4577        }
4578    }
4579
4580    /// Spec literal (`data x: literal`) or literal `with` binding at plan time.
4581    /// Not a suggestion; see [`Self::suggestion`].
4582    #[inline]
4583    pub fn prefilled_value(&self) -> Option<&LiteralValue> {
4584        self.value()
4585    }
4586
4587    /// Suggestion from `-> suggest ...` on a type declaration or reference.
4588    /// Surfaces in [`crate::planning::execution_plan::ShowData::suggestion`] for
4589    /// show/response/UI; the evaluator never commits it — unbound stays MissingData.
4590    pub fn suggestion(&self) -> Option<LiteralValue> {
4591        match self {
4592            DataDefinition::TypeDeclaration {
4593                resolved_type,
4594                declared_suggestion: Some(dv),
4595                ..
4596            } => Some(LiteralValue {
4597                value: dv.clone(),
4598                lemma_type: Arc::clone(resolved_type),
4599            }),
4600            DataDefinition::Reference {
4601                resolved_type,
4602                local_suggestion: Some(dv),
4603                ..
4604            } => Some(LiteralValue {
4605                value: dv.clone(),
4606                lemma_type: Arc::clone(resolved_type),
4607            }),
4608            DataDefinition::Value { .. }
4609            | DataDefinition::TypeDeclaration {
4610                declared_suggestion: None,
4611                ..
4612            }
4613            | DataDefinition::Reference {
4614                local_suggestion: None,
4615                ..
4616            }
4617            | DataDefinition::Import { .. } => None,
4618        }
4619    }
4620
4621    /// Returns the source location for this data.
4622    pub fn source(&self) -> &Source {
4623        match self {
4624            DataDefinition::Value { source, .. } => source,
4625            DataDefinition::TypeDeclaration { source, .. } => source,
4626            DataDefinition::Import { source, .. } => source,
4627            DataDefinition::Reference { source, .. } => source,
4628        }
4629    }
4630}
4631
4632/// Bind a type-agnostic [`Value::NumberWithUnit`] using the unit index entry for `unit_name`.
4633pub fn number_with_unit_to_value_kind(
4634    magnitude: rust_decimal::Decimal,
4635    unit_name: &str,
4636    lemma_type: &LemmaType,
4637) -> Result<ValueKind, String> {
4638    match &lemma_type.specifications {
4639        TypeSpecification::Ratio { units, .. } => {
4640            use crate::computation::rational::{checked_div, decimal_to_rational};
4641            let unit = units.get(unit_name)?;
4642            let magnitude_rational = decimal_to_rational(magnitude)
4643                .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
4644            let canonical_rational = checked_div(&magnitude_rational, &unit.value)
4645                .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
4646            Ok(ValueKind::Ratio(
4647                canonical_rational,
4648                Some(unit.name.clone()),
4649            ))
4650        }
4651        TypeSpecification::Measure { units, .. } => {
4652            use crate::computation::rational::checked_mul;
4653            let rational = lift_parser_decimal(magnitude)?;
4654            let unit = units.get(unit_name)?;
4655            let canonical = checked_mul(&rational, &unit.factor)
4656                .map_err(|failure| format!("measure canonicalization overflow: {failure}"))?;
4657            Ok(ValueKind::Measure(
4658                canonical,
4659                vec![(unit_name.to_string(), 1)],
4660            ))
4661        }
4662        _ => Err(format!(
4663            "Unit '{}' is defined on type '{}' which is not measure or ratio",
4664            unit_name,
4665            lemma_type.name()
4666        )),
4667    }
4668}
4669
4670/// Whether a [`ValueKind`] is structurally compatible with a [`TypeSpecification`].
4671/// Bound validation (min/max/decimals) is separate; this only checks shape.
4672pub(crate) fn value_kind_matches_spec(value: &ValueKind, type_spec: &TypeSpecification) -> bool {
4673    matches!(
4674        (type_spec, value),
4675        (TypeSpecification::Number { .. }, ValueKind::Number(_))
4676            | (TypeSpecification::Text { .. }, ValueKind::Text(_))
4677            | (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
4678            | (TypeSpecification::Date { .. }, ValueKind::Date(_))
4679            | (TypeSpecification::Time { .. }, ValueKind::Time(_))
4680            | (TypeSpecification::Measure { .. }, ValueKind::Measure(_, _))
4681            | (TypeSpecification::Ratio { .. }, ValueKind::Ratio(_, _))
4682            | (TypeSpecification::Ratio { .. }, ValueKind::Number(_))
4683            | (
4684                TypeSpecification::NumberRange { .. },
4685                ValueKind::Range(_, _)
4686            )
4687            | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
4688            | (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
4689            | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
4690            | (
4691                TypeSpecification::MeasureRange { .. },
4692                ValueKind::Range(_, _)
4693            )
4694            | (TypeSpecification::Veto { .. }, _)
4695            | (TypeSpecification::Undetermined, _)
4696    )
4697}
4698
4699fn parser_value_type_mismatch(
4700    value: &crate::literals::Value,
4701    type_spec: &TypeSpecification,
4702) -> String {
4703    use crate::parsing::ast::AsLemmaSource;
4704    let value_str = format!("{}", AsLemmaSource(value));
4705    match type_spec {
4706        TypeSpecification::Measure { units, .. } => {
4707            let unit_hint = units
4708                .iter()
4709                .find(|u| u.factor == crate::computation::rational::rational_one())
4710                .map(|u| u.name.as_str())
4711                .or_else(|| units.iter().next().map(|u| u.name.as_str()))
4712                .unwrap_or("unit");
4713            format!("cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}`")
4714        }
4715        TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
4716            let unit_hint = units
4717                .iter()
4718                .next()
4719                .map(|u| u.name.as_str())
4720                .unwrap_or("unit");
4721            format!(
4722                "cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}` or bare ratio"
4723            )
4724        }
4725        _ => format!("cannot use {value_str} as {type_spec}"),
4726    }
4727}
4728
4729/// Re-canonicalize a measure literal after compound unit factors were resolved.
4730///
4731/// Literals parsed before derived unit resolution were canonicalized with prefix-only
4732/// factors; multiply by `resolved_factor / stored_factor` to align with final factors.
4733pub fn refresh_measure_literal_canonical_magnitude(
4734    lit: &mut LiteralValue,
4735    resolved_type: &LemmaType,
4736) {
4737    let ValueKind::Measure(magnitude, signature) = &mut lit.value else {
4738        return;
4739    };
4740    let (unit_name, exponent) = signature
4741        .first()
4742        .expect("BUG: measure literal has empty signature during canonical magnitude refresh");
4743    if *exponent != 1 || signature.len() != 1 {
4744        return;
4745    }
4746    let stored_factor = lit.lemma_type.measure_unit_factor(unit_name);
4747    let resolved_factor = resolved_type.measure_unit_factor(unit_name);
4748    if stored_factor == resolved_factor {
4749        lit.lemma_type = Arc::new(resolved_type.clone());
4750        return;
4751    }
4752    let scaled = checked_mul(magnitude, resolved_factor)
4753        .expect("BUG: measure recanonicalization multiply overflow");
4754    *magnitude =
4755        checked_div(&scaled, stored_factor).expect("BUG: measure recanonicalization divide failed");
4756    lit.lemma_type = Arc::new(resolved_type.clone());
4757}
4758
4759/// Convert parser [`Value`] to [`ValueKind`] using the target type (canonicalizes ratio at bind).
4760pub fn parser_value_to_value_kind(
4761    value: &crate::literals::Value,
4762    type_spec: &TypeSpecification,
4763) -> Result<ValueKind, String> {
4764    use crate::computation::rational::decimal_to_rational;
4765    use crate::literals::Value;
4766    match (value, type_spec) {
4767        (Value::NumberWithUnit(magnitude, unit_name), TypeSpecification::Ratio { units, .. }) => {
4768            use crate::computation::rational::checked_div;
4769            let unit = units.get(unit_name.as_str())?;
4770            let magnitude_rational = decimal_to_rational(*magnitude)
4771                .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
4772            let canonical_rational = checked_div(&magnitude_rational, &unit.value)
4773                .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
4774            Ok(ValueKind::Ratio(
4775                canonical_rational,
4776                Some(unit.name.clone()),
4777            ))
4778        }
4779        (Value::NumberWithUnit(magnitude, unit_name), TypeSpecification::Measure { units, .. }) => {
4780            use crate::computation::rational::checked_mul;
4781            let rational = lift_parser_decimal(*magnitude)?;
4782            let unit = units.get(unit_name.as_str())?;
4783            let canonical = checked_mul(&rational, &unit.factor)
4784                .map_err(|failure| format!("measure canonicalization overflow: {failure}"))?;
4785            Ok(ValueKind::Measure(canonical, vec![(unit_name.clone(), 1)]))
4786        }
4787        (Value::NumberWithUnit(_, _), _) => {
4788            Err("number_with_unit literal requires a measure or ratio type".to_string())
4789        }
4790        (Value::Number(n), TypeSpecification::Number { .. }) => {
4791            Ok(ValueKind::Number(lift_parser_decimal(*n)?))
4792        }
4793        (Value::Number(n), TypeSpecification::Ratio { .. }) => {
4794            let r = decimal_to_rational(*n)
4795                .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
4796            Ok(ValueKind::Ratio(r, None))
4797        }
4798        (Value::Text(s), TypeSpecification::Text { .. }) => Ok(ValueKind::Text(s.clone())),
4799        (Value::Boolean(b), TypeSpecification::Boolean { .. }) => Ok(ValueKind::Boolean(b.into())),
4800        (Value::Date(dt), TypeSpecification::Date { .. }) => {
4801            Ok(ValueKind::Date(date_time_to_semantic(dt)))
4802        }
4803        (Value::Time(t), TypeSpecification::Time { .. }) => {
4804            Ok(ValueKind::Time(time_to_semantic(t)))
4805        }
4806        (
4807            Value::Range(left, right),
4808            range_spec @ (TypeSpecification::NumberRange { .. }
4809            | TypeSpecification::DateRange { .. }
4810            | TypeSpecification::TimeRange { .. }
4811            | TypeSpecification::RatioRange { .. }
4812            | TypeSpecification::MeasureRange { .. }),
4813        ) => {
4814            let endpoint = range_element_type_specification(range_spec).ok_or_else(|| {
4815                "BUG: range_element_type_specification missing arm for range type".to_string()
4816            })?;
4817            let left_lit = lift_range_endpoint(left, &endpoint)?;
4818            let right_lit = lift_range_endpoint(right, &endpoint)?;
4819            Ok(ValueKind::Range(Box::new(left_lit), Box::new(right_lit)))
4820        }
4821        (value, type_spec) => Err(parser_value_type_mismatch(value, type_spec)),
4822    }
4823}
4824
4825/// Convert parser Value to ValueKind for primitives and ranges only.
4826///
4827/// [`Value::NumberWithUnit`] requires [`parser_value_to_value_kind`] with a measure or ratio type.
4828pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
4829    use crate::parsing::ast::Value;
4830    Ok(match value {
4831        Value::Number(n) => ValueKind::Number(lift_parser_decimal(*n)?),
4832        Value::Text(s) => ValueKind::Text(s.clone()),
4833        Value::Boolean(b) => ValueKind::Boolean(bool::from(*b)),
4834        Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
4835        Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
4836        Value::NumberWithUnit(_, _) => {
4837            return Err(
4838                "number_with_unit literal requires type context (measure or ratio)".to_string(),
4839            );
4840        }
4841        Value::Range(_, _) => literal_value_from_parser_value(value)?.value,
4842    })
4843}
4844
4845/// Convert AST date-time to semantic (for tests and planning).
4846pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
4847    SemanticDateTime {
4848        year: dt.year,
4849        month: dt.month,
4850        day: dt.day,
4851        hour: dt.hour,
4852        minute: dt.minute,
4853        second: dt.second,
4854        microsecond: dt.microsecond,
4855        timezone: dt.timezone.as_ref().map(|tz| SemanticTimezone {
4856            offset_hours: tz.offset_hours,
4857            offset_minutes: tz.offset_minutes,
4858        }),
4859    }
4860}
4861
4862/// Convert AST time to semantic (for tests and planning).
4863pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
4864    SemanticTime {
4865        hour: t.hour.into(),
4866        minute: t.minute.into(),
4867        second: t.second.into(),
4868        microsecond: t.microsecond,
4869        timezone: t.timezone.as_ref().map(|tz| SemanticTimezone {
4870            offset_hours: tz.offset_hours,
4871            offset_minutes: tz.offset_minutes,
4872        }),
4873    }
4874}
4875
4876/// Compare two semantic date-time values by year, month, day, hour, minute,
4877/// second, then microsecond. Timezone normalisation is a separate concern
4878/// handled at evaluation time.
4879pub(crate) fn compare_semantic_dates(
4880    left: &SemanticDateTime,
4881    right: &SemanticDateTime,
4882) -> std::cmp::Ordering {
4883    left.year
4884        .cmp(&right.year)
4885        .then_with(|| left.month.cmp(&right.month))
4886        .then_with(|| left.day.cmp(&right.day))
4887        .then_with(|| left.hour.cmp(&right.hour))
4888        .then_with(|| left.minute.cmp(&right.minute))
4889        .then_with(|| left.second.cmp(&right.second))
4890        .then_with(|| left.microsecond.cmp(&right.microsecond))
4891}
4892
4893/// Compare two semantic time values by hour, minute, second, then microsecond.
4894/// Timezone is excluded for the same reason as [`compare_semantic_dates`].
4895pub(crate) fn compare_semantic_times(
4896    left: &SemanticTime,
4897    right: &SemanticTime,
4898) -> std::cmp::Ordering {
4899    left.hour
4900        .cmp(&right.hour)
4901        .then_with(|| left.minute.cmp(&right.minute))
4902        .then_with(|| left.second.cmp(&right.second))
4903        .then_with(|| left.microsecond.cmp(&right.microsecond))
4904}
4905
4906/// Convert AST conversion target to semantic (planning boundary; evaluation/computation use only semantic).
4907pub fn conversion_target_to_semantic(
4908    ct: &ConversionTarget,
4909    unit_index: Option<&crate::planning::unit_index::UnitIndex>,
4910) -> Result<SemanticConversionTarget, String> {
4911    match ct {
4912        ConversionTarget::Type(kind) => Ok(SemanticConversionTarget::Type(*kind)),
4913        ConversionTarget::Unit { unit_name } => {
4914            let index = unit_index.ok_or_else(|| format!("Unknown unit '{unit_name}'."))?;
4915            let (bare, owning_type) = index.resolve(unit_name)?;
4916            Ok(SemanticConversionTarget::Unit {
4917                unit_name: bare,
4918                owning_type,
4919            })
4920        }
4921    }
4922}
4923
4924// -----------------------------------------------------------------------------
4925// Primitive type constructors (moved from parsing::ast)
4926// -----------------------------------------------------------------------------
4927
4928// Statics for lazy initialization of production-used primitive types.
4929static PRIMITIVE_BOOLEAN: OnceLock<Arc<LemmaType>> = OnceLock::new();
4930static PRIMITIVE_NUMBER: OnceLock<Arc<LemmaType>> = OnceLock::new();
4931static PRIMITIVE_TEXT: OnceLock<Arc<LemmaType>> = OnceLock::new();
4932static PRIMITIVE_DATE: OnceLock<Arc<LemmaType>> = OnceLock::new();
4933static PRIMITIVE_DATE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
4934static PRIMITIVE_TIME: OnceLock<Arc<LemmaType>> = OnceLock::new();
4935static PRIMITIVE_RATIO: OnceLock<Arc<LemmaType>> = OnceLock::new();
4936
4937#[must_use]
4938pub fn primitive_boolean_arc() -> &'static Arc<LemmaType> {
4939    PRIMITIVE_BOOLEAN.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::boolean())))
4940}
4941
4942#[must_use]
4943pub fn primitive_number_arc() -> &'static Arc<LemmaType> {
4944    PRIMITIVE_NUMBER.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::number())))
4945}
4946
4947#[must_use]
4948pub fn primitive_text_arc() -> &'static Arc<LemmaType> {
4949    PRIMITIVE_TEXT.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::text())))
4950}
4951
4952#[must_use]
4953pub fn primitive_date_arc() -> &'static Arc<LemmaType> {
4954    PRIMITIVE_DATE.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::date())))
4955}
4956
4957#[must_use]
4958pub fn primitive_date_range_arc() -> &'static Arc<LemmaType> {
4959    PRIMITIVE_DATE_RANGE
4960        .get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::date_range())))
4961}
4962
4963#[must_use]
4964pub fn primitive_time_arc() -> &'static Arc<LemmaType> {
4965    PRIMITIVE_TIME.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::time())))
4966}
4967
4968#[must_use]
4969pub fn primitive_ratio_arc() -> &'static Arc<LemmaType> {
4970    PRIMITIVE_RATIO.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::ratio())))
4971}
4972
4973/// Map PrimitiveKind to TypeSpecification. Single source of truth for primitive type resolution.
4974#[must_use]
4975pub fn type_spec_for_primitive(kind: PrimitiveKind) -> TypeSpecification {
4976    match kind {
4977        PrimitiveKind::Boolean => TypeSpecification::boolean(),
4978        PrimitiveKind::Measure => TypeSpecification::measure(),
4979        PrimitiveKind::MeasureRange => TypeSpecification::measure_range(),
4980        PrimitiveKind::Number => TypeSpecification::number(),
4981        PrimitiveKind::NumberRange => TypeSpecification::number_range(),
4982        PrimitiveKind::Ratio => TypeSpecification::ratio(),
4983        PrimitiveKind::RatioRange => TypeSpecification::ratio_range(),
4984        PrimitiveKind::Text => TypeSpecification::text(),
4985        PrimitiveKind::Date => TypeSpecification::date(),
4986        PrimitiveKind::DateRange => TypeSpecification::date_range(),
4987        PrimitiveKind::Time => TypeSpecification::time(),
4988        PrimitiveKind::TimeRange => TypeSpecification::time_range(),
4989    }
4990}
4991
4992// -----------------------------------------------------------------------------
4993// Display implementations
4994// -----------------------------------------------------------------------------
4995
4996impl fmt::Display for PathSegment {
4997    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4998        write!(f, "{} → {}", self.data, self.spec)
4999    }
5000}
5001
5002impl fmt::Display for DataPath {
5003    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5004        for segment in &self.segments {
5005            write!(f, "{}.", segment)?;
5006        }
5007        write!(f, "{}", self.data)
5008    }
5009}
5010
5011impl fmt::Display for RulePath {
5012    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5013        for segment in &self.segments {
5014            write!(f, "{}.", segment)?;
5015        }
5016        write!(f, "{}", self.rule)
5017    }
5018}
5019
5020impl fmt::Display for LemmaType {
5021    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5022        write!(f, "{}", self.name())
5023    }
5024}
5025
5026fn decimal_places_in_display_value(decimal: &rust_decimal::Decimal) -> u32 {
5027    if decimal.is_integer() {
5028        return 0;
5029    }
5030    decimal.fract().normalize().scale()
5031}
5032
5033fn format_decimal_for_api(decimal: rust_decimal::Decimal, decimal_places: Option<u8>) -> String {
5034    match decimal_places {
5035        Some(decimal_places) => {
5036            let rounded = decimal.round_dp(u32::from(decimal_places));
5037            format!("{:.prec$}", rounded, prec = decimal_places as usize)
5038        }
5039        None => {
5040            let normalized = decimal.normalize();
5041            if normalized.fract().is_zero() {
5042                normalized.trunc().to_string()
5043            } else {
5044                normalized.to_string()
5045            }
5046        }
5047    }
5048}
5049
5050fn format_decimal_for_human_display(
5051    decimal: rust_decimal::Decimal,
5052    decimal_places: Option<u8>,
5053) -> String {
5054    match decimal_places {
5055        Some(decimal_places) => {
5056            let rounded = decimal.round_dp(u32::from(decimal_places));
5057            format!("{:.prec$}", rounded, prec = decimal_places as usize)
5058        }
5059        None => decimal.normalize().to_string(),
5060    }
5061}
5062
5063fn format_rational_for_human_display(
5064    magnitude: &crate::computation::rational::RationalInteger,
5065    decimal_places: Option<u8>,
5066) -> String {
5067    match magnitude.try_to_decimal() {
5068        Ok(decimal) => format_decimal_for_human_display(decimal, decimal_places),
5069        Err(crate::computation::rational::NumericFailure::Overflow) => magnitude.display_str(),
5070        Err(_) => magnitude.display_str(),
5071    }
5072}
5073
5074fn format_measure_canonical_for_display(
5075    canonical: &crate::computation::rational::RationalInteger,
5076    lemma_type: &LemmaType,
5077    signature: &[(String, i32)],
5078) -> String {
5079    use crate::computation::rational::{checked_div, rational_new};
5080    use rust_decimal::Decimal;
5081
5082    let decimals = lemma_type.decimal_places();
5083
5084    if let TypeSpecification::Measure { units, .. } = &lemma_type.specifications {
5085        if !units.is_empty() {
5086            if let [(sig_unit, 1)] = signature {
5087                if let Some(unit) = units.iter().find(|u| u.name == *sig_unit) {
5088                    let in_unit = checked_div(canonical, &unit.factor)
5089                        .expect("BUG: de-canonicalization for measure display must not fail");
5090                    let formatted = format_rational_for_human_display(&in_unit, decimals);
5091                    return format!("{} {}", formatted, unit.name);
5092                }
5093            }
5094
5095            struct UnitDisplayCandidate {
5096                unit_name: String,
5097                decimal_places: u32,
5098                under_1000: bool,
5099                decimal_abs: Option<Decimal>,
5100                formatted: String,
5101            }
5102
5103            let thousand = rational_new(1000, 1);
5104            let mut candidates: Vec<UnitDisplayCandidate> = Vec::with_capacity(units.len());
5105            for unit in units.iter() {
5106                let in_unit = checked_div(canonical, &unit.factor)
5107                    .expect("BUG: de-canonicalization for measure display must not fail");
5108                let formatted = format_rational_for_human_display(&in_unit, decimals);
5109                let decimal_abs = in_unit.try_to_decimal().ok().map(|decimal| decimal.abs());
5110                let decimal_places = decimal_abs
5111                    .as_ref()
5112                    .map(decimal_places_in_display_value)
5113                    .unwrap_or(u32::MAX);
5114                let under_1000 = in_unit
5115                    .try_cmp(&thousand)
5116                    .ok()
5117                    .is_some_and(|ordering| ordering == std::cmp::Ordering::Less);
5118                candidates.push(UnitDisplayCandidate {
5119                    unit_name: unit.name.clone(),
5120                    decimal_places,
5121                    under_1000,
5122                    decimal_abs,
5123                    formatted,
5124                });
5125            }
5126
5127            let pool: Vec<&UnitDisplayCandidate> = {
5128                let under: Vec<_> = candidates.iter().filter(|c| c.under_1000).collect();
5129                if under.is_empty() {
5130                    candidates.iter().collect()
5131                } else {
5132                    under
5133                }
5134            };
5135            let best = pool
5136                .iter()
5137                .min_by(|left, right| {
5138                    left.decimal_places
5139                        .cmp(&right.decimal_places)
5140                        .then_with(|| match (left.decimal_abs, right.decimal_abs) {
5141                            (Some(left_abs), Some(right_abs)) => left_abs.cmp(&right_abs),
5142                            (Some(_), None) => std::cmp::Ordering::Less,
5143                            (None, Some(_)) => std::cmp::Ordering::Greater,
5144                            (None, None) => std::cmp::Ordering::Equal,
5145                        })
5146                })
5147                .expect("BUG: measure type must have at least one declared unit");
5148            return format!("{} {}", best.formatted, best.unit_name);
5149        }
5150    }
5151
5152    let unit_label = match signature {
5153        [] => String::new(),
5154        [(name, 1)] => name.clone(),
5155        _ => format_signature_operator_style(signature),
5156    };
5157    let formatted = format_rational_for_human_display(canonical, decimals);
5158    if unit_label.is_empty() {
5159        formatted
5160    } else {
5161        format!("{formatted} {unit_label}")
5162    }
5163}
5164
5165impl fmt::Display for LiteralValue {
5166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5167        match &self.value {
5168            ValueKind::Measure(n, signature) => {
5169                write!(
5170                    f,
5171                    "{}",
5172                    format_measure_canonical_for_display(n, &self.lemma_type, signature)
5173                )
5174            }
5175            ValueKind::Ratio(_, Some(_unit_name)) => write!(f, "{}", self.value),
5176            ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
5177            _ => write!(f, "{}", self.value),
5178        }
5179    }
5180}
5181
5182// -----------------------------------------------------------------------------
5183// Tests
5184// -----------------------------------------------------------------------------
5185
5186#[cfg(test)]
5187pub(crate) mod tests {
5188    use super::*;
5189    use crate::computation::rational::decimal_to_rational;
5190    use crate::literals::DateGranularity;
5191    use crate::literals::Value;
5192    use crate::parsing::ast::{BooleanValue, DateTimeValue, PrimitiveKind, TimeValue};
5193    use rust_decimal::Decimal;
5194    use std::str::FromStr;
5195    use std::sync::{Arc, OnceLock};
5196
5197    static PRIMITIVE_MEASURE: OnceLock<Arc<LemmaType>> = OnceLock::new();
5198
5199    #[must_use]
5200    pub(crate) fn primitive_measure_arc() -> &'static Arc<LemmaType> {
5201        PRIMITIVE_MEASURE
5202            .get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::measure())))
5203    }
5204
5205    #[must_use]
5206    pub(crate) fn primitive_measure() -> &'static LemmaType {
5207        primitive_measure_arc().as_ref()
5208    }
5209
5210    #[test]
5211    fn default_primitive_help_is_goal_oriented() {
5212        let kinds = [
5213            PrimitiveKind::Boolean,
5214            PrimitiveKind::Measure,
5215            PrimitiveKind::MeasureRange,
5216            PrimitiveKind::Number,
5217            PrimitiveKind::NumberRange,
5218            PrimitiveKind::Ratio,
5219            PrimitiveKind::RatioRange,
5220            PrimitiveKind::Text,
5221            PrimitiveKind::Date,
5222            PrimitiveKind::DateRange,
5223            PrimitiveKind::Time,
5224            PrimitiveKind::TimeRange,
5225        ];
5226        for kind in kinds {
5227            let spec = type_spec_for_primitive(kind);
5228            let help = match &spec {
5229                TypeSpecification::Boolean { help, .. }
5230                | TypeSpecification::Number { help, .. }
5231                | TypeSpecification::NumberRange { help, .. }
5232                | TypeSpecification::Text { help, .. }
5233                | TypeSpecification::Measure { help, .. }
5234                | TypeSpecification::MeasureRange { help, .. }
5235                | TypeSpecification::Ratio { help, .. }
5236                | TypeSpecification::RatioRange { help, .. }
5237                | TypeSpecification::Date { help, .. }
5238                | TypeSpecification::DateRange { help, .. }
5239                | TypeSpecification::TimeRange { help, .. }
5240                | TypeSpecification::Time { help, .. } => help,
5241                TypeSpecification::Veto { .. } | TypeSpecification::Undetermined => {
5242                    unreachable!(
5243                        "BUG: primitive kind {:?} mapped to non-primitive spec",
5244                        kind
5245                    )
5246                }
5247            };
5248            assert!(!help.is_empty(), "help for {:?}", kind);
5249            assert!(
5250                !help.to_ascii_lowercase().contains("format:"),
5251                "help for {:?} must not describe syntax: {:?}",
5252                kind,
5253                help
5254            );
5255            assert_eq!(help, default_help_for_primitive(kind));
5256        }
5257    }
5258
5259    #[test]
5260    fn test_negated_comparison() {
5261        assert_eq!(
5262            negated_comparison(ComparisonComputation::LessThan),
5263            ComparisonComputation::GreaterThanOrEqual
5264        );
5265        assert_eq!(
5266            negated_comparison(ComparisonComputation::GreaterThanOrEqual),
5267            ComparisonComputation::LessThan
5268        );
5269        assert_eq!(
5270            negated_comparison(ComparisonComputation::Is),
5271            ComparisonComputation::IsNot
5272        );
5273        assert_eq!(
5274            negated_comparison(ComparisonComputation::IsNot),
5275            ComparisonComputation::Is
5276        );
5277    }
5278
5279    #[test]
5280    fn value_to_semantic_number_is_decimal() {
5281        let kind = value_to_semantic(&Value::Number(Decimal::from(42))).unwrap();
5282        assert!(matches!(kind, ValueKind::Number(d) if d == rational_new(42, 1)));
5283    }
5284
5285    #[test]
5286    fn value_kind_measure_serializes_with_signature() {
5287        let kind = ValueKind::Measure(
5288            decimal_to_rational(Decimal::from_str("99.50").unwrap()).unwrap(),
5289            vec![("eur".to_string(), 1)],
5290        );
5291        let json = serde_json::to_value(&kind).unwrap();
5292        assert_eq!(json["measure"]["value"], "99.5");
5293        assert_eq!(json["measure"]["signature"][0][0], "eur");
5294        assert_eq!(json["measure"]["signature"][0][1], 1);
5295    }
5296
5297    #[test]
5298    fn value_kind_measure_compound_signature_roundtrips() {
5299        let original = ValueKind::Measure(
5300            decimal_to_rational(Decimal::from_str("4800").unwrap()).unwrap(),
5301            vec![
5302                ("eur".to_string(), 1),
5303                ("hour".to_string(), 1),
5304                ("minute".to_string(), -1),
5305            ],
5306        );
5307        let json = serde_json::to_string(&original).unwrap();
5308        let parsed: ValueKind = serde_json::from_str(&json).unwrap();
5309        assert_eq!(original, parsed);
5310    }
5311
5312    #[test]
5313    fn value_kind_measure_empty_signature_roundtrips() {
5314        let original = ValueKind::Measure(
5315            decimal_to_rational(Decimal::from_str("12.5").unwrap()).unwrap(),
5316            Vec::new(),
5317        );
5318        let json = serde_json::to_string(&original).unwrap();
5319        let parsed: ValueKind = serde_json::from_str(&json).unwrap();
5320        assert_eq!(original, parsed);
5321    }
5322
5323    #[test]
5324    fn literal_value_number_serde_not_rational_array() {
5325        let lit = LiteralValue::number_from_decimal(Decimal::from(20));
5326        let json = serde_json::to_value(&lit).unwrap();
5327        let number = json
5328            .get("value")
5329            .and_then(|v| v.get("number"))
5330            .expect("number field");
5331        assert!(number.is_string());
5332        assert_eq!(number.as_str(), Some("20"));
5333        assert!(
5334            !number.is_array(),
5335            "stored number must not serialize as [n,d]"
5336        );
5337    }
5338
5339    #[test]
5340    fn test_literal_value_to_primitive_type() {
5341        let one = rational_new(1, 1);
5342
5343        assert_eq!(LiteralValue::text("".to_string()).lemma_type.name(), "text");
5344        assert_eq!(
5345            LiteralValue::number(one.clone()).lemma_type.name(),
5346            "number"
5347        );
5348        assert_eq!(
5349            LiteralValue::from_bool(bool::from(BooleanValue::True))
5350                .lemma_type
5351                .name(),
5352            "boolean"
5353        );
5354
5355        let dt = DateTimeValue {
5356            year: 2024,
5357            month: 1,
5358            day: 1,
5359            hour: 0,
5360            minute: 0,
5361            second: 0,
5362            microsecond: 0,
5363            timezone: None,
5364
5365            granularity: DateGranularity::Full,
5366        };
5367        assert_eq!(
5368            LiteralValue::date(date_time_to_semantic(&dt))
5369                .lemma_type
5370                .name(),
5371            "date"
5372        );
5373        assert_eq!(
5374            LiteralValue::ratio_from_decimal(Decimal::new(1, 2), Some("percent".to_string()))
5375                .lemma_type
5376                .name(),
5377            "ratio"
5378        );
5379        let dur_type = LemmaType::new(
5380            "duration".to_string(),
5381            TypeSpecification::Measure {
5382                minimum: None,
5383                maximum: None,
5384                decimals: None,
5385                units: MeasureUnits::from(vec![MeasureUnit {
5386                    name: "second".to_string(),
5387                    factor: crate::computation::rational::rational_one(),
5388                    derived_measure_factors: Vec::new(),
5389                    decomposition: BaseMeasureVector::new(),
5390                    minimum: None,
5391                    maximum: None,
5392                    suggestion_magnitude: None,
5393                }]),
5394                traits: vec![MeasureTrait::Duration],
5395                decomposition: None,
5396                help: String::new(),
5397            },
5398            TypeExtends::Primitive,
5399        );
5400        assert_eq!(
5401            LiteralValue::measure_with_type(one.clone(), "second".to_string(), Arc::new(dur_type))
5402                .lemma_type
5403                .name(),
5404            "duration"
5405        );
5406    }
5407
5408    #[test]
5409    fn test_type_display() {
5410        let specs = TypeSpecification::text();
5411        let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
5412        assert_eq!(format!("{}", lemma_type), "name");
5413    }
5414
5415    #[test]
5416    fn test_type_serialization() {
5417        let specs = TypeSpecification::number();
5418        let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
5419        let serialized = serde_json::to_string(&lemma_type).unwrap();
5420        let deserialized: LemmaType = serde_json::from_str(&serialized).unwrap();
5421        assert_eq!(lemma_type, deserialized);
5422    }
5423
5424    #[test]
5425    fn test_literal_value_display_value() {
5426        let ten = rational_new(10, 1);
5427
5428        assert_eq!(
5429            LiteralValue::text("hello".to_string()).display_value(),
5430            "hello"
5431        );
5432        assert_eq!(LiteralValue::number(ten).display_value(), "10");
5433        assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
5434        assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
5435
5436        // 0.10 ratio with "percent" unit displays as 10% (unit conversion applied)
5437        let ten_percent_ratio =
5438            LiteralValue::ratio_from_decimal(Decimal::new(1, 1), Some("percent".to_string()));
5439        assert_eq!(ten_percent_ratio.display_value(), "10%");
5440
5441        let time = TimeValue {
5442            hour: 14,
5443            minute: 30,
5444            second: 0,
5445            microsecond: 0,
5446            timezone: None,
5447        };
5448        let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
5449        assert!(time_display.contains("14"));
5450        assert!(time_display.contains("30"));
5451    }
5452
5453    #[test]
5454    fn test_measure_display_respects_type_decimals() {
5455        let money_type = LemmaType {
5456            name: Some("money".to_string()),
5457            specifications: TypeSpecification::Measure {
5458                minimum: None,
5459                maximum: None,
5460                decimals: Some(2),
5461                units: MeasureUnits::from(vec![MeasureUnit {
5462                    name: "eur".to_string(),
5463                    factor: crate::computation::rational::rational_one(),
5464                    derived_measure_factors: Vec::new(),
5465                    decomposition: BaseMeasureVector::new(),
5466                    minimum: None,
5467                    maximum: None,
5468                    suggestion_magnitude: None,
5469                }]),
5470                traits: Vec::new(),
5471                decomposition: None,
5472                help: String::new(),
5473            },
5474            extends: TypeExtends::Primitive,
5475        };
5476        let money_type = Arc::new(money_type);
5477        let val = LiteralValue::measure_with_type(
5478            decimal_to_rational(Decimal::from_str("1.8").unwrap()).unwrap(),
5479            "eur".to_string(),
5480            money_type.clone(),
5481        );
5482        assert_eq!(val.display_value(), "1.80 eur");
5483        let more_precision = LiteralValue::measure_with_type(
5484            decimal_to_rational(Decimal::from_str("1.80000").unwrap()).unwrap(),
5485            "eur".to_string(),
5486            money_type,
5487        );
5488        assert_eq!(more_precision.display_value(), "1.80 eur");
5489        let measure_no_decimals = LemmaType {
5490            name: Some("count".to_string()),
5491            specifications: TypeSpecification::Measure {
5492                minimum: None,
5493                maximum: None,
5494                decimals: None,
5495                units: MeasureUnits::from(vec![MeasureUnit {
5496                    name: "items".to_string(),
5497                    factor: crate::computation::rational::rational_one(),
5498                    derived_measure_factors: Vec::new(),
5499                    decomposition: BaseMeasureVector::new(),
5500                    minimum: None,
5501                    maximum: None,
5502                    suggestion_magnitude: None,
5503                }]),
5504                traits: Vec::new(),
5505                decomposition: None,
5506                help: String::new(),
5507            },
5508            extends: TypeExtends::Primitive,
5509        };
5510        let val_any = LiteralValue::measure_with_type(
5511            decimal_to_rational(Decimal::from_str("42.50").unwrap()).unwrap(),
5512            "items".to_string(),
5513            Arc::new(measure_no_decimals),
5514        );
5515        assert_eq!(val_any.display_value(), "42.5 items");
5516    }
5517
5518    #[test]
5519    fn test_literal_value_time_type() {
5520        let time = TimeValue {
5521            hour: 14,
5522            minute: 30,
5523            second: 0,
5524            microsecond: 0,
5525            timezone: None,
5526        };
5527        let lit = LiteralValue::time(time_to_semantic(&time));
5528        assert_eq!(lit.lemma_type.name(), "time");
5529    }
5530
5531    #[test]
5532    fn test_measure_family_name_primitive_root() {
5533        let measure_spec = TypeSpecification::measure();
5534        let money_primitive = LemmaType::new(
5535            "money".to_string(),
5536            measure_spec.clone(),
5537            TypeExtends::Primitive,
5538        );
5539        assert_eq!(money_primitive.measure_family_name(), Some("money"));
5540    }
5541
5542    #[test]
5543    fn test_measure_family_name_custom() {
5544        let measure_spec = TypeSpecification::measure();
5545        let money_custom = LemmaType::new(
5546            "money".to_string(),
5547            measure_spec,
5548            TypeExtends::custom_local("money".to_string(), "money".to_string()),
5549        );
5550        assert_eq!(money_custom.measure_family_name(), Some("money"));
5551    }
5552
5553    #[test]
5554    fn test_same_measure_family_same_name_different_extends() {
5555        let measure_spec = TypeSpecification::measure();
5556        let money_primitive = LemmaType::new(
5557            "money".to_string(),
5558            measure_spec.clone(),
5559            TypeExtends::Primitive,
5560        );
5561        let money_custom = LemmaType::new(
5562            "money".to_string(),
5563            measure_spec,
5564            TypeExtends::custom_local("money".to_string(), "money".to_string()),
5565        );
5566        assert!(money_primitive.same_measure_family(&money_custom));
5567        assert!(money_custom.same_measure_family(&money_primitive));
5568    }
5569
5570    #[test]
5571    fn test_same_measure_family_parent_and_child() {
5572        let measure_spec = TypeSpecification::measure();
5573        let type_x = LemmaType::new(
5574            "x".to_string(),
5575            measure_spec.clone(),
5576            TypeExtends::Primitive,
5577        );
5578        let type_x2 = LemmaType::new(
5579            "x2".to_string(),
5580            measure_spec,
5581            TypeExtends::custom_local("x".to_string(), "x".to_string()),
5582        );
5583        assert_eq!(type_x.measure_family_name(), Some("x"));
5584        assert_eq!(type_x2.measure_family_name(), Some("x"));
5585        assert!(type_x.same_measure_family(&type_x2));
5586        assert!(type_x2.same_measure_family(&type_x));
5587    }
5588
5589    #[test]
5590    fn test_same_measure_family_siblings() {
5591        let measure_spec = TypeSpecification::measure();
5592        let type_x2_a = LemmaType::new(
5593            "x2a".to_string(),
5594            measure_spec.clone(),
5595            TypeExtends::custom_local("x".to_string(), "x".to_string()),
5596        );
5597        let type_x2_b = LemmaType::new(
5598            "x2b".to_string(),
5599            measure_spec,
5600            TypeExtends::custom_local("x".to_string(), "x".to_string()),
5601        );
5602        assert!(type_x2_a.same_measure_family(&type_x2_b));
5603    }
5604
5605    #[test]
5606    fn test_same_measure_family_different_families() {
5607        let measure_spec = TypeSpecification::measure();
5608        let money = LemmaType::new(
5609            "money".to_string(),
5610            measure_spec.clone(),
5611            TypeExtends::Primitive,
5612        );
5613        let temperature = LemmaType::new(
5614            "temperature".to_string(),
5615            measure_spec,
5616            TypeExtends::Primitive,
5617        );
5618        assert!(!money.same_measure_family(&temperature));
5619        assert!(!temperature.same_measure_family(&money));
5620    }
5621
5622    #[test]
5623    fn test_same_measure_family_measure_vs_non_measure() {
5624        let measure_spec = TypeSpecification::measure();
5625        let number_spec = TypeSpecification::number();
5626        let measure_type =
5627            LemmaType::new("money".to_string(), measure_spec, TypeExtends::Primitive);
5628        let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
5629        assert!(!measure_type.same_measure_family(&number_type));
5630        assert!(!number_type.same_measure_family(&measure_type));
5631    }
5632
5633    #[test]
5634    fn test_same_measure_family_anonymous_measures_are_not_family_compatible() {
5635        let left = LemmaType::anonymous_for_decomposition(duration_decomposition());
5636        let right = LemmaType::anonymous_for_decomposition(duration_decomposition());
5637
5638        assert!(!left.same_measure_family(&right));
5639        assert!(left.compatible_with_anonymous_measure(&right));
5640    }
5641
5642    #[test]
5643    fn test_measure_family_name_non_measure_returns_none() {
5644        let number_spec = TypeSpecification::number();
5645        let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
5646        assert_eq!(number_type.measure_family_name(), None);
5647    }
5648
5649    #[test]
5650    fn test_lemma_type_inequality_local_vs_import_same_shape() {
5651        let measure_spec = TypeSpecification::measure();
5652        let local = LemmaType::new(
5653            "t".to_string(),
5654            measure_spec.clone(),
5655            TypeExtends::custom_local("money".to_string(), "money".to_string()),
5656        );
5657        let imported = LemmaType::new(
5658            "t".to_string(),
5659            measure_spec,
5660            TypeExtends::Custom {
5661                parent: "money".to_string(),
5662                family: "money".to_string(),
5663                defining_spec: TypeDefiningSpec::Import,
5664            },
5665        );
5666        assert_ne!(local, imported);
5667    }
5668
5669    #[test]
5670    fn test_lemma_type_equality_import_unit_variant() {
5671        let measure_spec = TypeSpecification::measure();
5672        let left = LemmaType::new(
5673            "t".to_string(),
5674            measure_spec.clone(),
5675            TypeExtends::Custom {
5676                parent: "money".to_string(),
5677                family: "money".to_string(),
5678                defining_spec: TypeDefiningSpec::Import,
5679            },
5680        );
5681        let right = LemmaType::new(
5682            "t".to_string(),
5683            measure_spec,
5684            TypeExtends::Custom {
5685                parent: "money".to_string(),
5686                family: "money".to_string(),
5687                defining_spec: TypeDefiningSpec::Import,
5688            },
5689        );
5690        assert_eq!(left, right);
5691    }
5692
5693    fn month_suggestion_arg() -> CommandArg {
5694        CommandArg::Literal(crate::literals::Value::NumberWithUnit(
5695            Decimal::ONE,
5696            "month".to_string(),
5697        ))
5698    }
5699
5700    fn unit_factor_arg(name: &str, factor: i64) -> [CommandArg; 2] {
5701        [
5702            CommandArg::Label(name.to_string()),
5703            CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::from(factor))),
5704        ]
5705    }
5706
5707    #[test]
5708    fn default_calendar_on_text_reports_hint() {
5709        let mut specs = TypeSpecification::text();
5710        let mut default = None;
5711        let err = specs
5712            .apply_constraint(
5713                "notes",
5714                TypeConstraintCommand::Suggest,
5715                &[month_suggestion_arg()],
5716                &mut default,
5717            )
5718            .unwrap_err();
5719        assert!(err.contains("Unit 'month' is for calendar data"));
5720        assert!(err.contains("double quotes"));
5721    }
5722
5723    #[test]
5724    fn default_calendar_on_duration_reports_valid_units() {
5725        let mut specs = TypeSpecification::measure();
5726        specs
5727            .apply_constraint(
5728                "duration",
5729                TypeConstraintCommand::Unit,
5730                &unit_factor_arg("second", 1),
5731                &mut None,
5732            )
5733            .unwrap();
5734        specs
5735            .apply_constraint(
5736                "duration",
5737                TypeConstraintCommand::Unit,
5738                &unit_factor_arg("week", 604_800),
5739                &mut None,
5740            )
5741            .unwrap();
5742        specs
5743            .apply_constraint(
5744                "duration",
5745                TypeConstraintCommand::Trait,
5746                &[CommandArg::Label("duration".to_string())],
5747                &mut None,
5748            )
5749            .unwrap();
5750        let mut default = None;
5751        let err = specs
5752            .apply_constraint(
5753                "duration",
5754                TypeConstraintCommand::Suggest,
5755                &[month_suggestion_arg()],
5756                &mut default,
5757            )
5758            .unwrap_err();
5759        assert!(err.contains("Unit 'month' is for calendar data"));
5760        assert!(err.contains("Valid 'duration' units are"));
5761        assert!(err.contains("week"));
5762    }
5763
5764    #[test]
5765    fn default_valid_duration_weeks_accepted() {
5766        let mut specs = TypeSpecification::measure();
5767        specs
5768            .apply_constraint(
5769                "duration",
5770                TypeConstraintCommand::Unit,
5771                &unit_factor_arg("second", 1),
5772                &mut None,
5773            )
5774            .unwrap();
5775        specs
5776            .apply_constraint(
5777                "duration",
5778                TypeConstraintCommand::Unit,
5779                &unit_factor_arg("week", 604_800),
5780                &mut None,
5781            )
5782            .unwrap();
5783        specs
5784            .apply_constraint(
5785                "duration",
5786                TypeConstraintCommand::Trait,
5787                &[CommandArg::Label("duration".to_string())],
5788                &mut None,
5789            )
5790            .unwrap();
5791        let mut default = None;
5792        specs
5793            .apply_constraint(
5794                "duration",
5795                TypeConstraintCommand::Suggest,
5796                &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
5797                    Decimal::from(4),
5798                    "week".to_string(),
5799                ))],
5800                &mut default,
5801            )
5802            .unwrap();
5803        assert!(matches!(
5804            default,
5805            Some(RawSuggestion::Measure {
5806                unit_name,
5807                ..
5808            }) if unit_name == "week"
5809        ));
5810    }
5811
5812    #[test]
5813    fn default_unknown_unit_on_duration_lists_valid_units() {
5814        let mut specs = TypeSpecification::measure();
5815        specs
5816            .apply_constraint(
5817                "duration",
5818                TypeConstraintCommand::Unit,
5819                &unit_factor_arg("second", 1),
5820                &mut None,
5821            )
5822            .unwrap();
5823        specs
5824            .apply_constraint(
5825                "duration",
5826                TypeConstraintCommand::Trait,
5827                &[CommandArg::Label("duration".to_string())],
5828                &mut None,
5829            )
5830            .unwrap();
5831        let mut default = None;
5832        let err = specs
5833            .apply_constraint(
5834                "duration",
5835                TypeConstraintCommand::Suggest,
5836                &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
5837                    Decimal::ONE,
5838                    "fortnight".to_string(),
5839                ))],
5840                &mut default,
5841            )
5842            .unwrap_err();
5843        assert!(err.contains("fortnight"));
5844        assert!(err.contains("not defined on 'duration'"));
5845        assert!(err.contains("Valid units are"));
5846    }
5847
5848    fn money_measure_type() -> LemmaType {
5849        LemmaType::new(
5850            "Money".to_string(),
5851            TypeSpecification::Measure {
5852                minimum: None,
5853                maximum: None,
5854                decimals: None,
5855                units: MeasureUnits::from(vec![
5856                    MeasureUnit {
5857                        name: "eur".to_string(),
5858                        factor: crate::computation::rational::rational_one(),
5859                        derived_measure_factors: Vec::new(),
5860                        decomposition: BaseMeasureVector::new(),
5861                        minimum: None,
5862                        maximum: None,
5863                        suggestion_magnitude: None,
5864                    },
5865                    MeasureUnit {
5866                        name: "usd".to_string(),
5867                        factor: crate::computation::rational::decimal_to_rational(Decimal::new(
5868                            91, 2,
5869                        ))
5870                        .expect("factor"),
5871                        derived_measure_factors: Vec::new(),
5872                        decomposition: BaseMeasureVector::new(),
5873                        minimum: None,
5874                        maximum: None,
5875                        suggestion_magnitude: None,
5876                    },
5877                ]),
5878                traits: Vec::new(),
5879                decomposition: None,
5880                help: String::new(),
5881            },
5882            TypeExtends::Primitive,
5883        )
5884    }
5885
5886    #[test]
5887    fn measure_unit_names_for_named_measure() {
5888        let money = money_measure_type();
5889        assert_eq!(money.measure_unit_names(), Some(vec!["eur", "usd"]));
5890    }
5891
5892    // ---------------------------------------------------------------------------
5893    // Phase 0 — pin combine_signatures and canonicalize_signature behavior
5894    // ---------------------------------------------------------------------------
5895
5896    fn sig(pairs: &[(&str, i32)]) -> Vec<(String, i32)> {
5897        pairs.iter().map(|(s, e)| (s.to_string(), *e)).collect()
5898    }
5899
5900    #[test]
5901    fn combine_signatures_multiply_adds_exponents() {
5902        let left = sig(&[("eur", 1)]);
5903        let right = sig(&[("hour", -1)]);
5904        let result = combine_signatures(&left, &right, true);
5905        assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
5906    }
5907
5908    #[test]
5909    fn combine_signatures_divide_subtracts_exponents() {
5910        let left = sig(&[("eur", 1)]);
5911        let right = sig(&[("hour", 1)]);
5912        let result = combine_signatures(&left, &right, false);
5913        assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
5914    }
5915
5916    #[test]
5917    fn combine_signatures_cancels_to_empty() {
5918        let left = sig(&[("ce", 1), ("minute", -1)]);
5919        let right = sig(&[("minute", 1)]);
5920        let result = combine_signatures(&left, &right, true);
5921        // ce * (ce/min * min) = ce; minute cancels
5922        assert_eq!(result, sig(&[("ce", 1)]));
5923    }
5924
5925    #[test]
5926    fn combine_signatures_output_is_canonical_form() {
5927        let left = sig(&[("eur", 1), ("hour", 1)]);
5928        let right = sig(&[("minute", 1)]);
5929        let result = combine_signatures(&left, &right, false); // divide
5930                                                               // [("eur",1),("hour",1)] / [("minute",1)] = [("eur",1),("hour",1),("minute",-1)]
5931        let expected = sig(&[("eur", 1), ("hour", 1), ("minute", -1)]);
5932        assert_eq!(result, expected);
5933    }
5934
5935    #[test]
5936    fn canonicalize_signature_drops_zero_exponents() {
5937        let sig_with_zero = sig(&[("eur", 1), ("hour", 0), ("minute", -1)]);
5938        let result = canonicalize_signature(&sig_with_zero);
5939        assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
5940    }
5941
5942    #[test]
5943    fn canonicalize_signature_sorts_by_name() {
5944        let unsorted = sig(&[("minute", -1), ("eur", 1)]);
5945        let result = canonicalize_signature(&unsorted);
5946        assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
5947    }
5948
5949    // ---------------------------------------------------------------------------
5950    // Phase 0 — format_signature_operator_style (to be implemented in
5951    // signature_factor_and_display todo)
5952    // ---------------------------------------------------------------------------
5953
5954    #[test]
5955    fn format_signature_operator_style_numerator_only() {
5956        let signature = sig(&[("eur", 1)]);
5957        let result = format_signature_operator_style(&signature);
5958        assert_eq!(result, "eur");
5959    }
5960
5961    #[test]
5962    fn format_signature_operator_style_with_denominator() {
5963        let signature = sig(&[("eur", 1), ("hour", -1)]);
5964        let result = format_signature_operator_style(&signature);
5965        assert_eq!(result, "eur/hour");
5966    }
5967
5968    #[test]
5969    fn format_signature_operator_style_denominator_only() {
5970        let signature = sig(&[("meter", -1)]);
5971        let result = format_signature_operator_style(&signature);
5972        assert_eq!(result, "1/meter");
5973    }
5974
5975    #[test]
5976    fn format_signature_operator_style_with_exponents() {
5977        let signature = sig(&[("meter", 2), ("second", -2)]);
5978        let result = format_signature_operator_style(&signature);
5979        assert_eq!(result, "meter^2/second^2");
5980    }
5981
5982    // ---------------------------------------------------------------------------
5983    // Phase 0 — calendar_unit_factor (to be implemented in builtin_calendar_factor_table)
5984    // ---------------------------------------------------------------------------
5985
5986    #[test]
5987    fn calendar_unit_factor_table_completeness() {
5988        // Every SemanticCalendarUnit Display string must resolve to a factor.
5989        // Today SemanticCalendarUnit only has Month and Year; more may be added.
5990        for unit in &[SemanticCalendarUnit::Month, SemanticCalendarUnit::Year] {
5991            let name = unit.to_string();
5992            assert!(
5993                calendar_unit_factor(&name).is_some(),
5994                "calendar_unit_factor('{}') must return Some",
5995                name
5996            );
5997        }
5998    }
5999
6000    #[test]
6001    fn semantic_calendar_unit_display_returns_singular() {
6002        // Today Month => "month", Year => "year" (plural).
6003        // After singular_calendar_names_everywhere, must be "month" and "year".
6004        assert_eq!(SemanticCalendarUnit::Month.to_string(), "month");
6005        assert_eq!(SemanticCalendarUnit::Year.to_string(), "year");
6006    }
6007
6008    // ---------------------------------------------------------------------------
6009    // Phase 0 — signature_factor (to be implemented in signature_factor_and_display)
6010    // ---------------------------------------------------------------------------
6011
6012    #[test]
6013    fn signature_factor_with_calendar_units() {
6014        let calendar = test_calendar_type_for_signature_factor();
6015        let unit_index = crate::planning::unit_index::UnitIndex::new();
6016        // month factor = 1, year factor = 12.
6017        // [(month,1),(year,-1)] = 1/12
6018        let sig_month_per_year = sig(&[("month", 1), ("year", -1)]);
6019        let factor = signature_factor(&sig_month_per_year, &unit_index, Some(&calendar))
6020            .expect("must not overflow");
6021        let expected = rational_new(1, 12);
6022        assert_eq!(factor, expected, "month/year factor must be 1/12");
6023    }
6024
6025    fn test_calendar_type_for_signature_factor() -> LemmaType {
6026        use crate::computation::rational::{decimal_to_rational, rational_one};
6027        use crate::literals::{MeasureUnit, MeasureUnits};
6028        use rust_decimal::Decimal;
6029        LemmaType::new(
6030            "calendar".to_string(),
6031            TypeSpecification::Measure {
6032                minimum: None,
6033                maximum: None,
6034                decimals: None,
6035                units: MeasureUnits::from(vec![
6036                    MeasureUnit {
6037                        name: "month".to_string(),
6038                        factor: rational_one(),
6039                        minimum: None,
6040                        maximum: None,
6041                        suggestion_magnitude: None,
6042                        decomposition: calendar_decomposition(),
6043                        derived_measure_factors: Vec::new(),
6044                    },
6045                    MeasureUnit {
6046                        name: "year".to_string(),
6047                        factor: decimal_to_rational(Decimal::from(12)).expect("year factor"),
6048                        minimum: None,
6049                        maximum: None,
6050                        suggestion_magnitude: None,
6051                        decomposition: calendar_decomposition(),
6052                        derived_measure_factors: Vec::new(),
6053                    },
6054                ]),
6055                traits: vec![MeasureTrait::Calendar],
6056                decomposition: Some(calendar_decomposition()),
6057                help: String::new(),
6058            },
6059            TypeExtends::Primitive,
6060        )
6061    }
6062
6063    #[test]
6064    #[should_panic(expected = "BUG: signature_factor called with unresolved unit name")]
6065    fn signature_factor_panics_on_unresolved_name() {
6066        let unit_index = crate::planning::unit_index::UnitIndex::new();
6067        let bad_sig = sig(&[("nonexistent_unit_xyz", 1)]);
6068        let _ = signature_factor(&bad_sig, &unit_index, None);
6069    }
6070
6071    #[test]
6072    fn signature_factor_uses_owner_when_expression_index_empty() {
6073        let money = test_money_type_for_signature_factor();
6074        let expression_units = crate::planning::unit_index::UnitIndex::new();
6075        let sig_usd = sig(&[("usd", 1)]);
6076        let factor =
6077            signature_factor(&sig_usd, &expression_units, Some(&money)).expect("must not overflow");
6078        assert_eq!(factor, rational_new(91, 100));
6079    }
6080
6081    fn test_money_type_for_signature_factor() -> LemmaType {
6082        use crate::computation::rational::decimal_to_rational;
6083        use crate::literals::{MeasureUnit, MeasureUnits};
6084        use rust_decimal::Decimal;
6085        LemmaType::new(
6086            "money".to_string(),
6087            TypeSpecification::Measure {
6088                minimum: None,
6089                maximum: None,
6090                decimals: Some(2),
6091                units: MeasureUnits::from(vec![
6092                    MeasureUnit {
6093                        name: "eur".to_string(),
6094                        factor: crate::computation::rational::rational_one(),
6095                        minimum: None,
6096                        maximum: None,
6097                        suggestion_magnitude: None,
6098                        decomposition: BaseMeasureVector::new(),
6099                        derived_measure_factors: Vec::new(),
6100                    },
6101                    MeasureUnit {
6102                        name: "usd".to_string(),
6103                        factor: decimal_to_rational(Decimal::new(91, 2)).expect("usd factor"),
6104                        minimum: None,
6105                        maximum: None,
6106                        suggestion_magnitude: None,
6107                        decomposition: BaseMeasureVector::new(),
6108                        derived_measure_factors: Vec::new(),
6109                    },
6110                ]),
6111                traits: Vec::new(),
6112                decomposition: None,
6113                help: String::new(),
6114            },
6115            TypeExtends::Primitive,
6116        )
6117    }
6118
6119    fn measure_type_with_kilogram() -> TypeSpecification {
6120        use crate::computation::rational::rational_one;
6121        use crate::literals::{MeasureUnit, MeasureUnits};
6122        let mut units = MeasureUnits::new();
6123        units.push(MeasureUnit {
6124            name: "kilogram".to_string(),
6125            factor: rational_one(),
6126            minimum: None,
6127            maximum: None,
6128            suggestion_magnitude: None,
6129            decomposition: BaseMeasureVector::new(),
6130            derived_measure_factors: Vec::new(),
6131        });
6132        TypeSpecification::Measure {
6133            minimum: None,
6134            maximum: None,
6135            decimals: None,
6136            units,
6137            traits: Vec::new(),
6138            decomposition: None,
6139            help: String::new(),
6140        }
6141    }
6142
6143    #[test]
6144    fn parser_value_to_value_kind_rejects_bare_number_for_measure() {
6145        let ten = Value::Number(Decimal::from(10));
6146        let err = parser_value_to_value_kind(&ten, &measure_type_with_kilogram())
6147            .expect_err("bare number must not bind to measure");
6148        assert!(
6149            err.contains("kilogram"),
6150            "error must hint expected unit, got: {err}"
6151        );
6152    }
6153
6154    #[test]
6155    fn parser_value_to_value_kind_accepts_number_with_unit_for_measure() {
6156        let ten_kg = Value::NumberWithUnit(Decimal::from(10), "kilogram".to_string());
6157        let kind = parser_value_to_value_kind(&ten_kg, &measure_type_with_kilogram())
6158            .expect("10 kilogram must bind to measure");
6159        assert!(matches!(kind, ValueKind::Measure(_, _)));
6160    }
6161
6162    #[test]
6163    fn parser_value_to_value_kind_accepts_bare_number_for_ratio() {
6164        let ten = Value::Number(Decimal::from(10));
6165        let kind =
6166            parser_value_to_value_kind(&ten, &TypeSpecification::ratio()).expect("number -> ratio");
6167        assert!(matches!(kind, ValueKind::Ratio(_, None)));
6168    }
6169
6170    #[test]
6171    fn value_kind_matches_spec_rejects_number_for_measure() {
6172        let n = ValueKind::Number(rational_new(10, 1));
6173        assert!(!value_kind_matches_spec(&n, &measure_type_with_kilogram()));
6174    }
6175
6176    #[test]
6177    fn apply_constraint_rejects_inherited_unit_factor_change() {
6178        let mut specs = TypeSpecification::measure();
6179        specs
6180            .apply_constraint(
6181                "money",
6182                TypeConstraintCommand::Unit,
6183                &unit_factor_arg("eur", 1),
6184                &mut None,
6185            )
6186            .expect("seed eur");
6187        let err = specs
6188            .apply_constraint(
6189                "money",
6190                TypeConstraintCommand::Unit,
6191                &[
6192                    CommandArg::Label("eur".to_string()),
6193                    CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::new(11, 1))),
6194                ],
6195                &mut None,
6196            )
6197            .expect_err("must not change inherited unit factor");
6198        assert!(err.contains("eur"), "error must name unit, got: {err}");
6199        assert!(
6200            err.contains("inherited") || err.contains("cannot change"),
6201            "error must reject factor change, got: {err}"
6202        );
6203    }
6204
6205    #[test]
6206    fn apply_constraint_allows_additive_unit_on_inherited_spec() {
6207        let mut specs = TypeSpecification::measure();
6208        specs
6209            .apply_constraint(
6210                "money",
6211                TypeConstraintCommand::Unit,
6212                &unit_factor_arg("eur", 1),
6213                &mut None,
6214            )
6215            .expect("seed eur");
6216        specs
6217            .apply_constraint(
6218                "money",
6219                TypeConstraintCommand::Unit,
6220                &unit_factor_arg("usd", 1),
6221                &mut None,
6222            )
6223            .expect("add usd");
6224        match &specs {
6225            TypeSpecification::Measure { units, .. } => assert_eq!(units.len(), 2),
6226            other => panic!("expected Measure, got {other:?}"),
6227        }
6228    }
6229
6230    #[test]
6231    fn apply_constraint_idempotent_inherited_unit_redeclare() {
6232        let mut specs = TypeSpecification::measure();
6233        specs
6234            .apply_constraint(
6235                "money",
6236                TypeConstraintCommand::Unit,
6237                &unit_factor_arg("eur", 1),
6238                &mut None,
6239            )
6240            .expect("seed eur");
6241        specs
6242            .apply_constraint(
6243                "money",
6244                TypeConstraintCommand::Unit,
6245                &unit_factor_arg("eur", 1),
6246                &mut None,
6247            )
6248            .expect("idempotent eur");
6249        match &specs {
6250            TypeSpecification::Measure { units, .. } => {
6251                assert_eq!(units.len(), 1);
6252                assert_eq!(
6253                    units.iter().find(|u| u.name == "eur").expect("eur").factor,
6254                    crate::computation::rational::rational_one()
6255                );
6256            }
6257            other => panic!("expected Measure, got {other:?}"),
6258        }
6259    }
6260
6261    #[test]
6262    fn element_from_range_returns_element_for_every_range_primitive() {
6263        type RangeElementMatcher = fn(&TypeSpecification) -> bool;
6264        let cases: [(PrimitiveKind, RangeElementMatcher); 5] = [
6265            (PrimitiveKind::NumberRange, |element| {
6266                matches!(element, TypeSpecification::Number { .. })
6267            }),
6268            (PrimitiveKind::MeasureRange, |element| {
6269                matches!(element, TypeSpecification::Measure { .. })
6270            }),
6271            (PrimitiveKind::RatioRange, |element| {
6272                matches!(element, TypeSpecification::Ratio { .. })
6273            }),
6274            (PrimitiveKind::DateRange, |element| {
6275                matches!(element, TypeSpecification::Date { .. })
6276            }),
6277            (PrimitiveKind::TimeRange, |element| {
6278                matches!(element, TypeSpecification::Time { .. })
6279            }),
6280        ];
6281        for (kind, matches_element) in cases {
6282            let range_spec = type_spec_for_primitive(kind);
6283            let element = range_spec
6284                .element_from_range()
6285                .unwrap_or_else(|| panic!("{kind:?} must define element_from_range"));
6286            assert!(
6287                matches_element(&element),
6288                "{kind:?} element must match documented mapping, got {element:?}"
6289            );
6290        }
6291    }
6292
6293    #[test]
6294    fn element_from_range_returns_none_for_non_range_primitives() {
6295        let non_range = [
6296            type_spec_for_primitive(PrimitiveKind::Boolean),
6297            type_spec_for_primitive(PrimitiveKind::Measure),
6298            TypeSpecification::Undetermined,
6299            TypeSpecification::veto(),
6300        ];
6301        for spec in non_range {
6302            assert!(
6303                spec.element_from_range().is_none(),
6304                "{spec:?} must not define element_from_range"
6305            );
6306        }
6307    }
6308}