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