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