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