Skip to main content

lemma/planning/
semantics.rs

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