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