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