Skip to main content

lemma/planning/
semantics.rs

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