Skip to main content

lemma/planning/
semantics.rs

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