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    /// Wire-form [`ValueKind`] for API JSON (schema defaults, response data echo).
3493    pub fn value_kind_for_api_wire(&self, canonical: &ValueKind) -> Result<ValueKind, String> {
3494        match canonical {
3495            ValueKind::Measure(canonical_magnitude, signature) => {
3496                let unit_name = LiteralValue::single_measure_signature_unit_name(signature)
3497                    .ok_or_else(|| {
3498                        format!(
3499                            "BUG: API measure literal on type '{}' must have exactly one signature unit with exponent 1, got {signature:?}",
3500                            self.name()
3501                        )
3502                    })?;
3503                let per_unit_decimal = self
3504                    .try_materialize_measure_canonical_in_unit(canonical_magnitude, unit_name)
3505                    .map_err(|failure| failure.to_string())?;
3506                let per_unit_rational = crate::literals::rational_from_parsed_decimal(
3507                    decimal_from_serialized_str(&per_unit_decimal)?,
3508                )?;
3509                Ok(ValueKind::Measure(per_unit_rational, signature.clone()))
3510            }
3511            ValueKind::Ratio(canonical_magnitude, unit) => match unit.as_deref() {
3512                Some(unit_name) => {
3513                    let per_unit_decimal = self
3514                        .try_materialize_ratio_canonical_in_unit(canonical_magnitude, unit_name)
3515                        .map_err(|failure| failure.to_string())?;
3516                    let per_unit_rational = crate::literals::rational_from_parsed_decimal(
3517                        decimal_from_serialized_str(&per_unit_decimal)?,
3518                    )?;
3519                    Ok(ValueKind::Ratio(per_unit_rational, unit.clone()))
3520                }
3521                None => Ok(ValueKind::Ratio(canonical_magnitude.clone(), None)),
3522            },
3523            ValueKind::Number(rational) => {
3524                let decimal_string = self
3525                    .try_materialize_rational_as_decimal_string(rational)
3526                    .map_err(|failure| failure.to_string())?;
3527                let decimal = decimal_from_serialized_str(&decimal_string)?;
3528                Ok(ValueKind::Number(
3529                    crate::literals::rational_from_parsed_decimal(decimal)?,
3530                ))
3531            }
3532            ValueKind::Range(left, right) => {
3533                let element_spec = range_element_type_specification(&self.specifications)
3534                    .ok_or_else(|| {
3535                        format!(
3536                            "BUG: range literal on type '{}' has no element specification",
3537                            self.name()
3538                        )
3539                    })?;
3540                let element_type = Arc::new(LemmaType::primitive(element_spec));
3541                Ok(ValueKind::Range(
3542                    Box::new(LiteralValue {
3543                        value: element_type.value_kind_for_api_wire(&left.value)?,
3544                        lemma_type: Arc::clone(&element_type),
3545                    }),
3546                    Box::new(LiteralValue {
3547                        value: element_type.value_kind_for_api_wire(&right.value)?,
3548                        lemma_type: Arc::clone(&element_type),
3549                    }),
3550                ))
3551            }
3552            other => Ok(other.clone()),
3553        }
3554    }
3555
3556    /// Restore in-memory canonical storage after API JSON deserialization.
3557    pub fn value_kind_from_api_wire(&self, wire: ValueKind) -> Result<ValueKind, String> {
3558        match (&self.specifications, wire) {
3559            (TypeSpecification::Measure { .. }, ValueKind::Measure(per_unit, signature)) => {
3560                let unit_name = LiteralValue::single_measure_signature_unit_name(&signature)
3561                    .ok_or_else(|| {
3562                        format!(
3563                            "measure literal on type '{}' must have exactly one signature unit with exponent 1",
3564                            self.name()
3565                        )
3566                    })?;
3567                let decimal = crate::computation::rational::commit_rational_to_decimal(&per_unit)
3568                    .map_err(|failure| failure.to_string())?;
3569                number_with_unit_to_value_kind(decimal, unit_name, self)
3570            }
3571            (TypeSpecification::Ratio { .. }, ValueKind::Ratio(per_unit, unit)) => {
3572                match unit.as_deref() {
3573                    Some(unit_name) => {
3574                        let decimal =
3575                            crate::computation::rational::commit_rational_to_decimal(&per_unit)
3576                                .map_err(|failure| failure.to_string())?;
3577                        number_with_unit_to_value_kind(decimal, unit_name, self)
3578                    }
3579                    None => Ok(ValueKind::Ratio(per_unit, unit)),
3580                }
3581            }
3582            (_, ValueKind::Range(left, right)) => {
3583                let element_spec = range_element_type_specification(&self.specifications)
3584                    .ok_or_else(|| {
3585                        format!(
3586                            "BUG: range literal on type '{}' has no element specification",
3587                            self.name()
3588                        )
3589                    })?;
3590                let element_type = Arc::new(LemmaType::primitive(element_spec));
3591                Ok(ValueKind::Range(
3592                    Box::new(LiteralValue {
3593                        value: element_type.value_kind_from_api_wire(left.value.clone())?,
3594                        lemma_type: Arc::clone(&element_type),
3595                    }),
3596                    Box::new(LiteralValue {
3597                        value: element_type.value_kind_from_api_wire(right.value.clone())?,
3598                        lemma_type: Arc::clone(&element_type),
3599                    }),
3600                ))
3601            }
3602            (_, other) => Ok(other),
3603        }
3604    }
3605
3606    /// Get an example value string for this type, suitable for UI help text
3607    pub fn example_value(&self) -> &'static str {
3608        match &self.specifications {
3609            TypeSpecification::Text { .. } => "\"hello world\"",
3610            TypeSpecification::Measure { .. } => "12.50 eur",
3611            TypeSpecification::MeasureRange { .. } => "30 kilogram...35 kilogram",
3612            TypeSpecification::Number { .. } => "3.14",
3613            TypeSpecification::NumberRange { .. } => "0...100",
3614            TypeSpecification::Boolean { .. } => "true",
3615            TypeSpecification::Date { .. } => "2023-12-25T14:30:00Z",
3616            TypeSpecification::DateRange { .. } => "2024-01-01...2024-12-31",
3617            TypeSpecification::TimeRange { .. } => "09:00...17:00",
3618            TypeSpecification::Veto { .. } => "veto",
3619            TypeSpecification::Time { .. } => "14:30:00",
3620            TypeSpecification::Ratio { .. } => "50%",
3621            TypeSpecification::RatioRange { .. } => "10%...50%",
3622            TypeSpecification::Undetermined => unreachable!(
3623                "BUG: example_value called on Undetermined sentinel type; this type must never reach user-facing code"
3624            ),
3625        }
3626    }
3627
3628    /// Factor for a unit of this measure type (for unit conversion during evaluation only).
3629    /// Planning must validate conversions first and return Error for invalid units.
3630    /// If called with a non-measure type or unknown unit name, panics (invariant violation).
3631    #[must_use]
3632    /// Returns the resolved `BaseMeasureVector` for Measure types, or `None` if
3633    /// the decomposition pass has not yet resolved this type.
3634    /// Panics if called on non-Measure types.
3635    pub fn measure_type_decomposition(&self) -> Option<&BaseMeasureVector> {
3636        match &self.specifications {
3637            TypeSpecification::Measure { decomposition, .. } => decomposition.as_ref(),
3638            _ => unreachable!(
3639                "BUG: measure_type_decomposition called on non-measure type {}",
3640                self.name()
3641            ),
3642        }
3643    }
3644
3645    /// Returns true if this is an anonymous (no-name) Measure — i.e. an anonymous
3646    /// intermediate produced by cross-axis arithmetic.
3647    pub fn is_anonymous_measure(&self) -> bool {
3648        self.name.is_none() && matches!(&self.specifications, TypeSpecification::Measure { .. })
3649    }
3650
3651    /// Build an anonymous `LemmaType` for a given dimensional decomposition.
3652    /// Used at plan time to represent the inferred type of cross-axis intermediates.
3653    /// Signatures live on the value, not on the type.
3654    pub fn anonymous_for_decomposition(decomposition: BaseMeasureVector) -> Self {
3655        Self {
3656            name: None,
3657            specifications: TypeSpecification::Measure {
3658                minimum: None,
3659                maximum: None,
3660                decimals: None,
3661                units: crate::literals::MeasureUnits::new(),
3662                traits: Vec::new(),
3663                decomposition: Some(decomposition),
3664                help: String::new(),
3665            },
3666            extends: TypeExtends::Primitive,
3667        }
3668    }
3669
3670    /// Declared unit names when the type carries a non-empty unit table (`None` otherwise).
3671    #[must_use]
3672    pub fn measure_unit_names(&self) -> Option<Vec<&str>> {
3673        match &self.specifications {
3674            TypeSpecification::Measure { units, .. } if !units.is_empty() => {
3675                Some(units.iter().map(|unit| unit.name.as_str()).collect())
3676            }
3677            TypeSpecification::MeasureRange { units, .. } if !units.is_empty() => {
3678                Some(units.iter().map(|unit| unit.name.as_str()).collect())
3679            }
3680            _ => None,
3681        }
3682    }
3683
3684    /// Return the conversion factor for a declared unit name on this measure type.
3685    pub fn measure_unit_factor(
3686        &self,
3687        unit_name: &str,
3688    ) -> &crate::computation::rational::RationalInteger {
3689        let units = match &self.specifications {
3690            TypeSpecification::Measure { units, .. } => units,
3691            TypeSpecification::MeasureRange { units, .. } => units,
3692            _ => unreachable!(
3693                "BUG: measure_unit_factor called with non-measure type {}; only call during evaluation after planning validated measure conversion",
3694                self.name()
3695            ),
3696        };
3697        match units.get(unit_name) {
3698            Ok(MeasureUnit { factor, .. }) => factor,
3699            Err(_) => {
3700                let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
3701                unreachable!(
3702                    "BUG: unknown unit '{}' for measure type {} (valid: {}); planning must reject invalid conversions with Error",
3703                    unit_name,
3704                    self.name(),
3705                    valid.join(", ")
3706                );
3707            }
3708        }
3709    }
3710
3711    pub fn ratio_unit_factor(
3712        &self,
3713        unit_name: &str,
3714    ) -> &crate::computation::rational::RationalInteger {
3715        let units = match &self.specifications {
3716            TypeSpecification::Ratio { units, .. } => units,
3717            _ => unreachable!(
3718                "BUG: ratio_unit_factor called with non-ratio type {}; only call during evaluation after planning validated ratio conversion",
3719                self.name()
3720            ),
3721        };
3722        match units.get(unit_name) {
3723            Ok(RatioUnit { value, .. }) => value,
3724            Err(_) => {
3725                let valid: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
3726                unreachable!(
3727                    "BUG: unknown unit '{}' for ratio type {} (valid: {}); planning must reject invalid conversions with Error",
3728                    unit_name,
3729                    self.name(),
3730                    valid.join(", ")
3731                );
3732            }
3733        }
3734    }
3735}
3736
3737/// Literal value with type. The single value type in semantics.
3738#[derive(Clone, Debug, PartialEq, Eq, Hash)]
3739pub struct LiteralValue {
3740    pub value: ValueKind,
3741    pub lemma_type: Arc<LemmaType>,
3742}
3743
3744impl LiteralValue {
3745    fn single_measure_signature_unit_name(signature: &[(String, i32)]) -> Option<&str> {
3746        match signature {
3747            [(unit_name, 1)] => Some(unit_name.as_str()),
3748            _ => None,
3749        }
3750    }
3751}
3752
3753/// API-only serde for [`LiteralValue`]: per-unit wire magnitudes on schema/response fields.
3754pub mod api_wire_literal {
3755    use super::{Arc, LemmaType, LiteralValue, ValueKind};
3756    use serde::{Deserialize, Deserializer, Serializer};
3757
3758    pub fn serialize<S: Serializer>(
3759        literal: &LiteralValue,
3760        serializer: S,
3761    ) -> Result<S::Ok, S::Error> {
3762        use serde::ser::SerializeStruct;
3763        let wire_value = literal
3764            .lemma_type
3765            .value_kind_for_api_wire(&literal.value)
3766            .map_err(serde::ser::Error::custom)?;
3767        let mut state = serializer.serialize_struct("LiteralValue", 3)?;
3768        state.serialize_field("value", &wire_value)?;
3769        state.serialize_field("lemma_type", literal.lemma_type.as_ref())?;
3770        state.serialize_field("display_value", &literal.display_value())?;
3771        state.end()
3772    }
3773
3774    pub fn deserialize<'de, D: Deserializer<'de>>(
3775        deserializer: D,
3776    ) -> Result<LiteralValue, D::Error> {
3777        #[derive(Deserialize)]
3778        struct Raw {
3779            value: ValueKind,
3780            lemma_type: LemmaType,
3781        }
3782        let raw = Raw::deserialize(deserializer)?;
3783        let lemma_type = Arc::new(raw.lemma_type);
3784        let value = lemma_type
3785            .value_kind_from_api_wire(raw.value)
3786            .map_err(serde::de::Error::custom)?;
3787        Ok(LiteralValue { value, lemma_type })
3788    }
3789
3790    pub fn serialize_option<S: Serializer>(
3791        literal: &Option<LiteralValue>,
3792        serializer: S,
3793    ) -> Result<S::Ok, S::Error> {
3794        match literal {
3795            Some(value) => serialize(value, serializer),
3796            None => serializer.serialize_none(),
3797        }
3798    }
3799
3800    pub fn deserialize_option<'de, D: Deserializer<'de>>(
3801        deserializer: D,
3802    ) -> Result<Option<LiteralValue>, D::Error> {
3803        use serde::de::{self, Visitor};
3804        use std::fmt;
3805
3806        struct OptionVisitor;
3807
3808        impl<'de> Visitor<'de> for OptionVisitor {
3809            type Value = Option<LiteralValue>;
3810
3811            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
3812                formatter.write_str("optional API-wire literal")
3813            }
3814
3815            fn visit_none<E>(self) -> Result<Self::Value, E>
3816            where
3817                E: de::Error,
3818            {
3819                Ok(None)
3820            }
3821
3822            fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
3823            where
3824                D: Deserializer<'de>,
3825            {
3826                deserialize(deserializer).map(Some)
3827            }
3828        }
3829
3830        deserializer.deserialize_option(OptionVisitor)
3831    }
3832}
3833
3834impl Serialize for LiteralValue {
3835    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3836    where
3837        S: serde::Serializer,
3838    {
3839        use serde::ser::SerializeStruct;
3840        let mut state = serializer.serialize_struct("LiteralValue", 3)?;
3841        state.serialize_field("value", &self.value)?;
3842        state.serialize_field("lemma_type", self.lemma_type.as_ref())?;
3843        state.serialize_field("display_value", &self.display_value())?;
3844        state.end()
3845    }
3846}
3847
3848impl<'de> Deserialize<'de> for LiteralValue {
3849    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3850    where
3851        D: serde::Deserializer<'de>,
3852    {
3853        #[derive(Deserialize)]
3854        struct Raw {
3855            value: ValueKind,
3856            lemma_type: LemmaType,
3857        }
3858        let raw = Raw::deserialize(deserializer)?;
3859        Ok(Self {
3860            value: raw.value,
3861            lemma_type: Arc::new(raw.lemma_type),
3862        })
3863    }
3864}
3865
3866impl LiteralValue {
3867    pub fn text(s: String) -> Self {
3868        Self {
3869            value: ValueKind::Text(s),
3870            lemma_type: primitive_text_arc().clone(),
3871        }
3872    }
3873
3874    pub fn text_with_type(s: String, lemma_type: Arc<LemmaType>) -> Self {
3875        Self {
3876            value: ValueKind::Text(s),
3877            lemma_type,
3878        }
3879    }
3880
3881    pub fn number(n: RationalInteger) -> Self {
3882        Self {
3883            value: ValueKind::Number(n),
3884            lemma_type: primitive_number_arc().clone(),
3885        }
3886    }
3887
3888    pub fn number_from_decimal(decimal: Decimal) -> Self {
3889        Self::number(
3890            crate::literals::rational_from_parsed_decimal(decimal)
3891                .expect("BUG: literal number from decimal must lift at boundary"),
3892        )
3893    }
3894
3895    pub fn number_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
3896        Self {
3897            value: ValueKind::Number(n),
3898            lemma_type,
3899        }
3900    }
3901
3902    pub fn number_with_type_from_decimal(decimal: Decimal, lemma_type: Arc<LemmaType>) -> Self {
3903        Self::number_with_type(
3904            crate::literals::rational_from_parsed_decimal(decimal)
3905                .expect("BUG: literal number from decimal must lift at boundary"),
3906            lemma_type,
3907        )
3908    }
3909
3910    /// Build a Measure literal carrying a single user-typed unit name.
3911    /// The signature is `[(unit_name, 1)]`; the normalize pass expands compound names
3912    /// against `unit_index` so all stored signatures end up in canonical (base-unit) form.
3913    pub fn measure_with_type(n: RationalInteger, unit: String, lemma_type: Arc<LemmaType>) -> Self {
3914        Self {
3915            value: ValueKind::Measure(n, vec![(unit, 1)]),
3916            lemma_type,
3917        }
3918    }
3919
3920    /// Build a Measure literal with an explicit signature (already in canonical form).
3921    /// Used by arithmetic when combining operand signatures yields a multi-term result.
3922    pub fn measure_with_signature(
3923        n: RationalInteger,
3924        signature: Vec<(String, i32)>,
3925        lemma_type: Arc<LemmaType>,
3926    ) -> Self {
3927        Self {
3928            value: ValueKind::Measure(n, signature),
3929            lemma_type,
3930        }
3931    }
3932
3933    /// Number interpreted as a measure value in the given unit (e.g. "3 as usd" where 3 is a number).
3934    /// Creates an anonymous one-unit measure type so computation does not depend on parsing types.
3935    pub fn number_interpreted_as_measure(value: RationalInteger, unit_name: String) -> Self {
3936        Self {
3937            value: ValueKind::Measure(value, vec![(unit_name, 1)]),
3938            lemma_type: Arc::new(anonymous_measure_type()),
3939        }
3940    }
3941
3942    pub fn from_bool(b: bool) -> Self {
3943        Self {
3944            value: ValueKind::Boolean(b),
3945            lemma_type: primitive_boolean_arc().clone(),
3946        }
3947    }
3948
3949    pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
3950        Self::date(date_time_to_semantic(dt))
3951    }
3952
3953    /// Magnitude string for decimal input prompts (number, single-unit measure, ratio with percent/permille scaling).
3954    #[must_use]
3955    pub fn magnitude_default_for_decimal_prompt(&self) -> Option<String> {
3956        match &self.value {
3957            ValueKind::Number(n) => Some(
3958                self.lemma_type
3959                    .try_materialize_rational_as_decimal_string(n)
3960                    .expect("BUG: stored number literal must materialize for decimal prompt"),
3961            ),
3962            ValueKind::Measure(n, signature) => {
3963                let unit_name = Self::single_measure_signature_unit_name(signature).expect(
3964                    "BUG: measure prompt requires exactly one signature unit with exponent 1",
3965                );
3966                Some(
3967                    self.lemma_type
3968                        .try_materialize_measure_canonical_in_unit(n, unit_name)
3969                        .expect("BUG: stored measure literal must materialize for decimal prompt"),
3970                )
3971            }
3972            ValueKind::Ratio(n, Some(unit_name)) => Some(
3973                self.lemma_type
3974                    .try_materialize_ratio_canonical_in_unit(n, unit_name)
3975                    .expect("BUG: stored ratio literal must materialize for decimal prompt"),
3976            ),
3977            ValueKind::Ratio(n, None) => Some(
3978                self.lemma_type
3979                    .try_materialize_rational_as_decimal_string(n)
3980                    .expect("BUG: stored bare ratio literal must materialize for decimal prompt"),
3981            ),
3982            _ => None,
3983        }
3984    }
3985
3986    pub fn date(dt: SemanticDateTime) -> Self {
3987        Self {
3988            value: ValueKind::Date(dt),
3989            lemma_type: primitive_date_arc().clone(),
3990        }
3991    }
3992
3993    pub fn date_with_type(dt: SemanticDateTime, lemma_type: Arc<LemmaType>) -> Self {
3994        Self {
3995            value: ValueKind::Date(dt),
3996            lemma_type,
3997        }
3998    }
3999
4000    pub fn time(t: SemanticTime) -> Self {
4001        Self {
4002            value: ValueKind::Time(t),
4003            lemma_type: primitive_time_arc().clone(),
4004        }
4005    }
4006
4007    pub fn time_with_type(t: SemanticTime, lemma_type: Arc<LemmaType>) -> Self {
4008        Self {
4009            value: ValueKind::Time(t),
4010            lemma_type,
4011        }
4012    }
4013
4014    pub fn calendar(
4015        value: RationalInteger,
4016        unit: SemanticCalendarUnit,
4017        lemma_type: Arc<LemmaType>,
4018    ) -> Self {
4019        Self::measure_with_type(value, unit.to_string(), lemma_type)
4020    }
4021
4022    pub fn calendar_from_decimal(
4023        value: Decimal,
4024        unit: SemanticCalendarUnit,
4025        lemma_type: Arc<LemmaType>,
4026    ) -> Self {
4027        Self::calendar(
4028            crate::literals::rational_from_parsed_decimal(value)
4029                .expect("BUG: calendar literal from decimal must lift at boundary"),
4030            unit,
4031            lemma_type,
4032        )
4033    }
4034
4035    pub fn calendar_with_type(
4036        value: RationalInteger,
4037        unit: SemanticCalendarUnit,
4038        lemma_type: Arc<LemmaType>,
4039    ) -> Self {
4040        Self::calendar(value, unit, lemma_type)
4041    }
4042
4043    /// Derive seconds from a duration measure's canonical magnitude.
4044    pub fn duration_canonical_seconds(&self) -> RationalInteger {
4045        let ValueKind::Measure(magnitude, _) = &self.value else {
4046            unreachable!(
4047                "BUG: duration_canonical_seconds called with {:?}",
4048                self.value
4049            );
4050        };
4051        if !self.lemma_type.is_duration_like_measure() {
4052            unreachable!(
4053                "BUG: duration_canonical_seconds called with type {}",
4054                self.lemma_type.name()
4055            );
4056        }
4057        let factor = self.lemma_type.measure_unit_factor("second");
4058        checked_div(magnitude, factor).expect("BUG: duration unit factor cannot be zero")
4059    }
4060
4061    /// Derive months from a calendar measure's canonical magnitude.
4062    pub fn calendar_canonical_months(&self) -> RationalInteger {
4063        let ValueKind::Measure(magnitude, _) = &self.value else {
4064            unreachable!(
4065                "BUG: calendar_canonical_months called with {:?}",
4066                self.value
4067            );
4068        };
4069        if !self.lemma_type.is_calendar_like() {
4070            unreachable!(
4071                "BUG: calendar_canonical_months called with type {}",
4072                self.lemma_type.name()
4073            );
4074        }
4075        let factor = self.lemma_type.measure_unit_factor("month");
4076        checked_div(magnitude, factor).expect("BUG: calendar unit factor cannot be zero")
4077    }
4078
4079    pub fn ratio(r: RationalInteger, unit: Option<String>) -> Self {
4080        Self {
4081            value: ValueKind::Ratio(r, unit),
4082            lemma_type: primitive_ratio_arc().clone(),
4083        }
4084    }
4085
4086    pub fn ratio_from_decimal(r: Decimal, unit: Option<String>) -> Self {
4087        Self::ratio(
4088            crate::literals::rational_from_parsed_decimal(r)
4089                .expect("BUG: ratio literal from decimal must lift at boundary"),
4090            unit,
4091        )
4092    }
4093
4094    pub fn ratio_with_type(
4095        r: RationalInteger,
4096        unit: Option<String>,
4097        lemma_type: Arc<LemmaType>,
4098    ) -> Self {
4099        Self {
4100            value: ValueKind::Ratio(r, unit),
4101            lemma_type,
4102        }
4103    }
4104
4105    pub fn range(left: LiteralValue, right: LiteralValue) -> Self {
4106        let specifications =
4107            range_type_specification_from_endpoints(&left.lemma_type, &right.lemma_type)
4108                .unwrap_or_else(|| {
4109                    unreachable!(
4110                "BUG: attempted to construct a range literal from incompatible endpoint types"
4111            )
4112                });
4113
4114        Self {
4115            value: ValueKind::Range(Box::new(left), Box::new(right)),
4116            lemma_type: Arc::new(LemmaType::primitive(specifications)),
4117        }
4118    }
4119
4120    /// Get a display string for this value (for UI/output)
4121    pub fn display_value(&self) -> String {
4122        format!("{}", self)
4123    }
4124
4125    /// Approximate byte size for resource limit checks (string representation length)
4126    pub fn byte_size(&self) -> usize {
4127        format!("{}", self).len()
4128    }
4129
4130    /// Get the resolved type of this literal
4131    pub fn get_type(&self) -> &LemmaType {
4132        &self.lemma_type
4133    }
4134}
4135
4136/// Response/UI row for spec data: [`LemmaType`] plus optional evaluated literal.
4137#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
4138#[serde(rename_all = "snake_case")]
4139pub enum DataValue {
4140    Definition {
4141        schema_type: LemmaType,
4142        #[serde(
4143            default,
4144            skip_serializing_if = "Option::is_none",
4145            serialize_with = "api_wire_literal::serialize_option",
4146            deserialize_with = "api_wire_literal::deserialize_option"
4147        )]
4148        value: Option<LiteralValue>,
4149    },
4150}
4151
4152impl DataValue {
4153    #[must_use]
4154    pub fn from_literal(value: LiteralValue) -> Self {
4155        let schema_type = value.get_type().clone();
4156        Self::Definition {
4157            schema_type,
4158            value: Some(value),
4159        }
4160    }
4161}
4162
4163/// Data: path, value, and source location.
4164#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
4165pub struct Data {
4166    pub path: DataPath,
4167    pub value: DataValue,
4168    pub source: Option<Source>,
4169}
4170
4171/// What a [`DataDefinition::Reference`] copies its value from: either another data path
4172/// or a rule whose result becomes this data's value.
4173#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
4174#[serde(rename_all = "snake_case", tag = "kind")]
4175pub enum ReferenceTarget {
4176    Data(DataPath),
4177    Rule(RulePath),
4178}
4179
4180/// Resolved data value for the execution plan: aligned with [`DataValue`] but with source per variant.
4181#[derive(Clone, Debug, Serialize, Deserialize)]
4182#[serde(rename_all = "snake_case")]
4183pub enum DataDefinition {
4184    /// Value-holding data: current value (literal or default); type is on the value.
4185    Value { value: LiteralValue, source: Source },
4186    /// Type-only data: schema known, value to be supplied (e.g. via with_values).
4187    /// `declared_default` carries the `-> default ...` payload for this binding or
4188    /// the default inherited from the parent type chain, if any; value-promoting code
4189    /// uses it instead of re-deriving defaults from [`TypeSpecification`].
4190    TypeDeclaration {
4191        resolved_type: Arc<LemmaType>,
4192        declared_default: Option<ValueKind>,
4193        source: Source,
4194    },
4195    /// Import (`uses`): resolved target lemma for this alias.
4196    Import {
4197        spec: Arc<crate::parsing::ast::LemmaSpec>,
4198        source: Source,
4199    },
4200    /// Value-copy reference to another data or a rule result.
4201    ///
4202    /// `resolved_type` is the merged type that the copied value must satisfy at
4203    /// evaluation time. Merging folds together: (1) the LHS's own declared type,
4204    /// if any; (2) the target's type (data schema type or rule return type);
4205    /// (3) any `local_constraints` written after the `->` on the reference itself.
4206    /// Merging happens in a dedicated pass once all data and rule types are
4207    /// known; before that pass, `resolved_type` holds a provisional value and
4208    /// must not be consumed for type checking.
4209    ///
4210    /// `local_constraints` preserves the raw constraint list from the reference's
4211    /// `-> ...` tail (e.g. `minimum 5` in `data license2: law.other -> minimum 5`)
4212    /// for that merging pass. It is `None` when the reference has no trailing
4213    /// constraints.
4214    ///
4215    /// `local_default` carries any `default <value>` constraint from the
4216    /// reference's `-> ...` tail. The reference-merge pass extracts it from the
4217    /// constraint list during type resolution. It is materialized into a
4218    /// concrete value by the evaluator when the caller does not supply a value.
4219    ///
4220    /// The reference itself is evaluated by copying the target's value (data path)
4221    /// or the target rule's result in topological order; caller values in
4222    /// [`crate::planning::execution_plan::DataOverlay`] override the reference.
4223    Reference {
4224        target: ReferenceTarget,
4225        resolved_type: Arc<LemmaType>,
4226        local_constraints: Option<Vec<Constraint>>,
4227        local_default: Option<ValueKind>,
4228        source: Source,
4229    },
4230}
4231
4232impl DataDefinition {
4233    /// Schema type for value, type-declaration, and reference data; `None` for imports.
4234    pub fn schema_type(&self) -> Option<&LemmaType> {
4235        match self {
4236            DataDefinition::Value { value, .. } => Some(value.lemma_type.as_ref()),
4237            DataDefinition::TypeDeclaration { resolved_type, .. } => Some(resolved_type.as_ref()),
4238            DataDefinition::Reference { resolved_type, .. } => Some(resolved_type.as_ref()),
4239            DataDefinition::Import { .. } => None,
4240        }
4241    }
4242
4243    /// Returns the literal value when the data already holds one. A `Reference`'s
4244    /// value is produced by the evaluator at runtime, so at plan-time it has no
4245    /// value yet.
4246    pub fn value(&self) -> Option<&LiteralValue> {
4247        match self {
4248            DataDefinition::Value { value, .. } => Some(value),
4249            DataDefinition::TypeDeclaration { .. }
4250            | DataDefinition::Import { .. }
4251            | DataDefinition::Reference { .. } => None,
4252        }
4253    }
4254
4255    /// Spec literal (`data x: literal`) or literal `with` binding at plan time.
4256    /// Not a suggestion; see [`Self::default_suggestion`].
4257    #[inline]
4258    pub fn prefilled_value(&self) -> Option<&LiteralValue> {
4259        self.value()
4260    }
4261
4262    /// Suggestion from `-> default ...` on a type declaration or reference.
4263    /// Surfaces in [`crate::planning::execution_plan::DataEntry::default`] for
4264    /// prefill/UI; the evaluator applies it when the caller does not supply a value.
4265    pub fn default_suggestion(&self) -> Option<LiteralValue> {
4266        match self {
4267            DataDefinition::TypeDeclaration {
4268                resolved_type,
4269                declared_default: Some(dv),
4270                ..
4271            } => Some(LiteralValue {
4272                value: dv.clone(),
4273                lemma_type: Arc::clone(resolved_type),
4274            }),
4275            DataDefinition::Reference {
4276                resolved_type,
4277                local_default: Some(dv),
4278                ..
4279            } => Some(LiteralValue {
4280                value: dv.clone(),
4281                lemma_type: Arc::clone(resolved_type),
4282            }),
4283            DataDefinition::Value { .. }
4284            | DataDefinition::TypeDeclaration {
4285                declared_default: None,
4286                ..
4287            }
4288            | DataDefinition::Reference {
4289                local_default: None,
4290                ..
4291            }
4292            | DataDefinition::Import { .. } => None,
4293        }
4294    }
4295
4296    /// Returns the source location for this data.
4297    pub fn source(&self) -> &Source {
4298        match self {
4299            DataDefinition::Value { source, .. } => source,
4300            DataDefinition::TypeDeclaration { source, .. } => source,
4301            DataDefinition::Import { source, .. } => source,
4302            DataDefinition::Reference { source, .. } => source,
4303        }
4304    }
4305
4306    /// Returns the reference target when this data copies a value from another
4307    /// data path or rule result; `None` otherwise.
4308    pub fn reference_target(&self) -> Option<&ReferenceTarget> {
4309        match self {
4310            DataDefinition::Reference { target, .. } => Some(target),
4311            _ => None,
4312        }
4313    }
4314}
4315
4316/// Bind a type-agnostic [`Value::NumberWithUnit`] using the unit index entry for `unit_name`.
4317pub fn number_with_unit_to_value_kind(
4318    magnitude: rust_decimal::Decimal,
4319    unit_name: &str,
4320    lemma_type: &LemmaType,
4321) -> Result<ValueKind, String> {
4322    match &lemma_type.specifications {
4323        TypeSpecification::Ratio { units, .. } => {
4324            use crate::computation::rational::{checked_div, decimal_to_rational};
4325            let unit = units.get(unit_name)?;
4326            let magnitude_rational = decimal_to_rational(magnitude)
4327                .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
4328            let canonical_rational = checked_div(&magnitude_rational, &unit.value)
4329                .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
4330            Ok(ValueKind::Ratio(
4331                canonical_rational,
4332                Some(unit.name.clone()),
4333            ))
4334        }
4335        TypeSpecification::Measure { units, .. } => {
4336            use crate::computation::rational::checked_mul;
4337            let rational = lift_parser_decimal(magnitude)?;
4338            let unit = units.get(unit_name)?;
4339            let canonical = checked_mul(&rational, &unit.factor)
4340                .map_err(|failure| format!("measure canonicalization overflow: {failure}"))?;
4341            Ok(ValueKind::Measure(
4342                canonical,
4343                vec![(unit_name.to_string(), 1)],
4344            ))
4345        }
4346        _ => Err(format!(
4347            "Unit '{}' is defined on type '{}' which is not measure or ratio",
4348            unit_name,
4349            lemma_type.name()
4350        )),
4351    }
4352}
4353
4354/// Whether a [`ValueKind`] is structurally compatible with a [`TypeSpecification`].
4355/// Bound validation (min/max/decimals) is separate; this only checks shape.
4356pub(crate) fn value_kind_matches_spec(value: &ValueKind, type_spec: &TypeSpecification) -> bool {
4357    matches!(
4358        (type_spec, value),
4359        (TypeSpecification::Number { .. }, ValueKind::Number(_))
4360            | (TypeSpecification::Text { .. }, ValueKind::Text(_))
4361            | (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
4362            | (TypeSpecification::Date { .. }, ValueKind::Date(_))
4363            | (TypeSpecification::Time { .. }, ValueKind::Time(_))
4364            | (TypeSpecification::Measure { .. }, ValueKind::Measure(_, _))
4365            | (TypeSpecification::Ratio { .. }, ValueKind::Ratio(_, _))
4366            | (TypeSpecification::Ratio { .. }, ValueKind::Number(_))
4367            | (
4368                TypeSpecification::NumberRange { .. },
4369                ValueKind::Range(_, _)
4370            )
4371            | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
4372            | (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
4373            | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
4374            | (
4375                TypeSpecification::MeasureRange { .. },
4376                ValueKind::Range(_, _)
4377            )
4378            | (TypeSpecification::Veto { .. }, _)
4379            | (TypeSpecification::Undetermined, _)
4380    )
4381}
4382
4383fn value_kind_tag_for_type(spec: &TypeSpecification) -> &'static str {
4384    match spec {
4385        TypeSpecification::Boolean { .. } => "boolean",
4386        TypeSpecification::Measure { .. } => "measure",
4387        TypeSpecification::Number { .. } => "number",
4388        TypeSpecification::NumberRange { .. }
4389        | TypeSpecification::MeasureRange { .. }
4390        | TypeSpecification::DateRange { .. }
4391        | TypeSpecification::TimeRange { .. }
4392        | TypeSpecification::RatioRange { .. } => "range",
4393        TypeSpecification::Ratio { .. } => "ratio",
4394        TypeSpecification::Text { .. } => "text",
4395        TypeSpecification::Date { .. } => "date",
4396        TypeSpecification::Time { .. } => "time",
4397        TypeSpecification::Veto { .. } => "veto",
4398        TypeSpecification::Undetermined => "undetermined",
4399    }
4400}
4401
4402fn parser_value_type_mismatch(
4403    value: &crate::literals::Value,
4404    type_spec: &TypeSpecification,
4405) -> String {
4406    use crate::parsing::ast::AsLemmaSource;
4407    let value_str = format!("{}", AsLemmaSource(value));
4408    let expected = value_kind_tag_for_type(type_spec);
4409    match type_spec {
4410        TypeSpecification::Measure { units, .. } => {
4411            let unit_hint = units
4412                .iter()
4413                .find(|u| u.factor == crate::computation::rational::rational_one())
4414                .map(|u| u.name.as_str())
4415                .or_else(|| units.iter().next().map(|u| u.name.as_str()))
4416                .unwrap_or("unit");
4417            format!("cannot use {value_str} as {expected}: expected `<n> {unit_hint}`")
4418        }
4419        TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
4420            let unit_hint = units
4421                .iter()
4422                .next()
4423                .map(|u| u.name.as_str())
4424                .unwrap_or("unit");
4425            format!(
4426                "cannot use {value_str} as {expected}: expected `<n> {unit_hint}` or bare ratio"
4427            )
4428        }
4429        _ => format!("cannot use {value_str} as {expected}"),
4430    }
4431}
4432
4433/// Re-canonicalize a measure literal after compound unit factors were resolved.
4434///
4435/// Literals parsed before derived unit resolution were canonicalized with prefix-only
4436/// factors; multiply by `resolved_factor / stored_factor` to align with final factors.
4437pub fn refresh_measure_literal_canonical_magnitude(
4438    lit: &mut LiteralValue,
4439    resolved_type: &LemmaType,
4440) {
4441    let ValueKind::Measure(magnitude, signature) = &mut lit.value else {
4442        return;
4443    };
4444    let (unit_name, exponent) = signature
4445        .first()
4446        .expect("BUG: measure literal has empty signature during canonical magnitude refresh");
4447    if *exponent != 1 || signature.len() != 1 {
4448        return;
4449    }
4450    let stored_factor = lit.lemma_type.measure_unit_factor(unit_name);
4451    let resolved_factor = resolved_type.measure_unit_factor(unit_name);
4452    if stored_factor == resolved_factor {
4453        lit.lemma_type = Arc::new(resolved_type.clone());
4454        return;
4455    }
4456    let scaled = checked_mul(magnitude, resolved_factor)
4457        .expect("BUG: measure recanonicalization multiply overflow");
4458    *magnitude =
4459        checked_div(&scaled, stored_factor).expect("BUG: measure recanonicalization divide failed");
4460    lit.lemma_type = Arc::new(resolved_type.clone());
4461}
4462
4463/// Convert parser [`Value`] to [`ValueKind`] using the target type (canonicalizes ratio at bind).
4464pub fn parser_value_to_value_kind(
4465    value: &crate::literals::Value,
4466    type_spec: &TypeSpecification,
4467) -> Result<ValueKind, String> {
4468    use crate::computation::rational::decimal_to_rational;
4469    use crate::literals::Value;
4470    match (value, type_spec) {
4471        (Value::NumberWithUnit(magnitude, unit_name), TypeSpecification::Ratio { units, .. }) => {
4472            use crate::computation::rational::checked_div;
4473            let unit = units.get(unit_name.as_str())?;
4474            let magnitude_rational = decimal_to_rational(*magnitude)
4475                .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
4476            let canonical_rational = checked_div(&magnitude_rational, &unit.value)
4477                .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
4478            Ok(ValueKind::Ratio(
4479                canonical_rational,
4480                Some(unit.name.clone()),
4481            ))
4482        }
4483        (Value::NumberWithUnit(magnitude, unit_name), TypeSpecification::Measure { units, .. }) => {
4484            use crate::computation::rational::checked_mul;
4485            let rational = lift_parser_decimal(*magnitude)?;
4486            let unit = units.get(unit_name.as_str())?;
4487            let canonical = checked_mul(&rational, &unit.factor)
4488                .map_err(|failure| format!("measure canonicalization overflow: {failure}"))?;
4489            Ok(ValueKind::Measure(canonical, vec![(unit_name.clone(), 1)]))
4490        }
4491        (Value::NumberWithUnit(_, _), _) => {
4492            Err("number_with_unit literal requires a measure or ratio type".to_string())
4493        }
4494        (Value::Number(n), TypeSpecification::Number { .. }) => {
4495            Ok(ValueKind::Number(lift_parser_decimal(*n)?))
4496        }
4497        (Value::Number(n), TypeSpecification::Ratio { .. }) => {
4498            let r = decimal_to_rational(*n)
4499                .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
4500            Ok(ValueKind::Ratio(r, None))
4501        }
4502        (Value::Text(s), TypeSpecification::Text { .. }) => Ok(ValueKind::Text(s.clone())),
4503        (Value::Boolean(b), TypeSpecification::Boolean { .. }) => Ok(ValueKind::Boolean(b.into())),
4504        (Value::Date(dt), TypeSpecification::Date { .. }) => {
4505            Ok(ValueKind::Date(date_time_to_semantic(dt)))
4506        }
4507        (Value::Time(t), TypeSpecification::Time { .. }) => {
4508            Ok(ValueKind::Time(time_to_semantic(t)))
4509        }
4510        (
4511            Value::Range(left, right),
4512            range_spec @ (TypeSpecification::NumberRange { .. }
4513            | TypeSpecification::DateRange { .. }
4514            | TypeSpecification::TimeRange { .. }
4515            | TypeSpecification::RatioRange { .. }
4516            | TypeSpecification::MeasureRange { .. }),
4517        ) => {
4518            let endpoint = range_element_type_specification(range_spec).ok_or_else(|| {
4519                "BUG: range_element_type_specification missing arm for range type".to_string()
4520            })?;
4521            let left_lit = lift_range_endpoint(left, &endpoint)?;
4522            let right_lit = lift_range_endpoint(right, &endpoint)?;
4523            Ok(ValueKind::Range(Box::new(left_lit), Box::new(right_lit)))
4524        }
4525        (value, type_spec) => Err(parser_value_type_mismatch(value, type_spec)),
4526    }
4527}
4528
4529/// Convert parser Value to ValueKind for primitives and ranges only.
4530///
4531/// [`Value::NumberWithUnit`] requires [`parser_value_to_value_kind`] with a measure or ratio type.
4532pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
4533    use crate::parsing::ast::Value;
4534    Ok(match value {
4535        Value::Number(n) => ValueKind::Number(lift_parser_decimal(*n)?),
4536        Value::Text(s) => ValueKind::Text(s.clone()),
4537        Value::Boolean(b) => ValueKind::Boolean(bool::from(*b)),
4538        Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
4539        Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
4540        Value::NumberWithUnit(_, _) => {
4541            return Err(
4542                "number_with_unit literal requires type context (measure or ratio)".to_string(),
4543            );
4544        }
4545        Value::Range(_, _) => literal_value_from_parser_value(value)?.value,
4546    })
4547}
4548
4549/// Convert AST date-time to semantic (for tests and planning).
4550pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
4551    SemanticDateTime {
4552        year: dt.year,
4553        month: dt.month,
4554        day: dt.day,
4555        hour: dt.hour,
4556        minute: dt.minute,
4557        second: dt.second,
4558        microsecond: dt.microsecond,
4559        timezone: dt.timezone.as_ref().map(|tz| SemanticTimezone {
4560            offset_hours: tz.offset_hours,
4561            offset_minutes: tz.offset_minutes,
4562        }),
4563    }
4564}
4565
4566/// Convert AST time to semantic (for tests and planning).
4567pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
4568    SemanticTime {
4569        hour: t.hour.into(),
4570        minute: t.minute.into(),
4571        second: t.second.into(),
4572        microsecond: t.microsecond,
4573        timezone: t.timezone.as_ref().map(|tz| SemanticTimezone {
4574            offset_hours: tz.offset_hours,
4575            offset_minutes: tz.offset_minutes,
4576        }),
4577    }
4578}
4579
4580/// Compare two semantic date-time values by year, month, day, hour, minute,
4581/// second, then microsecond. Timezone normalisation is a separate concern
4582/// handled at evaluation time.
4583pub(crate) fn compare_semantic_dates(
4584    left: &SemanticDateTime,
4585    right: &SemanticDateTime,
4586) -> std::cmp::Ordering {
4587    left.year
4588        .cmp(&right.year)
4589        .then_with(|| left.month.cmp(&right.month))
4590        .then_with(|| left.day.cmp(&right.day))
4591        .then_with(|| left.hour.cmp(&right.hour))
4592        .then_with(|| left.minute.cmp(&right.minute))
4593        .then_with(|| left.second.cmp(&right.second))
4594        .then_with(|| left.microsecond.cmp(&right.microsecond))
4595}
4596
4597/// Compare two semantic time values by hour, minute, second, then microsecond.
4598/// Timezone is excluded for the same reason as [`compare_semantic_dates`].
4599pub(crate) fn compare_semantic_times(
4600    left: &SemanticTime,
4601    right: &SemanticTime,
4602) -> std::cmp::Ordering {
4603    left.hour
4604        .cmp(&right.hour)
4605        .then_with(|| left.minute.cmp(&right.minute))
4606        .then_with(|| left.second.cmp(&right.second))
4607        .then_with(|| left.microsecond.cmp(&right.microsecond))
4608}
4609
4610/// Convert AST conversion target to semantic (planning boundary; evaluation/computation use only semantic).
4611pub fn conversion_target_to_semantic(
4612    ct: &ConversionTarget,
4613    unit_index: Option<&HashMap<String, Arc<LemmaType>>>,
4614) -> Result<SemanticConversionTarget, String> {
4615    match ct {
4616        ConversionTarget::Type(kind) => Ok(SemanticConversionTarget::Type(*kind)),
4617        ConversionTarget::Unit { unit_name } => {
4618            let unit_name = crate::parsing::ast::ascii_lowercase_logical_name(unit_name.clone());
4619            let index = unit_index.ok_or_else(|| format!("Unknown unit '{unit_name}'."))?;
4620            let owning_type = index
4621                .get(&unit_name)
4622                .ok_or_else(|| format!("Unknown unit '{unit_name}'."))?
4623                .clone();
4624            Ok(SemanticConversionTarget::Unit {
4625                unit_name,
4626                owning_type,
4627            })
4628        }
4629    }
4630}
4631
4632// -----------------------------------------------------------------------------
4633// Primitive type constructors (moved from parsing::ast)
4634// -----------------------------------------------------------------------------
4635
4636// Statics for lazy initialization of production-used primitive types.
4637static PRIMITIVE_BOOLEAN: OnceLock<Arc<LemmaType>> = OnceLock::new();
4638static PRIMITIVE_NUMBER: OnceLock<Arc<LemmaType>> = OnceLock::new();
4639static PRIMITIVE_TEXT: OnceLock<Arc<LemmaType>> = OnceLock::new();
4640static PRIMITIVE_DATE: OnceLock<Arc<LemmaType>> = OnceLock::new();
4641static PRIMITIVE_DATE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
4642static PRIMITIVE_TIME: OnceLock<Arc<LemmaType>> = OnceLock::new();
4643static PRIMITIVE_RATIO: OnceLock<Arc<LemmaType>> = OnceLock::new();
4644
4645#[must_use]
4646pub fn primitive_boolean_arc() -> &'static Arc<LemmaType> {
4647    PRIMITIVE_BOOLEAN.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::boolean())))
4648}
4649
4650#[must_use]
4651pub fn primitive_number_arc() -> &'static Arc<LemmaType> {
4652    PRIMITIVE_NUMBER.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::number())))
4653}
4654
4655#[must_use]
4656pub fn primitive_text_arc() -> &'static Arc<LemmaType> {
4657    PRIMITIVE_TEXT.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::text())))
4658}
4659
4660#[must_use]
4661pub fn primitive_date_arc() -> &'static Arc<LemmaType> {
4662    PRIMITIVE_DATE.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::date())))
4663}
4664
4665#[must_use]
4666pub fn primitive_date_range_arc() -> &'static Arc<LemmaType> {
4667    PRIMITIVE_DATE_RANGE
4668        .get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::date_range())))
4669}
4670
4671#[must_use]
4672pub fn primitive_time_arc() -> &'static Arc<LemmaType> {
4673    PRIMITIVE_TIME.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::time())))
4674}
4675
4676#[must_use]
4677pub fn primitive_ratio_arc() -> &'static Arc<LemmaType> {
4678    PRIMITIVE_RATIO.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::ratio())))
4679}
4680
4681// Test-only non-Arc wrappers used exclusively in unit tests.
4682#[cfg(test)]
4683static PRIMITIVE_MEASURE: OnceLock<Arc<LemmaType>> = OnceLock::new();
4684
4685#[cfg(test)]
4686#[must_use]
4687pub fn primitive_boolean() -> &'static LemmaType {
4688    primitive_boolean_arc().as_ref()
4689}
4690
4691#[cfg(test)]
4692#[must_use]
4693pub fn primitive_measure() -> &'static LemmaType {
4694    primitive_measure_arc().as_ref()
4695}
4696
4697#[cfg(test)]
4698#[must_use]
4699pub fn primitive_measure_arc() -> &'static Arc<LemmaType> {
4700    PRIMITIVE_MEASURE.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::measure())))
4701}
4702
4703#[cfg(test)]
4704#[must_use]
4705pub fn primitive_number() -> &'static LemmaType {
4706    primitive_number_arc().as_ref()
4707}
4708
4709#[cfg(test)]
4710#[must_use]
4711pub fn primitive_text() -> &'static LemmaType {
4712    primitive_text_arc().as_ref()
4713}
4714
4715#[cfg(test)]
4716#[must_use]
4717pub fn primitive_date() -> &'static LemmaType {
4718    primitive_date_arc().as_ref()
4719}
4720
4721#[cfg(test)]
4722#[must_use]
4723pub fn primitive_time() -> &'static LemmaType {
4724    primitive_time_arc().as_ref()
4725}
4726
4727#[cfg(test)]
4728#[must_use]
4729pub fn primitive_ratio() -> &'static LemmaType {
4730    primitive_ratio_arc().as_ref()
4731}
4732
4733/// Map PrimitiveKind to TypeSpecification. Single source of truth for primitive type resolution.
4734#[must_use]
4735pub fn type_spec_for_primitive(kind: PrimitiveKind) -> TypeSpecification {
4736    match kind {
4737        PrimitiveKind::Boolean => TypeSpecification::boolean(),
4738        PrimitiveKind::Measure => TypeSpecification::measure(),
4739        PrimitiveKind::MeasureRange => TypeSpecification::measure_range(),
4740        PrimitiveKind::Number => TypeSpecification::number(),
4741        PrimitiveKind::NumberRange => TypeSpecification::number_range(),
4742        PrimitiveKind::Ratio => TypeSpecification::ratio(),
4743        PrimitiveKind::RatioRange => TypeSpecification::ratio_range(),
4744        PrimitiveKind::Text => TypeSpecification::text(),
4745        PrimitiveKind::Date => TypeSpecification::date(),
4746        PrimitiveKind::DateRange => TypeSpecification::date_range(),
4747        PrimitiveKind::Time => TypeSpecification::time(),
4748        PrimitiveKind::TimeRange => TypeSpecification::time_range(),
4749    }
4750}
4751
4752// -----------------------------------------------------------------------------
4753// Display implementations
4754// -----------------------------------------------------------------------------
4755
4756impl fmt::Display for PathSegment {
4757    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4758        write!(f, "{} → {}", self.data, self.spec)
4759    }
4760}
4761
4762impl fmt::Display for DataPath {
4763    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4764        for segment in &self.segments {
4765            write!(f, "{}.", segment)?;
4766        }
4767        write!(f, "{}", self.data)
4768    }
4769}
4770
4771impl fmt::Display for RulePath {
4772    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4773        for segment in &self.segments {
4774            write!(f, "{}.", segment)?;
4775        }
4776        write!(f, "{}", self.rule)
4777    }
4778}
4779
4780impl fmt::Display for LemmaType {
4781    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4782        write!(f, "{}", self.name())
4783    }
4784}
4785
4786fn decimal_places_in_display_value(decimal: &rust_decimal::Decimal) -> u32 {
4787    if decimal.is_integer() {
4788        return 0;
4789    }
4790    decimal.fract().normalize().scale()
4791}
4792
4793fn format_committed_decimal_for_api(
4794    decimal: rust_decimal::Decimal,
4795    decimal_places: Option<u8>,
4796) -> String {
4797    match decimal_places {
4798        Some(decimal_places) => {
4799            let rounded = decimal.round_dp(u32::from(decimal_places));
4800            format!("{:.prec$}", rounded, prec = decimal_places as usize)
4801        }
4802        None => {
4803            let normalized = decimal.normalize();
4804            if normalized.fract().is_zero() {
4805                normalized.trunc().to_string()
4806            } else {
4807                normalized.to_string()
4808            }
4809        }
4810    }
4811}
4812
4813fn format_committed_decimal_for_human_display(
4814    decimal: rust_decimal::Decimal,
4815    decimal_places: Option<u8>,
4816) -> String {
4817    match decimal_places {
4818        Some(decimal_places) => {
4819            let rounded = decimal.round_dp(u32::from(decimal_places));
4820            format!("{:.prec$}", rounded, prec = decimal_places as usize)
4821        }
4822        None => decimal.normalize().to_string(),
4823    }
4824}
4825
4826fn format_rational_for_human_display(
4827    magnitude: &crate::computation::rational::RationalInteger,
4828    decimal_places: Option<u8>,
4829) -> String {
4830    use crate::computation::rational::{commit_rational_to_decimal, rational_to_display_str};
4831    match commit_rational_to_decimal(magnitude) {
4832        Ok(decimal) => format_committed_decimal_for_human_display(decimal, decimal_places),
4833        Err(_) => rational_to_display_str(magnitude),
4834    }
4835}
4836
4837fn format_measure_canonical_for_display(
4838    canonical: &crate::computation::rational::RationalInteger,
4839    lemma_type: &LemmaType,
4840    signature: &[(String, i32)],
4841) -> String {
4842    use crate::computation::rational::{checked_div, commit_rational_to_decimal};
4843    use rust_decimal::Decimal;
4844
4845    let decimals = lemma_type.decimal_places();
4846
4847    if let TypeSpecification::Measure { units, .. } = &lemma_type.specifications {
4848        if !units.is_empty() {
4849            if let [(sig_unit, 1)] = signature {
4850                if let Some(unit) = units.iter().find(|u| u.name == *sig_unit) {
4851                    let in_unit = checked_div(canonical, &unit.factor)
4852                        .expect("BUG: de-canonicalization for measure display must not fail");
4853                    let formatted = format_rational_for_human_display(&in_unit, decimals);
4854                    return format!("{} {}", formatted, unit.name);
4855                }
4856            }
4857
4858            struct UnitDisplayCandidate {
4859                unit_name: String,
4860                decimal_places: u32,
4861                under_1000: bool,
4862                abs_magnitude: Decimal,
4863                formatted: String,
4864            }
4865
4866            let mut candidates: Vec<UnitDisplayCandidate> = Vec::with_capacity(units.len());
4867            for unit in units.iter() {
4868                let in_unit = checked_div(canonical, &unit.factor)
4869                    .expect("BUG: de-canonicalization for measure display must not fail");
4870                let formatted = format_rational_for_human_display(&in_unit, decimals);
4871                let abs_magnitude = match commit_rational_to_decimal(&in_unit) {
4872                    Ok(decimal) => decimal.abs(),
4873                    Err(_) => Decimal::MAX,
4874                };
4875                let decimal_places = match commit_rational_to_decimal(&in_unit) {
4876                    Ok(decimal) => decimal_places_in_display_value(&decimal),
4877                    Err(_) => u32::MAX,
4878                };
4879                let under_1000 = abs_magnitude < Decimal::from(1000);
4880                candidates.push(UnitDisplayCandidate {
4881                    unit_name: unit.name.clone(),
4882                    decimal_places,
4883                    under_1000,
4884                    abs_magnitude,
4885                    formatted,
4886                });
4887            }
4888
4889            let pool: Vec<&UnitDisplayCandidate> = {
4890                let under: Vec<_> = candidates.iter().filter(|c| c.under_1000).collect();
4891                if under.is_empty() {
4892                    candidates.iter().collect()
4893                } else {
4894                    under
4895                }
4896            };
4897            let best = pool
4898                .iter()
4899                .min_by(|left, right| {
4900                    left.decimal_places
4901                        .cmp(&right.decimal_places)
4902                        .then_with(|| left.abs_magnitude.cmp(&right.abs_magnitude))
4903                })
4904                .expect("BUG: measure type must have at least one declared unit");
4905            return format!("{} {}", best.formatted, best.unit_name);
4906        }
4907    }
4908
4909    let unit_label = match signature {
4910        [] => String::new(),
4911        [(name, 1)] => name.clone(),
4912        _ => format_signature_operator_style(signature),
4913    };
4914    let formatted = format_rational_for_human_display(canonical, decimals);
4915    if unit_label.is_empty() {
4916        formatted
4917    } else {
4918        format!("{formatted} {unit_label}")
4919    }
4920}
4921
4922impl fmt::Display for LiteralValue {
4923    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4924        match &self.value {
4925            ValueKind::Measure(n, signature) => {
4926                write!(
4927                    f,
4928                    "{}",
4929                    format_measure_canonical_for_display(n, &self.lemma_type, signature)
4930                )
4931            }
4932            ValueKind::Ratio(_, Some(_unit_name)) => write!(f, "{}", self.value),
4933            ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
4934            _ => write!(f, "{}", self.value),
4935        }
4936    }
4937}
4938
4939// -----------------------------------------------------------------------------
4940// Tests
4941// -----------------------------------------------------------------------------
4942
4943#[cfg(test)]
4944mod tests {
4945    use super::*;
4946    use crate::computation::rational::decimal_to_rational;
4947    use crate::literals::DateGranularity;
4948    use crate::literals::Value;
4949    use crate::parsing::ast::{BooleanValue, DateTimeValue, LemmaSpec, PrimitiveKind, TimeValue};
4950    use rust_decimal::Decimal;
4951    use std::str::FromStr;
4952    use std::sync::Arc;
4953
4954    #[test]
4955    fn default_primitive_help_is_goal_oriented() {
4956        let kinds = [
4957            PrimitiveKind::Boolean,
4958            PrimitiveKind::Measure,
4959            PrimitiveKind::MeasureRange,
4960            PrimitiveKind::Number,
4961            PrimitiveKind::NumberRange,
4962            PrimitiveKind::Ratio,
4963            PrimitiveKind::RatioRange,
4964            PrimitiveKind::Text,
4965            PrimitiveKind::Date,
4966            PrimitiveKind::DateRange,
4967            PrimitiveKind::Time,
4968            PrimitiveKind::TimeRange,
4969        ];
4970        for kind in kinds {
4971            let spec = type_spec_for_primitive(kind);
4972            let help = match &spec {
4973                TypeSpecification::Boolean { help, .. }
4974                | TypeSpecification::Number { help, .. }
4975                | TypeSpecification::NumberRange { help }
4976                | TypeSpecification::Text { help, .. }
4977                | TypeSpecification::Measure { help, .. }
4978                | TypeSpecification::MeasureRange { help, .. }
4979                | TypeSpecification::Ratio { help, .. }
4980                | TypeSpecification::RatioRange { help, .. }
4981                | TypeSpecification::Date { help, .. }
4982                | TypeSpecification::DateRange { help }
4983                | TypeSpecification::TimeRange { help }
4984                | TypeSpecification::Time { help, .. } => help,
4985                TypeSpecification::Veto { .. } | TypeSpecification::Undetermined => {
4986                    unreachable!(
4987                        "BUG: primitive kind {:?} mapped to non-primitive spec",
4988                        kind
4989                    )
4990                }
4991            };
4992            assert!(!help.is_empty(), "help for {:?}", kind);
4993            assert!(
4994                !help.to_ascii_lowercase().contains("format:"),
4995                "help for {:?} must not describe syntax: {:?}",
4996                kind,
4997                help
4998            );
4999            assert_eq!(help, default_help_for_primitive(kind));
5000        }
5001    }
5002
5003    #[test]
5004    fn test_negated_comparison() {
5005        assert_eq!(
5006            negated_comparison(ComparisonComputation::LessThan),
5007            ComparisonComputation::GreaterThanOrEqual
5008        );
5009        assert_eq!(
5010            negated_comparison(ComparisonComputation::GreaterThanOrEqual),
5011            ComparisonComputation::LessThan
5012        );
5013        assert_eq!(
5014            negated_comparison(ComparisonComputation::Is),
5015            ComparisonComputation::IsNot
5016        );
5017        assert_eq!(
5018            negated_comparison(ComparisonComputation::IsNot),
5019            ComparisonComputation::Is
5020        );
5021    }
5022
5023    #[test]
5024    fn value_to_semantic_number_is_decimal() {
5025        let kind = value_to_semantic(&Value::Number(Decimal::from(42))).unwrap();
5026        assert!(matches!(kind, ValueKind::Number(d) if d == rational_new(42, 1)));
5027    }
5028
5029    #[test]
5030    fn value_kind_measure_serializes_with_signature() {
5031        let kind = ValueKind::Measure(
5032            decimal_to_rational(Decimal::from_str("99.50").unwrap()).unwrap(),
5033            vec![("eur".to_string(), 1)],
5034        );
5035        let json = serde_json::to_value(&kind).unwrap();
5036        assert_eq!(json["measure"]["value"], "99.5");
5037        assert_eq!(json["measure"]["signature"][0][0], "eur");
5038        assert_eq!(json["measure"]["signature"][0][1], 1);
5039    }
5040
5041    #[test]
5042    fn value_kind_measure_compound_signature_roundtrips() {
5043        let original = ValueKind::Measure(
5044            decimal_to_rational(Decimal::from_str("4800").unwrap()).unwrap(),
5045            vec![
5046                ("eur".to_string(), 1),
5047                ("hour".to_string(), 1),
5048                ("minute".to_string(), -1),
5049            ],
5050        );
5051        let json = serde_json::to_string(&original).unwrap();
5052        let parsed: ValueKind = serde_json::from_str(&json).unwrap();
5053        assert_eq!(original, parsed);
5054    }
5055
5056    #[test]
5057    fn value_kind_measure_empty_signature_roundtrips() {
5058        let original = ValueKind::Measure(
5059            decimal_to_rational(Decimal::from_str("12.5").unwrap()).unwrap(),
5060            Vec::new(),
5061        );
5062        let json = serde_json::to_string(&original).unwrap();
5063        let parsed: ValueKind = serde_json::from_str(&json).unwrap();
5064        assert_eq!(original, parsed);
5065    }
5066
5067    #[test]
5068    fn literal_value_number_serde_not_rational_array() {
5069        let lit = LiteralValue::number_from_decimal(Decimal::from(20));
5070        let json = serde_json::to_value(&lit).unwrap();
5071        let number = json
5072            .get("value")
5073            .and_then(|v| v.get("number"))
5074            .expect("number field");
5075        assert!(number.is_string());
5076        assert_eq!(number.as_str(), Some("20"));
5077        assert!(
5078            !number.is_array(),
5079            "stored number must not serialize as [n,d]"
5080        );
5081    }
5082
5083    #[test]
5084    fn test_literal_value_to_primitive_type() {
5085        let one = rational_new(1, 1);
5086
5087        assert_eq!(LiteralValue::text("".to_string()).lemma_type.name(), "text");
5088        assert_eq!(
5089            LiteralValue::number(one.clone()).lemma_type.name(),
5090            "number"
5091        );
5092        assert_eq!(
5093            LiteralValue::from_bool(bool::from(BooleanValue::True))
5094                .lemma_type
5095                .name(),
5096            "boolean"
5097        );
5098
5099        let dt = DateTimeValue {
5100            year: 2024,
5101            month: 1,
5102            day: 1,
5103            hour: 0,
5104            minute: 0,
5105            second: 0,
5106            microsecond: 0,
5107            timezone: None,
5108
5109            granularity: DateGranularity::Full,
5110        };
5111        assert_eq!(
5112            LiteralValue::date(date_time_to_semantic(&dt))
5113                .lemma_type
5114                .name(),
5115            "date"
5116        );
5117        assert_eq!(
5118            LiteralValue::ratio_from_decimal(Decimal::new(1, 2), Some("percent".to_string()))
5119                .lemma_type
5120                .name(),
5121            "ratio"
5122        );
5123        let dur_type = LemmaType::new(
5124            "duration".to_string(),
5125            TypeSpecification::Measure {
5126                minimum: None,
5127                maximum: None,
5128                decimals: None,
5129                units: MeasureUnits::from(vec![MeasureUnit {
5130                    name: "second".to_string(),
5131                    factor: crate::computation::rational::rational_one(),
5132                    derived_measure_factors: Vec::new(),
5133                    decomposition: BaseMeasureVector::new(),
5134                    minimum: None,
5135                    maximum: None,
5136                    default_magnitude: None,
5137                }]),
5138                traits: vec![MeasureTrait::Duration],
5139                decomposition: None,
5140                help: String::new(),
5141            },
5142            TypeExtends::Primitive,
5143        );
5144        assert_eq!(
5145            LiteralValue::measure_with_type(one.clone(), "second".to_string(), Arc::new(dur_type))
5146                .lemma_type
5147                .name(),
5148            "duration"
5149        );
5150    }
5151
5152    #[test]
5153    fn test_type_display() {
5154        let specs = TypeSpecification::text();
5155        let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
5156        assert_eq!(format!("{}", lemma_type), "name");
5157    }
5158
5159    #[test]
5160    fn test_type_serialization() {
5161        let specs = TypeSpecification::number();
5162        let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
5163        let serialized = serde_json::to_string(&lemma_type).unwrap();
5164        let deserialized: LemmaType = serde_json::from_str(&serialized).unwrap();
5165        assert_eq!(lemma_type, deserialized);
5166    }
5167
5168    #[test]
5169    fn test_literal_value_display_value() {
5170        let ten = rational_new(10, 1);
5171
5172        assert_eq!(
5173            LiteralValue::text("hello".to_string()).display_value(),
5174            "hello"
5175        );
5176        assert_eq!(LiteralValue::number(ten).display_value(), "10");
5177        assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
5178        assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
5179
5180        // 0.10 ratio with "percent" unit displays as 10% (unit conversion applied)
5181        let ten_percent_ratio =
5182            LiteralValue::ratio_from_decimal(Decimal::new(1, 1), Some("percent".to_string()));
5183        assert_eq!(ten_percent_ratio.display_value(), "10%");
5184
5185        let time = TimeValue {
5186            hour: 14,
5187            minute: 30,
5188            second: 0,
5189            microsecond: 0,
5190            timezone: None,
5191        };
5192        let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
5193        assert!(time_display.contains("14"));
5194        assert!(time_display.contains("30"));
5195    }
5196
5197    #[test]
5198    fn test_measure_display_respects_type_decimals() {
5199        let money_type = LemmaType {
5200            name: Some("money".to_string()),
5201            specifications: TypeSpecification::Measure {
5202                minimum: None,
5203                maximum: None,
5204                decimals: Some(2),
5205                units: MeasureUnits::from(vec![MeasureUnit {
5206                    name: "eur".to_string(),
5207                    factor: crate::computation::rational::rational_one(),
5208                    derived_measure_factors: Vec::new(),
5209                    decomposition: BaseMeasureVector::new(),
5210                    minimum: None,
5211                    maximum: None,
5212                    default_magnitude: None,
5213                }]),
5214                traits: Vec::new(),
5215                decomposition: None,
5216                help: String::new(),
5217            },
5218            extends: TypeExtends::Primitive,
5219        };
5220        let money_type = Arc::new(money_type);
5221        let val = LiteralValue::measure_with_type(
5222            decimal_to_rational(Decimal::from_str("1.8").unwrap()).unwrap(),
5223            "eur".to_string(),
5224            money_type.clone(),
5225        );
5226        assert_eq!(val.display_value(), "1.80 eur");
5227        let more_precision = LiteralValue::measure_with_type(
5228            decimal_to_rational(Decimal::from_str("1.80000").unwrap()).unwrap(),
5229            "eur".to_string(),
5230            money_type,
5231        );
5232        assert_eq!(more_precision.display_value(), "1.80 eur");
5233        let measure_no_decimals = LemmaType {
5234            name: Some("count".to_string()),
5235            specifications: TypeSpecification::Measure {
5236                minimum: None,
5237                maximum: None,
5238                decimals: None,
5239                units: MeasureUnits::from(vec![MeasureUnit {
5240                    name: "items".to_string(),
5241                    factor: crate::computation::rational::rational_one(),
5242                    derived_measure_factors: Vec::new(),
5243                    decomposition: BaseMeasureVector::new(),
5244                    minimum: None,
5245                    maximum: None,
5246                    default_magnitude: None,
5247                }]),
5248                traits: Vec::new(),
5249                decomposition: None,
5250                help: String::new(),
5251            },
5252            extends: TypeExtends::Primitive,
5253        };
5254        let val_any = LiteralValue::measure_with_type(
5255            decimal_to_rational(Decimal::from_str("42.50").unwrap()).unwrap(),
5256            "items".to_string(),
5257            Arc::new(measure_no_decimals),
5258        );
5259        assert_eq!(val_any.display_value(), "42.5 items");
5260    }
5261
5262    #[test]
5263    fn test_literal_value_time_type() {
5264        let time = TimeValue {
5265            hour: 14,
5266            minute: 30,
5267            second: 0,
5268            microsecond: 0,
5269            timezone: None,
5270        };
5271        let lit = LiteralValue::time(time_to_semantic(&time));
5272        assert_eq!(lit.lemma_type.name(), "time");
5273    }
5274
5275    #[test]
5276    fn test_measure_family_name_primitive_root() {
5277        let measure_spec = TypeSpecification::measure();
5278        let money_primitive = LemmaType::new(
5279            "money".to_string(),
5280            measure_spec.clone(),
5281            TypeExtends::Primitive,
5282        );
5283        assert_eq!(money_primitive.measure_family_name(), Some("money"));
5284    }
5285
5286    #[test]
5287    fn test_measure_family_name_custom() {
5288        let measure_spec = TypeSpecification::measure();
5289        let money_custom = LemmaType::new(
5290            "money".to_string(),
5291            measure_spec,
5292            TypeExtends::custom_local("money".to_string(), "money".to_string()),
5293        );
5294        assert_eq!(money_custom.measure_family_name(), Some("money"));
5295    }
5296
5297    #[test]
5298    fn test_same_measure_family_same_name_different_extends() {
5299        let measure_spec = TypeSpecification::measure();
5300        let money_primitive = LemmaType::new(
5301            "money".to_string(),
5302            measure_spec.clone(),
5303            TypeExtends::Primitive,
5304        );
5305        let money_custom = LemmaType::new(
5306            "money".to_string(),
5307            measure_spec,
5308            TypeExtends::custom_local("money".to_string(), "money".to_string()),
5309        );
5310        assert!(money_primitive.same_measure_family(&money_custom));
5311        assert!(money_custom.same_measure_family(&money_primitive));
5312    }
5313
5314    #[test]
5315    fn test_same_measure_family_parent_and_child() {
5316        let measure_spec = TypeSpecification::measure();
5317        let type_x = LemmaType::new(
5318            "x".to_string(),
5319            measure_spec.clone(),
5320            TypeExtends::Primitive,
5321        );
5322        let type_x2 = LemmaType::new(
5323            "x2".to_string(),
5324            measure_spec,
5325            TypeExtends::custom_local("x".to_string(), "x".to_string()),
5326        );
5327        assert_eq!(type_x.measure_family_name(), Some("x"));
5328        assert_eq!(type_x2.measure_family_name(), Some("x"));
5329        assert!(type_x.same_measure_family(&type_x2));
5330        assert!(type_x2.same_measure_family(&type_x));
5331    }
5332
5333    #[test]
5334    fn test_same_measure_family_siblings() {
5335        let measure_spec = TypeSpecification::measure();
5336        let type_x2_a = LemmaType::new(
5337            "x2a".to_string(),
5338            measure_spec.clone(),
5339            TypeExtends::custom_local("x".to_string(), "x".to_string()),
5340        );
5341        let type_x2_b = LemmaType::new(
5342            "x2b".to_string(),
5343            measure_spec,
5344            TypeExtends::custom_local("x".to_string(), "x".to_string()),
5345        );
5346        assert!(type_x2_a.same_measure_family(&type_x2_b));
5347    }
5348
5349    #[test]
5350    fn test_same_measure_family_different_families() {
5351        let measure_spec = TypeSpecification::measure();
5352        let money = LemmaType::new(
5353            "money".to_string(),
5354            measure_spec.clone(),
5355            TypeExtends::Primitive,
5356        );
5357        let temperature = LemmaType::new(
5358            "temperature".to_string(),
5359            measure_spec,
5360            TypeExtends::Primitive,
5361        );
5362        assert!(!money.same_measure_family(&temperature));
5363        assert!(!temperature.same_measure_family(&money));
5364    }
5365
5366    #[test]
5367    fn test_same_measure_family_measure_vs_non_measure() {
5368        let measure_spec = TypeSpecification::measure();
5369        let number_spec = TypeSpecification::number();
5370        let measure_type =
5371            LemmaType::new("money".to_string(), measure_spec, TypeExtends::Primitive);
5372        let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
5373        assert!(!measure_type.same_measure_family(&number_type));
5374        assert!(!number_type.same_measure_family(&measure_type));
5375    }
5376
5377    #[test]
5378    fn test_same_measure_family_anonymous_measures_are_not_family_compatible() {
5379        let left = LemmaType::anonymous_for_decomposition(duration_decomposition());
5380        let right = LemmaType::anonymous_for_decomposition(duration_decomposition());
5381
5382        assert!(!left.same_measure_family(&right));
5383        assert!(left.compatible_with_anonymous_measure(&right));
5384    }
5385
5386    #[test]
5387    fn test_measure_family_name_non_measure_returns_none() {
5388        let number_spec = TypeSpecification::number();
5389        let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
5390        assert_eq!(number_type.measure_family_name(), None);
5391    }
5392
5393    #[test]
5394    fn test_lemma_type_inequality_local_vs_import_same_shape() {
5395        let dep = Arc::new(LemmaSpec::new("dep".to_string()));
5396        let measure_spec = TypeSpecification::measure();
5397        let local = LemmaType::new(
5398            "t".to_string(),
5399            measure_spec.clone(),
5400            TypeExtends::custom_local("money".to_string(), "money".to_string()),
5401        );
5402        let imported = LemmaType::new(
5403            "t".to_string(),
5404            measure_spec,
5405            TypeExtends::Custom {
5406                parent: "money".to_string(),
5407                family: "money".to_string(),
5408                defining_spec: TypeDefiningSpec::Import {
5409                    spec: Arc::clone(&dep),
5410                },
5411            },
5412        );
5413        assert_ne!(local, imported);
5414    }
5415
5416    #[test]
5417    fn test_lemma_type_equality_import_same_arc_pointer_identity() {
5418        // TypeDefiningSpec equality is by Arc pointer identity (Arc::ptr_eq).
5419        // Two types are equal iff they hold the same interned Arc, matching
5420        // the Context::insert_spec invariant.
5421        let shared_spec = Arc::new(LemmaSpec::new("dep".to_string()));
5422        let measure_spec = TypeSpecification::measure();
5423        let left = LemmaType::new(
5424            "t".to_string(),
5425            measure_spec.clone(),
5426            TypeExtends::Custom {
5427                parent: "money".to_string(),
5428                family: "money".to_string(),
5429                defining_spec: TypeDefiningSpec::Import {
5430                    spec: Arc::clone(&shared_spec),
5431                },
5432            },
5433        );
5434        let right = LemmaType::new(
5435            "t".to_string(),
5436            measure_spec,
5437            TypeExtends::Custom {
5438                parent: "money".to_string(),
5439                family: "money".to_string(),
5440                defining_spec: TypeDefiningSpec::Import {
5441                    spec: Arc::clone(&shared_spec),
5442                },
5443            },
5444        );
5445        assert_eq!(left, right);
5446    }
5447
5448    #[test]
5449    fn test_lemma_type_inequality_import_different_arc_pointer() {
5450        // Two distinct Arc<LemmaSpec> (even with identical content) are not equal.
5451        let spec_a = Arc::new(LemmaSpec::new("dep".to_string()));
5452        let spec_b = Arc::new(LemmaSpec::new("dep".to_string()));
5453        let measure_spec = TypeSpecification::measure();
5454        let left = LemmaType::new(
5455            "t".to_string(),
5456            measure_spec.clone(),
5457            TypeExtends::Custom {
5458                parent: "money".to_string(),
5459                family: "money".to_string(),
5460                defining_spec: TypeDefiningSpec::Import {
5461                    spec: Arc::clone(&spec_a),
5462                },
5463            },
5464        );
5465        let right = LemmaType::new(
5466            "t".to_string(),
5467            measure_spec,
5468            TypeExtends::Custom {
5469                parent: "money".to_string(),
5470                family: "money".to_string(),
5471                defining_spec: TypeDefiningSpec::Import { spec: spec_b },
5472            },
5473        );
5474        assert_ne!(left, right);
5475    }
5476
5477    fn month_default_arg() -> CommandArg {
5478        CommandArg::Literal(crate::literals::Value::NumberWithUnit(
5479            Decimal::ONE,
5480            "month".to_string(),
5481        ))
5482    }
5483
5484    fn unit_factor_arg(name: &str, factor: i64) -> [CommandArg; 2] {
5485        [
5486            CommandArg::Label(name.to_string()),
5487            CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::from(factor))),
5488        ]
5489    }
5490
5491    #[test]
5492    fn default_calendar_on_text_reports_hint() {
5493        let specs = TypeSpecification::text();
5494        let mut default = None;
5495        let err = specs
5496            .apply_constraint(
5497                "notes",
5498                TypeConstraintCommand::Default,
5499                &[month_default_arg()],
5500                &mut default,
5501            )
5502            .unwrap_err();
5503        assert!(err.contains("Unit 'month' is for calendar data"));
5504        assert!(err.contains("double quotes"));
5505    }
5506
5507    #[test]
5508    fn default_calendar_on_duration_reports_valid_units() {
5509        let mut specs = TypeSpecification::measure();
5510        specs = specs
5511            .apply_constraint(
5512                "duration",
5513                TypeConstraintCommand::Unit,
5514                &unit_factor_arg("second", 1),
5515                &mut None,
5516            )
5517            .unwrap();
5518        specs = specs
5519            .apply_constraint(
5520                "duration",
5521                TypeConstraintCommand::Unit,
5522                &unit_factor_arg("week", 604_800),
5523                &mut None,
5524            )
5525            .unwrap();
5526        specs = specs
5527            .apply_constraint(
5528                "duration",
5529                TypeConstraintCommand::Trait,
5530                &[CommandArg::Label("duration".to_string())],
5531                &mut None,
5532            )
5533            .unwrap();
5534        let mut default = None;
5535        let err = specs
5536            .apply_constraint(
5537                "duration",
5538                TypeConstraintCommand::Default,
5539                &[month_default_arg()],
5540                &mut default,
5541            )
5542            .unwrap_err();
5543        assert!(err.contains("Unit 'month' is for calendar data"));
5544        assert!(err.contains("Valid 'duration' units are"));
5545        assert!(err.contains("week"));
5546    }
5547
5548    #[test]
5549    fn default_valid_duration_weeks_accepted() {
5550        let mut specs = TypeSpecification::measure();
5551        specs = specs
5552            .apply_constraint(
5553                "duration",
5554                TypeConstraintCommand::Unit,
5555                &unit_factor_arg("second", 1),
5556                &mut None,
5557            )
5558            .unwrap();
5559        specs = specs
5560            .apply_constraint(
5561                "duration",
5562                TypeConstraintCommand::Unit,
5563                &unit_factor_arg("week", 604_800),
5564                &mut None,
5565            )
5566            .unwrap();
5567        specs = specs
5568            .apply_constraint(
5569                "duration",
5570                TypeConstraintCommand::Trait,
5571                &[CommandArg::Label("duration".to_string())],
5572                &mut None,
5573            )
5574            .unwrap();
5575        let mut default = None;
5576        specs
5577            .apply_constraint(
5578                "duration",
5579                TypeConstraintCommand::Default,
5580                &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
5581                    Decimal::from(4),
5582                    "week".to_string(),
5583                ))],
5584                &mut default,
5585            )
5586            .unwrap();
5587        assert!(matches!(
5588            default,
5589            Some(RawDefault::Measure {
5590                unit_name,
5591                ..
5592            }) if unit_name == "week"
5593        ));
5594    }
5595
5596    #[test]
5597    fn default_unknown_unit_on_duration_lists_valid_units() {
5598        let mut specs = TypeSpecification::measure();
5599        specs = specs
5600            .apply_constraint(
5601                "duration",
5602                TypeConstraintCommand::Unit,
5603                &unit_factor_arg("second", 1),
5604                &mut None,
5605            )
5606            .unwrap();
5607        specs = specs
5608            .apply_constraint(
5609                "duration",
5610                TypeConstraintCommand::Trait,
5611                &[CommandArg::Label("duration".to_string())],
5612                &mut None,
5613            )
5614            .unwrap();
5615        let mut default = None;
5616        let err = specs
5617            .apply_constraint(
5618                "duration",
5619                TypeConstraintCommand::Default,
5620                &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
5621                    Decimal::ONE,
5622                    "fortnight".to_string(),
5623                ))],
5624                &mut default,
5625            )
5626            .unwrap_err();
5627        assert!(err.contains("fortnight"));
5628        assert!(err.contains("not defined on 'duration'"));
5629        assert!(err.contains("Valid units are"));
5630    }
5631
5632    fn money_measure_type() -> LemmaType {
5633        LemmaType::new(
5634            "Money".to_string(),
5635            TypeSpecification::Measure {
5636                minimum: None,
5637                maximum: None,
5638                decimals: None,
5639                units: MeasureUnits::from(vec![
5640                    MeasureUnit {
5641                        name: "eur".to_string(),
5642                        factor: crate::computation::rational::rational_one(),
5643                        derived_measure_factors: Vec::new(),
5644                        decomposition: BaseMeasureVector::new(),
5645                        minimum: None,
5646                        maximum: None,
5647                        default_magnitude: None,
5648                    },
5649                    MeasureUnit {
5650                        name: "usd".to_string(),
5651                        factor: crate::computation::rational::decimal_to_rational(Decimal::new(
5652                            91, 2,
5653                        ))
5654                        .expect("factor"),
5655                        derived_measure_factors: Vec::new(),
5656                        decomposition: BaseMeasureVector::new(),
5657                        minimum: None,
5658                        maximum: None,
5659                        default_magnitude: None,
5660                    },
5661                ]),
5662                traits: Vec::new(),
5663                decomposition: None,
5664                help: String::new(),
5665            },
5666            TypeExtends::Primitive,
5667        )
5668    }
5669
5670    #[test]
5671    fn measure_unit_names_for_named_measure() {
5672        let money = money_measure_type();
5673        assert_eq!(money.measure_unit_names(), Some(vec!["eur", "usd"]));
5674    }
5675
5676    // ---------------------------------------------------------------------------
5677    // Phase 0 — pin combine_signatures and canonicalize_signature behavior
5678    // ---------------------------------------------------------------------------
5679
5680    fn sig(pairs: &[(&str, i32)]) -> Vec<(String, i32)> {
5681        pairs.iter().map(|(s, e)| (s.to_string(), *e)).collect()
5682    }
5683
5684    #[test]
5685    fn combine_signatures_multiply_adds_exponents() {
5686        let left = sig(&[("eur", 1)]);
5687        let right = sig(&[("hour", -1)]);
5688        let result = combine_signatures(&left, &right, true);
5689        assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
5690    }
5691
5692    #[test]
5693    fn combine_signatures_divide_subtracts_exponents() {
5694        let left = sig(&[("eur", 1)]);
5695        let right = sig(&[("hour", 1)]);
5696        let result = combine_signatures(&left, &right, false);
5697        assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
5698    }
5699
5700    #[test]
5701    fn combine_signatures_cancels_to_empty() {
5702        let left = sig(&[("ce", 1), ("minute", -1)]);
5703        let right = sig(&[("minute", 1)]);
5704        let result = combine_signatures(&left, &right, true);
5705        // ce * (ce/min * min) = ce; minute cancels
5706        assert_eq!(result, sig(&[("ce", 1)]));
5707    }
5708
5709    #[test]
5710    fn combine_signatures_output_is_canonical_form() {
5711        let left = sig(&[("eur", 1), ("hour", 1)]);
5712        let right = sig(&[("minute", 1)]);
5713        let result = combine_signatures(&left, &right, false); // divide
5714                                                               // [("eur",1),("hour",1)] / [("minute",1)] = [("eur",1),("hour",1),("minute",-1)]
5715        let expected = sig(&[("eur", 1), ("hour", 1), ("minute", -1)]);
5716        assert_eq!(result, expected);
5717    }
5718
5719    #[test]
5720    fn canonicalize_signature_drops_zero_exponents() {
5721        let sig_with_zero = sig(&[("eur", 1), ("hour", 0), ("minute", -1)]);
5722        let result = canonicalize_signature(&sig_with_zero);
5723        assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
5724    }
5725
5726    #[test]
5727    fn canonicalize_signature_sorts_by_name() {
5728        let unsorted = sig(&[("minute", -1), ("eur", 1)]);
5729        let result = canonicalize_signature(&unsorted);
5730        assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
5731    }
5732
5733    // ---------------------------------------------------------------------------
5734    // Phase 0 — format_signature_operator_style (to be implemented in
5735    // signature_factor_and_display todo)
5736    // ---------------------------------------------------------------------------
5737
5738    #[test]
5739    fn format_signature_operator_style_numerator_only() {
5740        let signature = sig(&[("eur", 1)]);
5741        let result = format_signature_operator_style(&signature);
5742        assert_eq!(result, "eur");
5743    }
5744
5745    #[test]
5746    fn format_signature_operator_style_with_denominator() {
5747        let signature = sig(&[("eur", 1), ("hour", -1)]);
5748        let result = format_signature_operator_style(&signature);
5749        assert_eq!(result, "eur/hour");
5750    }
5751
5752    #[test]
5753    fn format_signature_operator_style_denominator_only() {
5754        let signature = sig(&[("meter", -1)]);
5755        let result = format_signature_operator_style(&signature);
5756        assert_eq!(result, "1/meter");
5757    }
5758
5759    #[test]
5760    fn format_signature_operator_style_with_exponents() {
5761        let signature = sig(&[("meter", 2), ("second", -2)]);
5762        let result = format_signature_operator_style(&signature);
5763        assert_eq!(result, "meter^2/second^2");
5764    }
5765
5766    // ---------------------------------------------------------------------------
5767    // Phase 0 — calendar_unit_factor (to be implemented in builtin_calendar_factor_table)
5768    // ---------------------------------------------------------------------------
5769
5770    #[test]
5771    fn calendar_unit_factor_table_completeness() {
5772        // Every SemanticCalendarUnit Display string must resolve to a factor.
5773        // Today SemanticCalendarUnit only has Month and Year; more may be added.
5774        for unit in &[SemanticCalendarUnit::Month, SemanticCalendarUnit::Year] {
5775            let name = unit.to_string();
5776            assert!(
5777                calendar_unit_factor(&name).is_some(),
5778                "calendar_unit_factor('{}') must return Some",
5779                name
5780            );
5781        }
5782    }
5783
5784    #[test]
5785    fn semantic_calendar_unit_display_returns_singular() {
5786        // Today Month => "months", Year => "years" (plural).
5787        // After singular_calendar_names_everywhere, must be "month" and "year".
5788        assert_eq!(SemanticCalendarUnit::Month.to_string(), "month");
5789        assert_eq!(SemanticCalendarUnit::Year.to_string(), "year");
5790    }
5791
5792    // ---------------------------------------------------------------------------
5793    // Phase 0 — signature_factor (to be implemented in signature_factor_and_display)
5794    // ---------------------------------------------------------------------------
5795
5796    #[test]
5797    fn signature_factor_with_calendar_units() {
5798        use std::collections::HashMap;
5799        let calendar = test_calendar_type_for_signature_factor();
5800        let unit_index: HashMap<String, Arc<LemmaType>> = HashMap::new();
5801        // month factor = 1, year factor = 12.
5802        // [(month,1),(year,-1)] = 1/12
5803        let sig_month_per_year = sig(&[("month", 1), ("year", -1)]);
5804        let factor = signature_factor(&sig_month_per_year, &unit_index, Some(&calendar))
5805            .expect("must not overflow");
5806        let expected = rational_new(1, 12);
5807        assert_eq!(factor, expected, "month/year factor must be 1/12");
5808    }
5809
5810    fn test_calendar_type_for_signature_factor() -> LemmaType {
5811        use crate::computation::rational::{decimal_to_rational, rational_one};
5812        use crate::literals::{MeasureUnit, MeasureUnits};
5813        use rust_decimal::Decimal;
5814        LemmaType::new(
5815            "calendar".to_string(),
5816            TypeSpecification::Measure {
5817                minimum: None,
5818                maximum: None,
5819                decimals: None,
5820                units: MeasureUnits::from(vec![
5821                    MeasureUnit {
5822                        name: "month".to_string(),
5823                        factor: rational_one(),
5824                        minimum: None,
5825                        maximum: None,
5826                        default_magnitude: None,
5827                        decomposition: calendar_decomposition(),
5828                        derived_measure_factors: Vec::new(),
5829                    },
5830                    MeasureUnit {
5831                        name: "year".to_string(),
5832                        factor: decimal_to_rational(Decimal::from(12)).expect("year factor"),
5833                        minimum: None,
5834                        maximum: None,
5835                        default_magnitude: None,
5836                        decomposition: calendar_decomposition(),
5837                        derived_measure_factors: Vec::new(),
5838                    },
5839                ]),
5840                traits: vec![MeasureTrait::Calendar],
5841                decomposition: Some(calendar_decomposition()),
5842                help: String::new(),
5843            },
5844            TypeExtends::Primitive,
5845        )
5846    }
5847
5848    #[test]
5849    #[should_panic(expected = "BUG: signature_factor called with unresolved unit name")]
5850    fn signature_factor_panics_on_unresolved_name() {
5851        use std::collections::HashMap;
5852        let unit_index: HashMap<String, Arc<LemmaType>> = HashMap::new();
5853        let bad_sig = sig(&[("nonexistent_unit_xyz", 1)]);
5854        let _ = signature_factor(&bad_sig, &unit_index, None);
5855    }
5856
5857    #[test]
5858    fn signature_factor_uses_owner_when_expression_index_empty() {
5859        use std::collections::HashMap;
5860        let money = test_money_type_for_signature_factor();
5861        let expression_units: HashMap<String, Arc<LemmaType>> = HashMap::new();
5862        let sig_usd = sig(&[("usd", 1)]);
5863        let factor =
5864            signature_factor(&sig_usd, &expression_units, Some(&money)).expect("must not overflow");
5865        assert_eq!(factor, rational_new(91, 100));
5866    }
5867
5868    fn test_money_type_for_signature_factor() -> LemmaType {
5869        use crate::computation::rational::decimal_to_rational;
5870        use crate::literals::{MeasureUnit, MeasureUnits};
5871        use rust_decimal::Decimal;
5872        LemmaType::new(
5873            "money".to_string(),
5874            TypeSpecification::Measure {
5875                minimum: None,
5876                maximum: None,
5877                decimals: Some(2),
5878                units: MeasureUnits::from(vec![
5879                    MeasureUnit {
5880                        name: "eur".to_string(),
5881                        factor: crate::computation::rational::rational_one(),
5882                        minimum: None,
5883                        maximum: None,
5884                        default_magnitude: None,
5885                        decomposition: BaseMeasureVector::new(),
5886                        derived_measure_factors: Vec::new(),
5887                    },
5888                    MeasureUnit {
5889                        name: "usd".to_string(),
5890                        factor: decimal_to_rational(Decimal::new(91, 2)).expect("usd factor"),
5891                        minimum: None,
5892                        maximum: None,
5893                        default_magnitude: None,
5894                        decomposition: BaseMeasureVector::new(),
5895                        derived_measure_factors: Vec::new(),
5896                    },
5897                ]),
5898                traits: Vec::new(),
5899                decomposition: None,
5900                help: String::new(),
5901            },
5902            TypeExtends::Primitive,
5903        )
5904    }
5905
5906    fn measure_type_with_kilogram() -> TypeSpecification {
5907        use crate::computation::rational::rational_one;
5908        use crate::literals::{MeasureUnit, MeasureUnits};
5909        let mut units = MeasureUnits::new();
5910        units.push(MeasureUnit {
5911            name: "kilogram".to_string(),
5912            factor: rational_one(),
5913            minimum: None,
5914            maximum: None,
5915            default_magnitude: None,
5916            decomposition: BaseMeasureVector::new(),
5917            derived_measure_factors: Vec::new(),
5918        });
5919        TypeSpecification::Measure {
5920            minimum: None,
5921            maximum: None,
5922            decimals: None,
5923            units,
5924            traits: Vec::new(),
5925            decomposition: None,
5926            help: String::new(),
5927        }
5928    }
5929
5930    #[test]
5931    fn parser_value_to_value_kind_rejects_bare_number_for_measure() {
5932        let ten = Value::Number(Decimal::from(10));
5933        let err = parser_value_to_value_kind(&ten, &measure_type_with_kilogram())
5934            .expect_err("bare number must not bind to measure");
5935        assert!(
5936            err.contains("kilogram"),
5937            "error must hint expected unit, got: {err}"
5938        );
5939    }
5940
5941    #[test]
5942    fn parser_value_to_value_kind_accepts_number_with_unit_for_measure() {
5943        let ten_kg = Value::NumberWithUnit(Decimal::from(10), "kilogram".to_string());
5944        let kind = parser_value_to_value_kind(&ten_kg, &measure_type_with_kilogram())
5945            .expect("10 kilogram must bind to measure");
5946        assert!(matches!(kind, ValueKind::Measure(_, _)));
5947    }
5948
5949    #[test]
5950    fn parser_value_to_value_kind_accepts_bare_number_for_ratio() {
5951        let ten = Value::Number(Decimal::from(10));
5952        let kind =
5953            parser_value_to_value_kind(&ten, &TypeSpecification::ratio()).expect("number -> ratio");
5954        assert!(matches!(kind, ValueKind::Ratio(_, None)));
5955    }
5956
5957    #[test]
5958    fn value_kind_matches_spec_rejects_number_for_measure() {
5959        let n = ValueKind::Number(rational_new(10, 1));
5960        assert!(!value_kind_matches_spec(&n, &measure_type_with_kilogram()));
5961    }
5962
5963    #[test]
5964    fn apply_constraint_rejects_inherited_unit_factor_change() {
5965        let mut specs = TypeSpecification::measure();
5966        specs = specs
5967            .apply_constraint(
5968                "money",
5969                TypeConstraintCommand::Unit,
5970                &unit_factor_arg("eur", 1),
5971                &mut None,
5972            )
5973            .expect("seed eur");
5974        let err = specs
5975            .apply_constraint(
5976                "money",
5977                TypeConstraintCommand::Unit,
5978                &[
5979                    CommandArg::Label("eur".to_string()),
5980                    CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::new(11, 1))),
5981                ],
5982                &mut None,
5983            )
5984            .expect_err("must not change inherited unit factor");
5985        assert!(err.contains("eur"), "error must name unit, got: {err}");
5986        assert!(
5987            err.contains("inherited") || err.contains("cannot change"),
5988            "error must reject factor change, got: {err}"
5989        );
5990    }
5991
5992    #[test]
5993    fn apply_constraint_allows_additive_unit_on_inherited_spec() {
5994        let mut specs = TypeSpecification::measure();
5995        specs = specs
5996            .apply_constraint(
5997                "money",
5998                TypeConstraintCommand::Unit,
5999                &unit_factor_arg("eur", 1),
6000                &mut None,
6001            )
6002            .expect("seed eur");
6003        specs = specs
6004            .apply_constraint(
6005                "money",
6006                TypeConstraintCommand::Unit,
6007                &unit_factor_arg("usd", 1),
6008                &mut None,
6009            )
6010            .expect("add usd");
6011        match &specs {
6012            TypeSpecification::Measure { units, .. } => assert_eq!(units.len(), 2),
6013            other => panic!("expected Measure, got {other:?}"),
6014        }
6015    }
6016
6017    #[test]
6018    fn apply_constraint_idempotent_inherited_unit_redeclare() {
6019        let mut specs = TypeSpecification::measure();
6020        specs = specs
6021            .apply_constraint(
6022                "money",
6023                TypeConstraintCommand::Unit,
6024                &unit_factor_arg("eur", 1),
6025                &mut None,
6026            )
6027            .expect("seed eur");
6028        specs = specs
6029            .apply_constraint(
6030                "money",
6031                TypeConstraintCommand::Unit,
6032                &unit_factor_arg("eur", 1),
6033                &mut None,
6034            )
6035            .expect("idempotent eur");
6036        match &specs {
6037            TypeSpecification::Measure { units, .. } => {
6038                assert_eq!(units.len(), 1);
6039                assert_eq!(
6040                    units.iter().find(|u| u.name == "eur").expect("eur").factor,
6041                    crate::computation::rational::rational_one()
6042                );
6043            }
6044            other => panic!("expected Measure, got {other:?}"),
6045        }
6046    }
6047
6048    #[test]
6049    fn element_from_range_returns_element_for_every_range_primitive() {
6050        type RangeElementMatcher = fn(&TypeSpecification) -> bool;
6051        let cases: [(PrimitiveKind, RangeElementMatcher); 5] = [
6052            (PrimitiveKind::NumberRange, |element| {
6053                matches!(element, TypeSpecification::Number { .. })
6054            }),
6055            (PrimitiveKind::MeasureRange, |element| {
6056                matches!(element, TypeSpecification::Measure { .. })
6057            }),
6058            (PrimitiveKind::RatioRange, |element| {
6059                matches!(element, TypeSpecification::Ratio { .. })
6060            }),
6061            (PrimitiveKind::DateRange, |element| {
6062                matches!(element, TypeSpecification::Date { .. })
6063            }),
6064            (PrimitiveKind::TimeRange, |element| {
6065                matches!(element, TypeSpecification::Time { .. })
6066            }),
6067        ];
6068        for (kind, matches_element) in cases {
6069            let range_spec = type_spec_for_primitive(kind);
6070            let element = range_spec
6071                .element_from_range()
6072                .unwrap_or_else(|| panic!("{kind:?} must define element_from_range"));
6073            assert!(
6074                matches_element(&element),
6075                "{kind:?} element must match documented mapping, got {element:?}"
6076            );
6077        }
6078    }
6079
6080    #[test]
6081    fn element_from_range_returns_none_for_non_range_primitives() {
6082        let non_range = [
6083            type_spec_for_primitive(PrimitiveKind::Boolean),
6084            type_spec_for_primitive(PrimitiveKind::Measure),
6085            TypeSpecification::Undetermined,
6086            TypeSpecification::veto(),
6087        ];
6088        for spec in non_range {
6089            assert!(
6090                spec.element_from_range().is_none(),
6091                "{spec:?} must not define element_from_range"
6092            );
6093        }
6094    }
6095}