Skip to main content

lemma/parsing/
ast.rs

1//! AST types
2//!
3//! Infrastructure (Span, DepthTracker) and spec/data/rule/expression/value types from parsing.
4//!
5//! # Human `Display` vs canonical `AsLemmaSource`
6//!
7//! [`DataValue`] and [`CommandArg`] use human-oriented `Display` (stable for
8//! `to_string()`, logs, APIs). [`Expression`] and [`LemmaRule`]/[`LemmaSpec`]
9//! use canonical Lemma source for literals via [`AsLemmaSource`] around
10//! [`Value`]. Wrap [`DataValue`] in [`AsLemmaSource`] when emitting
11//! round-trippable source (e.g. the formatter).
12//!
13//! Logical identifier names (spec, data, rule, unit, reference path segments) are stored
14//! as ASCII lowercase after parse. String literals and text option values are unchanged.
15
16/// Fold a logical identifier name to canonical ASCII lowercase.
17pub(crate) fn ascii_lowercase_logical_name(name: String) -> String {
18    name.to_ascii_lowercase()
19}
20
21/// Span representing a location in source code
22#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
23pub struct Span {
24    pub start: usize,
25    pub end: usize,
26    pub line: usize,
27    pub col: usize,
28}
29
30/// Tracks expression nesting depth during parsing to prevent stack overflow
31pub struct DepthTracker {
32    depth: usize,
33    max_depth: usize,
34}
35
36impl DepthTracker {
37    pub fn with_max_depth(max_depth: usize) -> Self {
38        Self {
39            depth: 0,
40            max_depth,
41        }
42    }
43
44    /// Returns Ok(()) if within limits, Err(current_depth) if exceeded.
45    pub fn push_depth(&mut self) -> Result<(), usize> {
46        self.depth += 1;
47        if self.depth > self.max_depth {
48            return Err(self.depth);
49        }
50        Ok(())
51    }
52
53    pub fn pop_depth(&mut self) {
54        if self.depth > 0 {
55            self.depth -= 1;
56        }
57    }
58
59    pub fn max_depth(&self) -> usize {
60        self.max_depth
61    }
62}
63
64impl Default for DepthTracker {
65    fn default() -> Self {
66        Self {
67            depth: 0,
68            max_depth: 5,
69        }
70    }
71}
72
73// -----------------------------------------------------------------------------
74// Spec, data, rule, expression and value types
75// -----------------------------------------------------------------------------
76
77use crate::parsing::source::Source;
78use rust_decimal::Decimal;
79use serde::Serialize;
80use std::cmp::Ordering;
81use std::fmt;
82use std::hash::{Hash, Hasher};
83use std::sync::Arc;
84
85pub use crate::literals::{BooleanValue, DateTimeValue, TimeValue, TimezoneValue, Value};
86
87#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
88pub enum EffectiveDate {
89    Origin,
90    DateTimeValue(crate::DateTimeValue),
91}
92
93impl EffectiveDate {
94    pub fn as_ref(&self) -> Option<&crate::DateTimeValue> {
95        match self {
96            EffectiveDate::Origin => None,
97            EffectiveDate::DateTimeValue(dt) => Some(dt),
98        }
99    }
100
101    pub fn from_option(opt: Option<crate::DateTimeValue>) -> Self {
102        match opt {
103            None => EffectiveDate::Origin,
104            Some(dt) => EffectiveDate::DateTimeValue(dt),
105        }
106    }
107
108    pub fn to_option(&self) -> Option<crate::DateTimeValue> {
109        match self {
110            EffectiveDate::Origin => None,
111            EffectiveDate::DateTimeValue(dt) => Some(dt.clone()),
112        }
113    }
114
115    pub fn is_origin(&self) -> bool {
116        matches!(self, EffectiveDate::Origin)
117    }
118}
119
120impl PartialOrd for EffectiveDate {
121    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
122        Some(self.cmp(other))
123    }
124}
125
126impl Ord for EffectiveDate {
127    // As ref returns None for Origin, so Origin < DateTimeValue(_).
128    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
129        self.as_ref().cmp(&other.as_ref())
130    }
131}
132
133impl fmt::Display for EffectiveDate {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        match self {
136            EffectiveDate::Origin => Ok(()),
137            EffectiveDate::DateTimeValue(dt) => write!(f, "{}", dt),
138        }
139    }
140}
141
142/// A Lemma repository header. Identity carrier; never owns specs.
143///
144/// `name` includes the `@` prefix when present (e.g. `Some("@jack/finance")`).
145/// `None` for the default unnamed repository. Identity (used by
146/// `PartialEq`, `Eq`, `Hash`, and `Ord` for `BTreeMap` keying) is just `name`.
147/// `dependency`, `start_line` and `source_type` are metadata excluded from identity.
148///
149/// `dependency` is the provenance guard: `None` for locally loaded repos,
150/// `Some(id)` for repos introduced by a dependency. All specs in a repo must
151/// share the same `dependency` value — the engine rejects mismatches at load time.
152///
153/// The parser fills [`LemmaRepository`] for each `repo` section before grouping specs in
154/// [`ParseResult`]; loaders set `dependency` when inserting dependency bundles.
155#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
156pub struct LemmaRepository {
157    /// Repository name, including `@` when present. `None` for the default unnamed repository.
158    pub name: Option<String>,
159    /// Dependency provenance: `None` for locally loaded repos, `Some(id)` for dependency repos.
160    /// Not part of identity — used as an isolation guard at load time.
161    pub dependency: Option<String>,
162    pub start_line: usize,
163    pub source_type: Option<crate::parsing::source::SourceType>,
164}
165
166impl LemmaRepository {
167    #[must_use]
168    pub fn new(name: Option<String>) -> Self {
169        Self {
170            name: name.map(ascii_lowercase_logical_name),
171            dependency: None,
172            start_line: 1,
173            source_type: None,
174        }
175    }
176
177    #[must_use]
178    pub fn with_start_line(mut self, start_line: usize) -> Self {
179        self.start_line = start_line;
180        self
181    }
182
183    #[must_use]
184    pub fn with_source_type(mut self, source_type: crate::parsing::source::SourceType) -> Self {
185        self.source_type = Some(source_type);
186        self
187    }
188
189    #[must_use]
190    pub fn with_dependency(mut self, dependency_id: impl Into<String>) -> Self {
191        self.dependency = Some(dependency_id.into());
192        self
193    }
194}
195
196impl PartialEq for LemmaRepository {
197    fn eq(&self, other: &Self) -> bool {
198        self.name == other.name
199    }
200}
201
202impl Eq for LemmaRepository {}
203
204impl PartialOrd for LemmaRepository {
205    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
206        Some(self.cmp(other))
207    }
208}
209
210impl Ord for LemmaRepository {
211    fn cmp(&self, other: &Self) -> Ordering {
212        self.name.cmp(&other.name)
213    }
214}
215
216impl Hash for LemmaRepository {
217    fn hash<H: Hasher>(&self, state: &mut H) {
218        self.name.hash(state);
219    }
220}
221
222/// Textual repository qualifier as written in source (for example `@iso/countries`).
223/// `name` stores the qualifier verbatim, including a leading `@` when present. The planner
224/// resolves a [`RepositoryQualifier`] to an `Arc<LemmaRepository>` against the active context.
225#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
226pub struct RepositoryQualifier {
227    pub name: String,
228}
229
230impl RepositoryQualifier {
231    #[must_use]
232    pub fn new(name: impl Into<String>) -> Self {
233        Self {
234            name: ascii_lowercase_logical_name(name.into()),
235        }
236    }
237
238    /// Whether this repository qualifier refers to a registry (e.g., starts with `@`).
239    #[must_use]
240    pub fn is_registry(&self) -> bool {
241        self.name.starts_with('@')
242    }
243}
244
245impl fmt::Display for RepositoryQualifier {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        write!(f, "{}", self.name)
248    }
249}
250
251/// A Lemma spec containing data and rules.
252///
253/// Owning repository is carried by [`crate::engine::Context`], not fields here.
254/// Context identity is `(repository, name, EffectiveDate)` or `std::ptr::eq` on a
255/// Context-owned row — not [`PartialEq`]. [`PartialEq`] is full AST equality
256/// (including statement [`Source`] spans) for [`crate::Engine::update`] skip only.
257#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
258pub struct LemmaSpec {
259    pub name: String,
260    pub effective_from: EffectiveDate,
261    pub source_type: Option<crate::parsing::source::SourceType>,
262    pub start_line: usize,
263    pub commentary: Option<String>,
264    pub data: Vec<LemmaData>,
265    pub rules: Vec<LemmaRule>,
266    pub meta_fields: Vec<MetaField>,
267}
268
269#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
270pub struct MetaField {
271    pub key: String,
272    pub value: Value,
273    pub source_location: Source,
274}
275
276impl fmt::Display for MetaField {
277    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
278        write!(f, "meta {}: {}", self.key, AsLemmaSource(&self.value))
279    }
280}
281
282#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
283pub struct LemmaData {
284    pub reference: Reference,
285    pub value: DataValue,
286    pub source_location: Source,
287}
288
289/// An unless clause that provides an alternative result
290///
291/// Unless clauses are evaluated in order, and the last matching condition wins.
292/// This matches natural language: "X unless A then Y, unless B then Z" - if both
293/// A and B are true, Z is returned (the last match).
294#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
295pub struct UnlessClause {
296    pub condition: Expression,
297    pub result: Expression,
298    pub source_location: Source,
299}
300
301/// A rule with a single expression and optional unless clauses
302#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
303pub struct LemmaRule {
304    pub name: String,
305    pub expression: Expression,
306    pub unless_clauses: Vec<UnlessClause>,
307    pub source_location: Source,
308}
309
310/// An expression that can be evaluated, with source location
311///
312/// Expressions use semantic equality - two expressions with the same
313/// structure (kind) are equal regardless of source location.
314/// Hash is not implemented for AST Expression; use planning::semantics::Expression as map keys.
315#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
316pub struct Expression {
317    pub kind: ExpressionKind,
318    pub source_location: Option<Source>,
319}
320
321impl Expression {
322    /// Create a new expression with kind and source location
323    #[must_use]
324    pub fn new(kind: ExpressionKind, source_location: Source) -> Self {
325        Self {
326            kind,
327            source_location: Some(source_location),
328        }
329    }
330}
331
332/// Semantic equality - compares expressions by structure only, ignoring source location
333impl PartialEq for Expression {
334    fn eq(&self, other: &Self) -> bool {
335        self.kind == other.kind
336    }
337}
338
339impl Eq for Expression {}
340
341/// Whether a date is relative to `now` in the past or future direction.
342#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
343#[serde(rename_all = "snake_case")]
344pub enum DateRelativeKind {
345    InPast,
346    InFuture,
347}
348
349/// Calendar-period membership checks.
350#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
351#[serde(rename_all = "snake_case")]
352pub enum DateCalendarKind {
353    Current,
354    Past,
355    Future,
356    NotIn,
357}
358
359/// Granularity of a calendar-period check.
360#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
361#[serde(rename_all = "snake_case")]
362pub enum CalendarPeriodUnit {
363    Year,
364    Month,
365    Week,
366}
367
368impl CalendarPeriodUnit {
369    #[must_use]
370    pub fn from_keyword(s: &str) -> Option<Self> {
371        match s.trim().to_lowercase().as_str() {
372            "year" => Some(Self::Year),
373            "month" => Some(Self::Month),
374            "week" => Some(Self::Week),
375            _ => None,
376        }
377    }
378}
379
380impl fmt::Display for DateRelativeKind {
381    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
382        match self {
383            DateRelativeKind::InPast => write!(f, "in past"),
384            DateRelativeKind::InFuture => write!(f, "in future"),
385        }
386    }
387}
388
389impl fmt::Display for DateCalendarKind {
390    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391        match self {
392            DateCalendarKind::Current => write!(f, "in calendar"),
393            DateCalendarKind::Past => write!(f, "in past calendar"),
394            DateCalendarKind::Future => write!(f, "in future calendar"),
395            DateCalendarKind::NotIn => write!(f, "not in calendar"),
396        }
397    }
398}
399
400impl fmt::Display for CalendarPeriodUnit {
401    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
402        match self {
403            CalendarPeriodUnit::Year => write!(f, "year"),
404            CalendarPeriodUnit::Month => write!(f, "month"),
405            CalendarPeriodUnit::Week => write!(f, "week"),
406        }
407    }
408}
409
410/// The kind/type of expression
411#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
412#[serde(rename_all = "snake_case")]
413pub enum ExpressionKind {
414    /// Parse-time literal value (type will be resolved during planning)
415    Literal(Value),
416    /// Unresolved reference (identifier or dot path). Resolved during planning to DataPath or RulePath.
417    Reference(Reference),
418    /// The `now` keyword — resolves to the evaluation datetime (= effective).
419    Now,
420    /// Date-relative sugar: `<date_expr> in past` / `<date_expr> in future`
421    /// Fields: (kind, date_expression)
422    DateRelative(DateRelativeKind, Arc<Expression>),
423    /// Calendar-period sugar: `<date_expr> in [past|future] calendar year|month|week`
424    /// Fields: (kind, unit, date_expression)
425    DateCalendar(DateCalendarKind, CalendarPeriodUnit, Arc<Expression>),
426    /// Range literal: `{left_expr}...{right_expr}`
427    RangeLiteral(Arc<Expression>, Arc<Expression>),
428    /// Relative date range: `past 7 day` / `future 30 day`
429    PastFutureRange(DateRelativeKind, Arc<Expression>),
430    /// Range containment: `{value_expr} in {range_expr}`
431    RangeContainment(Arc<Expression>, Arc<Expression>),
432    /// Ordered endpoint of a range: `lower` (included) or `upper` (excluded).
433    RangeBound(RangeBound, Arc<Expression>),
434    LogicalAnd(Arc<Expression>, Arc<Expression>),
435    Arithmetic(Arc<Expression>, ArithmeticComputation, Arc<Expression>),
436    Comparison(Arc<Expression>, ComparisonComputation, Arc<Expression>),
437    UnitConversion(Arc<Expression>, ConversionTarget),
438    LogicalNegation(Arc<Expression>, NegationType),
439    MathematicalComputation(MathematicalComputation, Arc<Expression>),
440    Veto(VetoExpression),
441    /// `expr is veto` / `veto is expr` — boolean: whether evaluating `expr` yields `OperationResult::Veto`.
442    ResultIsVeto(Arc<Expression>),
443}
444
445/// Unresolved reference from parser
446///
447/// Reference to a data or rule (identifier or dot path).
448///
449/// Used in expressions and in LemmaData. During planning, references
450/// are resolved to DataPath or RulePath (semantics layer).
451/// Examples:
452/// - Local "age": segments=[], name="age"
453/// - Cross-spec "employee.salary": segments=["employee"], name="salary"
454#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
455pub struct Reference {
456    pub segments: Vec<String>,
457    pub name: String,
458}
459
460impl Reference {
461    #[must_use]
462    pub fn local(name: String) -> Self {
463        Self {
464            segments: Vec::new(),
465            name: ascii_lowercase_logical_name(name),
466        }
467    }
468
469    #[must_use]
470    pub fn from_path(path: Vec<String>) -> Self {
471        if path.is_empty() {
472            Self {
473                segments: Vec::new(),
474                name: String::new(),
475            }
476        } else {
477            // Safe: path is non-empty.
478            let name = ascii_lowercase_logical_name(path[path.len() - 1].clone());
479            let segments = path[..path.len() - 1]
480                .iter()
481                .map(|segment| ascii_lowercase_logical_name(segment.clone()))
482                .collect();
483            Self { segments, name }
484        }
485    }
486
487    #[must_use]
488    pub fn is_local(&self) -> bool {
489        self.segments.is_empty()
490    }
491
492    #[must_use]
493    pub fn full_path(&self) -> Vec<String> {
494        let mut path = self.segments.clone();
495        path.push(self.name.clone());
496        path
497    }
498}
499
500impl fmt::Display for Reference {
501    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
502        for segment in &self.segments {
503            write!(f, "{}.", segment)?;
504        }
505        write!(f, "{}", self.name)
506    }
507}
508
509/// Arithmetic computations
510#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
511#[serde(rename_all = "snake_case")]
512pub enum ArithmeticComputation {
513    Add,
514    Subtract,
515    Multiply,
516    Divide,
517    Modulo,
518    Power,
519}
520
521/// Comparison computations
522#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
523#[serde(rename_all = "snake_case")]
524pub enum ComparisonComputation {
525    GreaterThan,
526    LessThan,
527    GreaterThanOrEqual,
528    LessThanOrEqual,
529    Is,
530    IsNot,
531}
532
533impl ComparisonComputation {
534    /// Check if this is an equality comparison (`is`)
535    #[must_use]
536    pub fn is_equal(&self) -> bool {
537        matches!(self, ComparisonComputation::Is)
538    }
539
540    /// Check if this is an inequality comparison (`is not`)
541    #[must_use]
542    pub fn is_not_equal(&self) -> bool {
543        matches!(self, ComparisonComputation::IsNot)
544    }
545}
546
547/// The target type for `as` cast expressions (e.g. `as number`, `as eur`).
548#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
549#[serde(rename_all = "snake_case")]
550pub enum ConversionTarget {
551    Type(PrimitiveKind),
552    Unit { unit_name: String },
553}
554
555/// Types of logical negation
556#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
557#[serde(rename_all = "snake_case")]
558pub enum NegationType {
559    Not,
560}
561
562/// A veto expression that prohibits any valid verdict from the rule
563///
564/// A veto prevents the rule from producing any valid result. This is used for
565/// validation and constraint enforcement — distinct from boolean `false`.
566///
567/// Example: `veto "Must be over 18"` - blocks the rule entirely with a message
568#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
569pub struct VetoExpression {
570    pub message: Option<String>,
571}
572
573/// Mathematical computations
574#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
575#[serde(rename_all = "snake_case")]
576pub enum MathematicalComputation {
577    Sqrt,
578    Sin,
579    Cos,
580    Tan,
581    Asin,
582    Acos,
583    Atan,
584    Log,
585    Exp,
586    Abs,
587    Floor,
588    Ceil,
589    Round,
590}
591
592/// Which endpoint of a range `lower` / `upper` reads.
593#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
594#[serde(rename_all = "snake_case")]
595pub enum RangeBound {
596    Lower,
597    Upper,
598}
599
600impl fmt::Display for RangeBound {
601    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
602        match self {
603            RangeBound::Lower => write!(f, "lower"),
604            RangeBound::Upper => write!(f, "upper"),
605        }
606    }
607}
608
609/// A spec reference written in source.
610///
611/// [`SpecRef::repository`] is `None` for same-repository references, or
612/// `Some(RepositoryQualifier)` when a repository qualifier was written before the spec name.
613/// `effective` carries an optional explicit pin written next to the spec name.
614#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
615pub struct SpecRef {
616    /// Optional explicit repository qualifier. `None` means the reference resolves against
617    /// the consumer spec's own repository.
618    pub repository: Option<RepositoryQualifier>,
619    /// The spec name.
620    pub name: String,
621    /// Optional explicit effective datetime pin written in source.
622    pub effective: Option<DateTimeValue>,
623    /// Source span of the repository qualifier (when `repository` is present).
624    pub repository_span: Option<Span>,
625    /// Source span of `name` and optional `effective`.
626    pub target_span: Option<Span>,
627}
628
629impl std::fmt::Display for SpecRef {
630    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
631        if let Some(qualifier) = &self.repository {
632            write!(f, "{} ", qualifier)?;
633        }
634        write!(f, "{}", self.name)?;
635        if let Some(d) = &self.effective {
636            write!(f, " {}", d)?;
637        }
638        Ok(())
639    }
640}
641
642impl SpecRef {
643    /// Same-repository reference: resolution uses the consumer's repository.
644    pub fn same_repository(name: impl Into<String>) -> Self {
645        Self {
646            name: ascii_lowercase_logical_name(name.into()),
647            repository: None,
648            effective: None,
649            repository_span: None,
650            target_span: None,
651        }
652    }
653
654    /// Cross-repository reference with an explicit repository qualifier.
655    pub fn cross_repository(name: impl Into<String>, qualifier: RepositoryQualifier) -> Self {
656        Self {
657            name: ascii_lowercase_logical_name(name.into()),
658            repository: Some(qualifier),
659            effective: None,
660            repository_span: None,
661            target_span: None,
662        }
663    }
664
665    /// Resolve the effective instant for this reference given the planning slice's `effective`.
666    /// Explicit qualifier on the reference wins; otherwise inherits the slice instant.
667    pub fn at(&self, effective: &EffectiveDate) -> EffectiveDate {
668        self.effective
669            .clone()
670            .map_or_else(|| effective.clone(), EffectiveDate::DateTimeValue)
671    }
672
673    /// Concrete instant for evaluation or navigation: explicit ref pin wins, else consumer bound.
674    /// Returns `None` when neither side names a concrete instant (consumer at Origin, no ref pin).
675    pub fn resolved_instant(
676        &self,
677        consumer_effective_from: Option<&DateTimeValue>,
678    ) -> Option<DateTimeValue> {
679        self.effective
680            .clone()
681            .or_else(|| consumer_effective_from.cloned())
682    }
683}
684
685/// A single factor in a compound unit expression.
686///
687/// `measure_ref` is the name of the referenced unit (e.g. `"meter"`, `"second"`).
688/// `exp` is the integer exponent, positive for numerator and negative for denominator.
689/// For example `meter/second^2` produces:
690/// - `UnitFactor { measure_ref: "meter", exp: 1 }`
691/// - `UnitFactor { measure_ref: "second", exp: -2 }`
692#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
693pub struct UnitFactor {
694    pub measure_ref: String,
695    pub exp: i32,
696}
697
698/// The argument to a `-> unit <name> ...` command, either a plain numeric
699/// conversion factor or a compound unit expression.
700///
701/// - `Factor(v)` — simple unit: `-> unit meter: 1`, `-> unit kilometer: 1000`
702/// - `Expr(prefix, factors)` — compound unit: `-> unit mps: meter/second`,
703///   `-> unit kmh: 3.6 meter/second`
704///   The `prefix` is an additional scalar multiplier beyond what the unit
705///   factor references contribute; it defaults to `1` when omitted.
706#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
707pub enum UnitArg {
708    Factor(#[serde(with = "crate::literals::decimal_binary_serde")] Decimal),
709    Expr(
710        #[serde(with = "crate::literals::decimal_binary_serde")] Decimal,
711        Vec<UnitFactor>,
712    ),
713}
714
715impl fmt::Display for UnitArg {
716    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
717        match self {
718            UnitArg::Factor(v) => write!(f, "{}", v),
719            UnitArg::Expr(prefix, factors) => {
720                if *prefix != Decimal::ONE {
721                    write!(f, "{} ", prefix)?;
722                }
723                for (index, factor) in factors.iter().enumerate() {
724                    if factor.exp == 0 {
725                        unreachable!("BUG: unit factor exponent cannot be zero");
726                    }
727                    if factor.exp > 0 {
728                        if index > 0 {
729                            write!(f, " * ")?;
730                        }
731                        write!(f, "{}", factor.measure_ref)?;
732                        if factor.exp != 1 {
733                            write!(f, "^{}", factor.exp)?;
734                        }
735                    } else {
736                        let denominator_started =
737                            factors[..index].iter().any(|prior| prior.exp < 0);
738                        if denominator_started {
739                            write!(f, " * ")?;
740                        } else {
741                            write!(f, "/")?;
742                        }
743                        write!(f, "{}", factor.measure_ref)?;
744                        let positive_exp = factor
745                            .exp
746                            .checked_neg()
747                            .expect("BUG: negative unit factor exponent");
748                        if positive_exp != 1 {
749                            write!(f, "^{}", positive_exp)?;
750                        }
751                    }
752                }
753                Ok(())
754            }
755        }
756    }
757}
758
759/// A parsed constraint command argument, preserving the literal kind from the
760/// grammar rule `command_arg: { number_literal | boolean_literal | text_literal | label }`.
761///
762/// Three grammatical kinds appear after a constraint command:
763/// - **Literal** — a fully-typed value carrying the literal kind the parser
764///   recognised (`Number`, `Ratio`, `Measure`, `Date`, `Time`,
765///   `Boolean`, `Text`). Stored as the canonical [`crate::literals::Value`]
766///   so downstream consumers match on the variant rather than re-parsing strings.
767/// - **Label** — a bare identifier used as a name (e.g. the unit name `eur`
768///   in `unit eur 1.00`, or a primitive type keyword used as an option label).
769/// - **UnitExpr** — compound unit expression produced by the parser for
770///   `-> unit <name> ...` commands. Only appears as the second argument of a
771///   `Unit` command; the first argument is always the unit name as `Label`.
772///
773/// Planning validates each command's args against the variant kinds it accepts
774/// and rejects mismatches without coercion (a `Text` literal is never a `Number`,
775/// a `Ratio` literal is never a bare `Number`, etc.).
776#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
777pub enum CommandArg {
778    /// A typed literal value parsed by [`crate::parsing::parser::Parser::parse_literal_value`].
779    Literal(crate::literals::Value),
780    /// An identifier used as a name (unit name, option keyword, etc.).
781    Label(String),
782    /// A unit argument produced by the parser for `-> unit <name> ...` commands.
783    UnitExpr(UnitArg),
784}
785
786impl fmt::Display for CommandArg {
787    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
788        match self {
789            CommandArg::Literal(v) => write!(f, "{}", v),
790            CommandArg::Label(s) => write!(f, "{}", s),
791            CommandArg::UnitExpr(unit_arg) => write!(f, "{}", unit_arg),
792        }
793    }
794}
795
796/// Constraint command for type definitions. Derived from lexer tokens; no string matching.
797#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
798#[serde(rename_all = "snake_case")]
799pub enum TypeConstraintCommand {
800    Help,
801    Suggest,
802    Fill,
803    Unit,
804    Trait,
805    Minimum,
806    Maximum,
807    Lower,
808    Upper,
809    Decimals,
810    Option,
811    Options,
812    Length,
813}
814
815impl fmt::Display for TypeConstraintCommand {
816    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
817        let s = match self {
818            TypeConstraintCommand::Help => "help",
819            TypeConstraintCommand::Suggest => "suggest",
820            TypeConstraintCommand::Fill => "fill",
821            TypeConstraintCommand::Unit => "unit",
822            TypeConstraintCommand::Trait => "trait",
823            TypeConstraintCommand::Minimum => "minimum",
824            TypeConstraintCommand::Maximum => "maximum",
825            TypeConstraintCommand::Lower => "lower",
826            TypeConstraintCommand::Upper => "upper",
827            TypeConstraintCommand::Decimals => "decimals",
828            TypeConstraintCommand::Option => "option",
829            TypeConstraintCommand::Options => "options",
830            TypeConstraintCommand::Length => "length",
831        };
832        write!(f, "{}", s)
833    }
834}
835
836/// Parses a constraint command name. Returns None for unknown (parser returns error).
837#[must_use]
838pub fn try_parse_type_constraint_command(s: &str) -> Option<TypeConstraintCommand> {
839    // ASCII fold without allocating a lowercased String on every `->` command.
840    match s.trim() {
841        s if s.eq_ignore_ascii_case("help") => Some(TypeConstraintCommand::Help),
842        s if s.eq_ignore_ascii_case("suggest") => Some(TypeConstraintCommand::Suggest),
843        s if s.eq_ignore_ascii_case("fill") => Some(TypeConstraintCommand::Fill),
844        s if s.eq_ignore_ascii_case("unit") => Some(TypeConstraintCommand::Unit),
845        s if s.eq_ignore_ascii_case("trait") => Some(TypeConstraintCommand::Trait),
846        s if s.eq_ignore_ascii_case("minimum") => Some(TypeConstraintCommand::Minimum),
847        s if s.eq_ignore_ascii_case("maximum") => Some(TypeConstraintCommand::Maximum),
848        s if s.eq_ignore_ascii_case("lower") => Some(TypeConstraintCommand::Lower),
849        s if s.eq_ignore_ascii_case("upper") => Some(TypeConstraintCommand::Upper),
850        s if s.eq_ignore_ascii_case("decimals") => Some(TypeConstraintCommand::Decimals),
851        s if s.eq_ignore_ascii_case("option") => Some(TypeConstraintCommand::Option),
852        s if s.eq_ignore_ascii_case("options") => Some(TypeConstraintCommand::Options),
853        s if s.eq_ignore_ascii_case("length") => Some(TypeConstraintCommand::Length),
854        _ => None,
855    }
856}
857
858/// Whether a `->` continuation uses assignment shape (`key: value`) or space-separated args.
859#[derive(Debug, Clone, Copy, PartialEq, Eq)]
860pub enum ContinuationShape {
861    Assignment,
862    SpaceSeparated,
863}
864
865impl TypeConstraintCommand {
866    #[must_use]
867    pub fn continuation_shape(self) -> ContinuationShape {
868        match self {
869            TypeConstraintCommand::Unit => ContinuationShape::Assignment,
870            TypeConstraintCommand::Help
871            | TypeConstraintCommand::Suggest
872            | TypeConstraintCommand::Fill
873            | TypeConstraintCommand::Trait
874            | TypeConstraintCommand::Minimum
875            | TypeConstraintCommand::Maximum
876            | TypeConstraintCommand::Lower
877            | TypeConstraintCommand::Upper
878            | TypeConstraintCommand::Decimals
879            | TypeConstraintCommand::Option
880            | TypeConstraintCommand::Options
881            | TypeConstraintCommand::Length => ContinuationShape::SpaceSeparated,
882        }
883    }
884}
885
886/// One `-> command …` row on a [`DataValue::Definition`].
887#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
888pub struct Constraint {
889    pub command: TypeConstraintCommand,
890    pub args: Vec<CommandArg>,
891    pub source_location: crate::parsing::source::Source,
892    /// Parsed from deprecated `unit name value` without colon (removed in a future release).
893    pub deprecated_without_colon: bool,
894}
895
896impl Constraint {
897    #[must_use]
898    pub fn new(
899        command: TypeConstraintCommand,
900        args: Vec<CommandArg>,
901        source_location: crate::parsing::source::Source,
902    ) -> Self {
903        Self {
904            command,
905            args,
906            source_location,
907            deprecated_without_colon: false,
908        }
909    }
910}
911
912#[cfg(test)]
913pub(crate) fn test_constraint(command: TypeConstraintCommand, args: Vec<CommandArg>) -> Constraint {
914    Constraint::new(
915        command,
916        args,
917        crate::parsing::source::Source::new(
918            crate::parsing::source::SourceType::Volatile,
919            Span {
920                start: 0,
921                end: 0,
922                line: 1,
923                col: 0,
924            },
925        ),
926    )
927}
928
929/// Right-hand side of a `uses` block `-> with` binding: literal value or reference to copy.
930#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
931#[serde(rename_all = "snake_case")]
932pub enum WithRhs {
933    Literal(Value),
934    Reference { target: Reference },
935}
936
937/// One `-> with path: value` row under a [`DataValue::Import`] block.
938#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
939pub struct UsesBinding {
940    /// Path relative to the imported spec (no import alias prefix).
941    pub path: Reference,
942    pub rhs: WithRhs,
943    pub source_location: Source,
944    /// Parsed from deprecated standalone `with alias.path: …` (removed in a future release).
945    pub deprecated_standalone_with: bool,
946}
947
948/// Prefix import alias onto a relative binding path (`pricing.tax_rate` under alias `line` → `line.pricing.tax_rate`).
949#[must_use]
950pub fn prefix_reference(alias: &str, relative: &Reference) -> Reference {
951    let mut segments = vec![ascii_lowercase_logical_name(alias.to_string())];
952    segments.extend(relative.segments.iter().cloned());
953    Reference {
954        segments,
955        name: relative.name.clone(),
956    }
957}
958
959#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
960#[serde(rename_all = "snake_case")]
961/// Parse-time data value (before type resolution)
962pub enum DataValue {
963    /// Declares data: optional explicit parent type, optional constraints (`-> ...`),
964    /// and optional literal value.
965    ///
966    /// Examples:
967    /// - `data x: 3.14` → `base: None`, `value: Some(Number)`
968    /// - `data x: number -> minimum 0` → `base: Some(Number)`, `constraints: Some(...)`
969    /// - `data x: finance.money` → `base: Some(Qualified { spec_alias: "finance", inner: Custom("money") })`
970    Definition {
971        base: Option<ParentType>,
972        constraints: Option<Vec<Constraint>>,
973        value: Option<Value>,
974    },
975    /// Import from another spec (surface syntax is `uses`; alias is [`LemmaData::reference`]).
976    Import {
977        spec_ref: SpecRef,
978        bindings: Vec<UsesBinding>,
979    },
980}
981
982impl DataValue {
983    #[must_use]
984    pub fn import(spec_ref: SpecRef) -> Self {
985        Self::Import {
986            spec_ref,
987            bindings: Vec::new(),
988        }
989    }
990
991    /// Whether this is only a literal RHS (`data x: 3.14`), valid as a binding value.
992    #[must_use]
993    pub fn is_definition_literal_only(&self) -> bool {
994        matches!(
995            self,
996            DataValue::Definition {
997                base: None,
998                constraints: None,
999                value: Some(_),
1000            }
1001        )
1002    }
1003
1004    /// Whether planning must resolve this [`LemmaData`] row through the type resolver / named types.
1005    #[must_use]
1006    pub fn definition_needs_type_resolution(&self) -> bool {
1007        match self {
1008            DataValue::Definition { base: Some(_), .. }
1009            | DataValue::Definition {
1010                constraints: Some(_),
1011                ..
1012            } => true,
1013            DataValue::Definition {
1014                base: None,
1015                constraints: None,
1016                value: Some(v),
1017            } => !matches!(v, Value::NumberWithUnit(_, _)),
1018            DataValue::Import { .. } | DataValue::Definition { .. } => false,
1019        }
1020    }
1021}
1022
1023/// Render a chain of `-> command args ...` constraints for display purposes.
1024/// Shared between [`DataValue::Definition`] constraint chains.
1025fn format_constraint_chain(constraints: &[Constraint]) -> String {
1026    constraints
1027        .iter()
1028        .map(|row| format_constraint_as_source(&row.command, &row.args))
1029        .collect::<Vec<_>>()
1030        .join(" -> ")
1031}
1032
1033impl fmt::Display for DataValue {
1034    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1035        match self {
1036            DataValue::Definition {
1037                base,
1038                constraints,
1039                value,
1040            } => {
1041                if base.is_none() && constraints.is_none() {
1042                    return match value {
1043                        Some(v) => write!(f, "{}", v),
1044                        None => Ok(()),
1045                    };
1046                }
1047                let base_str = match base.as_ref() {
1048                    Some(b) => format!("{b}"),
1049                    None => match value {
1050                        Some(v) => {
1051                            if let Some(ref constraints_vec) = constraints {
1052                                let constraint_str = format_constraint_chain(constraints_vec);
1053                                return write!(f, "{v} -> {constraint_str}");
1054                            }
1055                            return write!(f, "{v}");
1056                        }
1057                        None => String::new(),
1058                    },
1059                };
1060                if let Some(ref constraints_vec) = constraints {
1061                    let constraint_str = format_constraint_chain(constraints_vec);
1062                    write!(f, "{base_str} -> {constraint_str}")
1063                } else {
1064                    write!(f, "{base_str}")
1065                }
1066            }
1067            DataValue::Import {
1068                spec_ref,
1069                bindings: _,
1070            } => {
1071                write!(f, "uses {}", spec_ref)
1072            }
1073        }
1074    }
1075}
1076
1077impl LemmaData {
1078    #[must_use]
1079    pub fn new(reference: Reference, value: DataValue, source_location: Source) -> Self {
1080        Self {
1081            reference,
1082            value,
1083            source_location,
1084        }
1085    }
1086}
1087
1088impl LemmaSpec {
1089    #[must_use]
1090    pub fn new(name: String) -> Self {
1091        Self {
1092            name: ascii_lowercase_logical_name(name),
1093            effective_from: EffectiveDate::Origin,
1094            source_type: None,
1095            start_line: 1,
1096            commentary: None,
1097            data: Vec::new(),
1098            rules: Vec::new(),
1099            meta_fields: Vec::new(),
1100        }
1101    }
1102
1103    /// Temporal range start. Origin (None) means −∞.
1104    pub fn effective_from(&self) -> Option<&DateTimeValue> {
1105        self.effective_from.as_ref()
1106    }
1107
1108    #[must_use]
1109    pub fn with_source_type(mut self, source_type: crate::parsing::source::SourceType) -> Self {
1110        self.source_type = Some(source_type);
1111        self
1112    }
1113
1114    #[must_use]
1115    pub fn with_start_line(mut self, start_line: usize) -> Self {
1116        self.start_line = start_line;
1117        self
1118    }
1119
1120    #[must_use]
1121    pub fn set_commentary(mut self, commentary: String) -> Self {
1122        self.commentary = Some(commentary);
1123        self
1124    }
1125
1126    #[must_use]
1127    pub fn add_data(mut self, data: LemmaData) -> Self {
1128        self.data.push(data);
1129        self
1130    }
1131
1132    #[must_use]
1133    pub fn add_rule(mut self, rule: LemmaRule) -> Self {
1134        self.rules.push(rule);
1135        self
1136    }
1137
1138    #[must_use]
1139    pub fn add_meta_field(mut self, meta: MetaField) -> Self {
1140        self.meta_fields.push(meta);
1141        self
1142    }
1143}
1144
1145impl fmt::Display for LemmaSpec {
1146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1147        write!(f, "spec {}", self.name)?;
1148        if let EffectiveDate::DateTimeValue(ref af) = self.effective_from {
1149            write!(f, " {}", af)?;
1150        }
1151        writeln!(f)?;
1152
1153        if let Some(ref commentary) = self.commentary {
1154            writeln!(f, "\"\"\"")?;
1155            writeln!(f, "{}", commentary)?;
1156            writeln!(f, "\"\"\"")?;
1157        }
1158
1159        if !self.data.is_empty() {
1160            writeln!(f)?;
1161            for data in &self.data {
1162                write!(f, "{}", data)?;
1163            }
1164        }
1165
1166        if !self.rules.is_empty() {
1167            writeln!(f)?;
1168            for (index, rule) in self.rules.iter().enumerate() {
1169                if index > 0 {
1170                    writeln!(f)?;
1171                }
1172                write!(f, "{}", rule)?;
1173            }
1174        }
1175
1176        if !self.meta_fields.is_empty() {
1177            writeln!(f)?;
1178            for meta in &self.meta_fields {
1179                writeln!(f, "{}", meta)?;
1180            }
1181        }
1182
1183        Ok(())
1184    }
1185}
1186
1187impl fmt::Display for LemmaData {
1188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1189        writeln!(f, "data {}: {}", self.reference, self.value)
1190    }
1191}
1192
1193impl fmt::Display for LemmaRule {
1194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1195        write!(f, "rule {}: {}", self.name, self.expression)?;
1196        for unless_clause in &self.unless_clauses {
1197            write!(
1198                f,
1199                "\n  unless {} then {}",
1200                unless_clause.condition, unless_clause.result
1201            )?;
1202        }
1203        writeln!(f)?;
1204        Ok(())
1205    }
1206}
1207
1208/// Precedence level for an expression kind.
1209///
1210/// Higher values bind tighter. Used by `Expression::Display` and the formatter
1211/// to insert parentheses only where needed.
1212///
1213/// `...` binds tighter than prefix operators, so `lower 5...10` is `lower` of the
1214/// range and prints without parentheses. A prefix that is itself a range endpoint
1215/// prints in parentheses: `(lower start)...(upper start)`.
1216/// Prefix operators bind tighter than `as` and arithmetic.
1217/// Parser climb in [`crate::parsing::parser::Parser`] must match this table.
1218pub fn expression_precedence(kind: &ExpressionKind) -> u8 {
1219    match kind {
1220        ExpressionKind::LogicalAnd(..) => 2,
1221        ExpressionKind::LogicalNegation(..) => 3,
1222        ExpressionKind::Comparison(..) | ExpressionKind::ResultIsVeto(..) => 4,
1223        ExpressionKind::RangeContainment(..) => 4,
1224        ExpressionKind::DateRelative(..) | ExpressionKind::DateCalendar(..) => 4,
1225        ExpressionKind::Arithmetic(_, op, _) => arithmetic_precedence(op),
1226        ExpressionKind::UnitConversion(..) => 8,
1227        ExpressionKind::MathematicalComputation(..) | ExpressionKind::RangeBound(..) => 9,
1228        ExpressionKind::RangeLiteral(..)
1229        | ExpressionKind::PastFutureRange(..)
1230        | ExpressionKind::Literal(..)
1231        | ExpressionKind::Reference(..)
1232        | ExpressionKind::Now
1233        | ExpressionKind::Veto(..) => 10,
1234    }
1235}
1236
1237/// Precedence for an arithmetic operator. Must match [`expression_precedence`].
1238pub fn arithmetic_precedence(op: &ArithmeticComputation) -> u8 {
1239    match op {
1240        ArithmeticComputation::Add | ArithmeticComputation::Subtract => 5,
1241        ArithmeticComputation::Multiply
1242        | ArithmeticComputation::Divide
1243        | ArithmeticComputation::Modulo => 6,
1244        ArithmeticComputation::Power => 7,
1245    }
1246}
1247
1248/// Operand position under a parent operator.
1249#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1250pub enum OperandSide {
1251    Left,
1252    Right,
1253}
1254
1255/// Associativity of a binary (or n-ary chain) operator.
1256#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1257pub enum Associativity {
1258    Left,
1259    Right,
1260}
1261
1262/// Whether a child expression must be wrapped in parentheses when printed under a parent.
1263///
1264/// - `parent_assoc == None`: unary / prefix parent — wrap only when `child_prec < parent_prec`.
1265/// - `Some(Left)`: wrap looser children, and same-prec **right** children.
1266/// - `Some(Right)`: wrap looser children, and same-prec **left** children.
1267pub fn operand_needs_parentheses(
1268    child_prec: u8,
1269    parent_prec: u8,
1270    side: OperandSide,
1271    parent_assoc: Option<Associativity>,
1272) -> bool {
1273    if child_prec < parent_prec {
1274        return true;
1275    }
1276    if child_prec > parent_prec {
1277        return false;
1278    }
1279    match parent_assoc {
1280        None => false,
1281        Some(Associativity::Left) => matches!(side, OperandSide::Right),
1282        Some(Associativity::Right) => matches!(side, OperandSide::Left),
1283    }
1284}
1285
1286pub fn arithmetic_associativity(op: &ArithmeticComputation) -> Associativity {
1287    match op {
1288        ArithmeticComputation::Power => Associativity::Right,
1289        ArithmeticComputation::Add
1290        | ArithmeticComputation::Subtract
1291        | ArithmeticComputation::Multiply
1292        | ArithmeticComputation::Divide
1293        | ArithmeticComputation::Modulo => Associativity::Left,
1294    }
1295}
1296
1297fn write_expression_child(
1298    f: &mut fmt::Formatter<'_>,
1299    child: &Expression,
1300    parent_prec: u8,
1301    side: OperandSide,
1302    parent_assoc: Option<Associativity>,
1303) -> fmt::Result {
1304    let child_prec = expression_precedence(&child.kind);
1305    if operand_needs_parentheses(child_prec, parent_prec, side, parent_assoc) {
1306        write!(f, "({})", child)
1307    } else {
1308        write!(f, "{}", child)
1309    }
1310}
1311
1312impl fmt::Display for Expression {
1313    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1314        match &self.kind {
1315            ExpressionKind::Literal(lit) => write!(f, "{}", AsLemmaSource(lit)),
1316            ExpressionKind::Reference(r) => write!(f, "{}", r),
1317            ExpressionKind::Arithmetic(left, op, right) => {
1318                let my_prec = expression_precedence(&self.kind);
1319                let assoc = Some(arithmetic_associativity(op));
1320                write_expression_child(f, left, my_prec, OperandSide::Left, assoc)?;
1321                write!(f, " {} ", op)?;
1322                write_expression_child(f, right, my_prec, OperandSide::Right, assoc)
1323            }
1324            ExpressionKind::Comparison(left, op, right) => {
1325                let my_prec = expression_precedence(&self.kind);
1326                write_expression_child(f, left, my_prec, OperandSide::Left, None)?;
1327                write!(f, " {} ", op)?;
1328                write_expression_child(f, right, my_prec, OperandSide::Right, None)
1329            }
1330            ExpressionKind::UnitConversion(value, target) => {
1331                let my_prec = expression_precedence(&self.kind);
1332                write_expression_child(f, value, my_prec, OperandSide::Left, None)?;
1333                write!(f, " as {}", target)
1334            }
1335            ExpressionKind::LogicalNegation(expr, negation) => {
1336                if let (NegationType::Not, ExpressionKind::ResultIsVeto(operand)) =
1337                    (negation, &expr.kind)
1338                {
1339                    let my_prec = expression_precedence(&self.kind);
1340                    write_expression_child(f, operand, my_prec, OperandSide::Left, None)?;
1341                    write!(f, " is not veto")
1342                } else {
1343                    let my_prec = expression_precedence(&self.kind);
1344                    write!(f, "not ")?;
1345                    write_expression_child(f, expr, my_prec, OperandSide::Right, None)
1346                }
1347            }
1348            ExpressionKind::ResultIsVeto(operand) => {
1349                let my_prec = expression_precedence(&self.kind);
1350                write_expression_child(f, operand, my_prec, OperandSide::Left, None)?;
1351                write!(f, " is veto")
1352            }
1353            ExpressionKind::LogicalAnd(left, right) => {
1354                let my_prec = expression_precedence(&self.kind);
1355                let assoc = Some(Associativity::Left);
1356                write_expression_child(f, left, my_prec, OperandSide::Left, assoc)?;
1357                write!(f, " and ")?;
1358                write_expression_child(f, right, my_prec, OperandSide::Right, assoc)
1359            }
1360            ExpressionKind::MathematicalComputation(op, operand) => {
1361                let my_prec = expression_precedence(&self.kind);
1362                write!(f, "{op} ")?;
1363                write_expression_child(f, operand, my_prec, OperandSide::Right, None)
1364            }
1365            ExpressionKind::RangeBound(bound, operand) => {
1366                let my_prec = expression_precedence(&self.kind);
1367                write!(f, "{bound} ")?;
1368                write_expression_child(f, operand, my_prec, OperandSide::Right, None)
1369            }
1370            ExpressionKind::Veto(veto) => match &veto.message {
1371                Some(msg) => write!(f, "veto {}", quote_lemma_text(msg)),
1372                None => write!(f, "veto"),
1373            },
1374            ExpressionKind::Now => write!(f, "now"),
1375            ExpressionKind::DateRelative(kind, date_expr) => {
1376                write!(f, "{} {}", date_expr, kind)?;
1377                Ok(())
1378            }
1379            ExpressionKind::DateCalendar(kind, unit, date_expr) => {
1380                write!(f, "{} {} {}", date_expr, kind, unit)
1381            }
1382            ExpressionKind::RangeLiteral(left, right) => {
1383                let my_prec = expression_precedence(&self.kind);
1384                write_expression_child(f, left, my_prec, OperandSide::Left, None)?;
1385                write!(f, "...")?;
1386                write_expression_child(f, right, my_prec, OperandSide::Right, None)
1387            }
1388            ExpressionKind::PastFutureRange(kind, offset_expr) => {
1389                match kind {
1390                    DateRelativeKind::InPast => write!(f, "past ")?,
1391                    DateRelativeKind::InFuture => write!(f, "future ")?,
1392                }
1393                let my_prec = expression_precedence(&self.kind);
1394                write_expression_child(f, offset_expr, my_prec, OperandSide::Right, None)
1395            }
1396            ExpressionKind::RangeContainment(value, range) => {
1397                let my_prec = expression_precedence(&self.kind);
1398                write_expression_child(f, value, my_prec, OperandSide::Left, None)?;
1399                write!(f, " in ")?;
1400                write_expression_child(f, range, my_prec, OperandSide::Right, None)
1401            }
1402        }
1403    }
1404}
1405
1406impl fmt::Display for ConversionTarget {
1407    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1408        match self {
1409            ConversionTarget::Type(kind) => write!(f, "{kind}"),
1410            ConversionTarget::Unit { unit_name } => write!(f, "{unit_name}"),
1411        }
1412    }
1413}
1414
1415impl fmt::Display for ArithmeticComputation {
1416    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1417        match self {
1418            ArithmeticComputation::Add => write!(f, "+"),
1419            ArithmeticComputation::Subtract => write!(f, "-"),
1420            ArithmeticComputation::Multiply => write!(f, "*"),
1421            ArithmeticComputation::Divide => write!(f, "/"),
1422            ArithmeticComputation::Modulo => write!(f, "%"),
1423            ArithmeticComputation::Power => write!(f, "^"),
1424        }
1425    }
1426}
1427
1428impl fmt::Display for ComparisonComputation {
1429    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1430        match self {
1431            ComparisonComputation::GreaterThan => write!(f, ">"),
1432            ComparisonComputation::LessThan => write!(f, "<"),
1433            ComparisonComputation::GreaterThanOrEqual => write!(f, ">="),
1434            ComparisonComputation::LessThanOrEqual => write!(f, "<="),
1435            ComparisonComputation::Is => write!(f, "is"),
1436            ComparisonComputation::IsNot => write!(f, "is not"),
1437        }
1438    }
1439}
1440
1441impl fmt::Display for MathematicalComputation {
1442    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1443        match self {
1444            MathematicalComputation::Sqrt => write!(f, "sqrt"),
1445            MathematicalComputation::Sin => write!(f, "sin"),
1446            MathematicalComputation::Cos => write!(f, "cos"),
1447            MathematicalComputation::Tan => write!(f, "tan"),
1448            MathematicalComputation::Asin => write!(f, "asin"),
1449            MathematicalComputation::Acos => write!(f, "acos"),
1450            MathematicalComputation::Atan => write!(f, "atan"),
1451            MathematicalComputation::Log => write!(f, "log"),
1452            MathematicalComputation::Exp => write!(f, "exp"),
1453            MathematicalComputation::Abs => write!(f, "abs"),
1454            MathematicalComputation::Floor => write!(f, "floor"),
1455            MathematicalComputation::Ceil => write!(f, "ceil"),
1456            MathematicalComputation::Round => write!(f, "round"),
1457        }
1458    }
1459}
1460
1461// -----------------------------------------------------------------------------
1462// Primitive type kinds and parent type references
1463// -----------------------------------------------------------------------------
1464
1465/// Built-in primitive type kind. Single source of truth for type keywords.
1466#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1467#[serde(rename_all = "snake_case")]
1468pub enum PrimitiveKind {
1469    Boolean,
1470    Measure,
1471    MeasureRange,
1472    Number,
1473    NumberRange,
1474    Ratio,
1475    RatioRange,
1476    Text,
1477    Date,
1478    DateRange,
1479    Time,
1480    TimeRange,
1481}
1482
1483impl std::fmt::Display for PrimitiveKind {
1484    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1485        let s = match self {
1486            PrimitiveKind::Boolean => "boolean",
1487            PrimitiveKind::Measure => "measure",
1488            PrimitiveKind::MeasureRange => "measure range",
1489            PrimitiveKind::Number => "number",
1490            PrimitiveKind::NumberRange => "number range",
1491            PrimitiveKind::Ratio => "ratio",
1492            PrimitiveKind::RatioRange => "ratio range",
1493            PrimitiveKind::Text => "text",
1494            PrimitiveKind::Date => "date",
1495            PrimitiveKind::DateRange => "date range",
1496            PrimitiveKind::Time => "time",
1497            PrimitiveKind::TimeRange => "time range",
1498        };
1499        write!(f, "{}", s)
1500    }
1501}
1502
1503/// Parent type in a type definition: built-in primitive or custom type name.
1504///
1505/// `name` is the declared type name (the data name that introduces this type).
1506/// For `data temperature: measure`, name = "temperature", primitive = Measure.
1507#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1508pub enum ParentType {
1509    Primitive {
1510        primitive: PrimitiveKind,
1511    },
1512    Custom {
1513        name: String,
1514    },
1515    /// Parent type defined in another spec: `spec_alias.inner` (e.g. `data x: finance.money`).
1516    /// `inner` must be [`ParentType::Primitive`] or [`ParentType::Custom`], not nested [`ParentType::Qualified`].
1517    Qualified {
1518        spec_alias: String,
1519        inner: Box<ParentType>,
1520    },
1521    /// Range over an element type: `<inner> range` (e.g. `money range`, `date range`).
1522    Ranged {
1523        inner: Box<ParentType>,
1524    },
1525}
1526
1527impl std::fmt::Display for ParentType {
1528    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1529        match self {
1530            ParentType::Primitive { primitive } => write!(f, "{}", primitive),
1531            ParentType::Custom { name } => write!(f, "{}", name),
1532            ParentType::Qualified { spec_alias, inner } => {
1533                write!(f, "{spec_alias}.{inner}")
1534            }
1535            ParentType::Ranged { inner } => write!(f, "{inner} range"),
1536        }
1537    }
1538}
1539
1540// =============================================================================
1541// AsLemmaSource<Value> — canonical literal formatting
1542// =============================================================================
1543
1544/// Wrap a value to emit canonical Lemma source (round-trippable). See module docs.
1545pub struct AsLemmaSource<'a, T: ?Sized>(pub &'a T);
1546
1547/// Escape a string and wrap it in double quotes for Lemma source output.
1548/// Handles `\` and `"` escaping.
1549pub fn quote_lemma_text(s: &str) -> String {
1550    let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
1551    format!("\"{}\"", escaped)
1552}
1553
1554/// Format a Decimal for Lemma source, preserving precision (trailing zeros).
1555/// Strips the fractional part only when it is zero (e.g. `100` stays `"100"`,
1556/// `1.00` stays `"1.00"`). Inserts underscore separators in the integer part
1557/// when it has 4+ digits (e.g. `30000000.50` → `"30_000_000.50"`).
1558fn format_decimal_source(n: &Decimal) -> String {
1559    let raw = if n.fract().is_zero() {
1560        n.trunc().to_string()
1561    } else {
1562        n.to_string()
1563    };
1564    group_digits(&raw)
1565}
1566
1567/// Insert `_` every 3 digits in the integer part of a numeric string.
1568/// Handles optional leading `-`/`+` sign and optional fractional part.
1569/// Only groups when the integer part has 4 or more digits.
1570fn group_digits(s: &str) -> String {
1571    let (sign, rest) = if s.starts_with('-') || s.starts_with('+') {
1572        (&s[..1], &s[1..])
1573    } else {
1574        ("", s)
1575    };
1576
1577    let (int_part, frac_part) = match rest.find('.') {
1578        Some(pos) => (&rest[..pos], &rest[pos..]),
1579        None => (rest, ""),
1580    };
1581
1582    if int_part.len() < 4 {
1583        return s.to_string();
1584    }
1585
1586    let mut grouped = String::with_capacity(int_part.len() + int_part.len() / 3);
1587    for (i, ch) in int_part.chars().enumerate() {
1588        let digits_remaining = int_part.len() - i;
1589        if i > 0 && digits_remaining % 3 == 0 {
1590            grouped.push('_');
1591        }
1592        grouped.push(ch);
1593    }
1594
1595    format!("{}{}{}", sign, grouped, frac_part)
1596}
1597
1598impl<'a> fmt::Display for AsLemmaSource<'a, CommandArg> {
1599    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1600        use crate::literals::Value;
1601        match self.0 {
1602            CommandArg::Literal(Value::Text(s)) => write!(f, "{}", quote_lemma_text(s)),
1603            CommandArg::Literal(Value::Number(d)) => {
1604                write!(f, "{}", group_digits(&d.to_string()))
1605            }
1606            CommandArg::Literal(Value::Boolean(bv)) => write!(f, "{}", bv),
1607            CommandArg::Literal(Value::NumberWithUnit(d, unit)) => {
1608                write!(f, "{} {}", group_digits(&d.to_string()), unit)
1609            }
1610            CommandArg::Literal(value @ Value::Range(_, _)) => {
1611                write!(f, "{}", AsLemmaSource(value))
1612            }
1613            CommandArg::Literal(Value::Date(dt)) => write!(f, "{}", dt),
1614            CommandArg::Literal(Value::Time(t)) => write!(f, "{}", t),
1615            CommandArg::Label(s) => write!(f, "{}", s),
1616            CommandArg::UnitExpr(unit_arg) => write!(f, "{}", unit_arg),
1617        }
1618    }
1619}
1620
1621/// Format `command key: value` for assignment continuations (`unit`, `with`).
1622pub(crate) fn format_assignment_continuation(
1623    command: &str,
1624    key: &str,
1625    value: &impl fmt::Display,
1626) -> String {
1627    format!("{} {}: {}", command, key, value)
1628}
1629
1630/// Format a single constraint command and its args as valid Lemma source.
1631pub(crate) fn format_constraint_as_source(
1632    cmd: &TypeConstraintCommand,
1633    args: &[CommandArg],
1634) -> String {
1635    if *cmd == TypeConstraintCommand::Unit {
1636        let Some(CommandArg::Label(name)) = args.first() else {
1637            return cmd.to_string();
1638        };
1639        let Some(CommandArg::UnitExpr(unit_arg)) = args.get(1) else {
1640            return format!("{} {}", cmd, name);
1641        };
1642        return format_assignment_continuation("unit", name, unit_arg);
1643    }
1644
1645    if args.is_empty() {
1646        cmd.to_string()
1647    } else {
1648        let args_str: Vec<String> = args
1649            .iter()
1650            .map(|a| format!("{}", AsLemmaSource(a)))
1651            .collect();
1652        format!("{} {}", cmd, args_str.join(" "))
1653    }
1654}
1655
1656/// Format a constraint list as valid Lemma source.
1657/// Returns the `cmd arg -> cmd arg` portion joined by `separator`.
1658fn format_constraints_as_source(constraints: &[Constraint], separator: &str) -> String {
1659    constraints
1660        .iter()
1661        .map(|row| format_constraint_as_source(&row.command, &row.args))
1662        .collect::<Vec<_>>()
1663        .join(separator)
1664}
1665
1666// -- Display for AsLemmaSource<Value> ----------------------------------------
1667
1668impl<'a> fmt::Display for AsLemmaSource<'a, Value> {
1669    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1670        match self.0 {
1671            Value::Number(n) => write!(f, "{}", format_decimal_source(n)),
1672            Value::Text(s) => write!(f, "{}", quote_lemma_text(s)),
1673            Value::Date(dt) => match dt.granularity {
1674                crate::literals::DateGranularity::Year => write!(f, "{:04}", dt.year),
1675                crate::literals::DateGranularity::YearMonth => {
1676                    write!(f, "{:04}-{:02}", dt.year, dt.month)
1677                }
1678                crate::literals::DateGranularity::IsoWeek { iso_year, week } => {
1679                    write!(f, "{:04}-W{:02}", iso_year, week)
1680                }
1681                crate::literals::DateGranularity::Full => {
1682                    write!(f, "{:04}-{:02}-{:02}", dt.year, dt.month, dt.day)
1683                }
1684                crate::literals::DateGranularity::DateTime => {
1685                    write!(
1686                        f,
1687                        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
1688                        dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second
1689                    )?;
1690                    if let Some(tz) = &dt.timezone {
1691                        write!(f, "{}", tz)?;
1692                    }
1693                    Ok(())
1694                }
1695            },
1696            Value::Time(t) => {
1697                write!(f, "{:02}:{:02}:{:02}", t.hour, t.minute, t.second)?;
1698                if let Some(tz) = &t.timezone {
1699                    write!(f, "{}", tz)?;
1700                }
1701                Ok(())
1702            }
1703            Value::Boolean(b) => write!(f, "{}", b),
1704            Value::NumberWithUnit(n, u) => match u.as_str() {
1705                "percent" => write!(f, "{}%", format_decimal_source(n)),
1706                "permille" => write!(f, "{}%%", format_decimal_source(n)),
1707                unit => write!(f, "{} {}", format_decimal_source(n), unit),
1708            },
1709            Value::Range(left, right) => {
1710                write!(
1711                    f,
1712                    "{}...{}",
1713                    AsLemmaSource(left.as_ref()),
1714                    AsLemmaSource(right.as_ref())
1715                )
1716            }
1717        }
1718    }
1719}
1720
1721// -- AsLemmaSource: DataValue (formatter / round-trip) ---
1722
1723impl<'a> fmt::Display for AsLemmaSource<'a, DataValue> {
1724    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1725        match self.0 {
1726            DataValue::Definition {
1727                base,
1728                constraints,
1729                value,
1730            } => {
1731                if base.is_none() && constraints.is_none() {
1732                    if let Some(v) = value {
1733                        return write!(f, "{}", AsLemmaSource(v));
1734                    }
1735                }
1736                let base_str = match base.as_ref() {
1737                    Some(b) => format!("{}", b),
1738                    None => match value {
1739                        Some(v) => {
1740                            if let Some(ref constraints_vec) = constraints {
1741                                let constraint_str =
1742                                    format_constraints_as_source(constraints_vec, " -> ");
1743                                return write!(f, "{} -> {}", AsLemmaSource(v), constraint_str);
1744                            }
1745                            return write!(f, "{}", AsLemmaSource(v));
1746                        }
1747                        None => String::new(),
1748                    },
1749                };
1750                if let Some(ref constraints_vec) = constraints {
1751                    let constraint_str = format_constraints_as_source(constraints_vec, " -> ");
1752                    write!(f, "{} -> {}", base_str, constraint_str)
1753                } else {
1754                    write!(f, "{}", base_str)
1755                }
1756            }
1757            DataValue::Import {
1758                spec_ref,
1759                bindings: _,
1760            } => {
1761                write!(f, "uses {}", spec_ref)
1762            }
1763        }
1764    }
1765}
1766
1767pub(crate) fn canonicalize_value(value: &mut Value) {
1768    if let Value::NumberWithUnit(_, unit) = value {
1769        *unit = ascii_lowercase_logical_name(std::mem::take(unit));
1770    }
1771}
1772
1773pub(crate) fn canonicalize_reference(reference: &mut Reference) {
1774    for segment in &mut reference.segments {
1775        *segment = ascii_lowercase_logical_name(std::mem::take(segment));
1776    }
1777    reference.name = ascii_lowercase_logical_name(std::mem::take(&mut reference.name));
1778}
1779
1780pub(crate) fn canonicalize_spec_ref(spec_ref: &mut SpecRef) {
1781    spec_ref.name = ascii_lowercase_logical_name(std::mem::take(&mut spec_ref.name));
1782    if let Some(qualifier) = spec_ref.repository.as_mut() {
1783        qualifier.name = ascii_lowercase_logical_name(std::mem::take(&mut qualifier.name));
1784    }
1785}
1786
1787pub(crate) fn canonicalize_parent_type(parent: &mut ParentType) {
1788    match parent {
1789        ParentType::Custom { name } => {
1790            *name = ascii_lowercase_logical_name(std::mem::take(name));
1791        }
1792        ParentType::Qualified { spec_alias, inner } => {
1793            *spec_alias = ascii_lowercase_logical_name(std::mem::take(spec_alias));
1794            canonicalize_parent_type(inner);
1795        }
1796        ParentType::Ranged { inner } => {
1797            canonicalize_parent_type(inner);
1798        }
1799        ParentType::Primitive { .. } => {}
1800    }
1801}
1802
1803pub(crate) fn canonicalize_unit_factor(factor: &mut UnitFactor) {
1804    factor.measure_ref = ascii_lowercase_logical_name(std::mem::take(&mut factor.measure_ref));
1805}
1806
1807pub(crate) fn canonicalize_unit_arg(unit_arg: &mut UnitArg) {
1808    if let UnitArg::Expr(_, factors) = unit_arg {
1809        for factor in factors {
1810            canonicalize_unit_factor(factor);
1811        }
1812    }
1813}
1814
1815pub(crate) fn canonicalize_command_arg(command_arg: &mut CommandArg) {
1816    match command_arg {
1817        CommandArg::Literal(value) => canonicalize_value(value),
1818        CommandArg::Label(label) => {
1819            *label = ascii_lowercase_logical_name(std::mem::take(label));
1820        }
1821        CommandArg::UnitExpr(unit_arg) => canonicalize_unit_arg(unit_arg),
1822    }
1823}
1824
1825pub(crate) fn canonicalize_constraints(constraints: &mut [Constraint]) {
1826    for row in constraints {
1827        for arg in &mut row.args {
1828            canonicalize_command_arg(arg);
1829        }
1830    }
1831}
1832
1833pub(crate) fn canonicalize_expression(expression: &mut Expression) {
1834    match &mut expression.kind {
1835        ExpressionKind::Literal(value) => canonicalize_value(value),
1836        ExpressionKind::Reference(reference) => canonicalize_reference(reference),
1837        ExpressionKind::Now => {}
1838        ExpressionKind::DateRelative(_, expression) => {
1839            canonicalize_expression(Arc::make_mut(expression));
1840        }
1841        ExpressionKind::DateCalendar(_, _, expression) => {
1842            canonicalize_expression(Arc::make_mut(expression));
1843        }
1844        ExpressionKind::RangeLiteral(left, right) => {
1845            canonicalize_expression(Arc::make_mut(left));
1846            canonicalize_expression(Arc::make_mut(right));
1847        }
1848        ExpressionKind::PastFutureRange(_, expression) => {
1849            canonicalize_expression(Arc::make_mut(expression));
1850        }
1851        ExpressionKind::RangeContainment(value, range) => {
1852            canonicalize_expression(Arc::make_mut(value));
1853            canonicalize_expression(Arc::make_mut(range));
1854        }
1855        ExpressionKind::LogicalAnd(left, right) => {
1856            canonicalize_expression(Arc::make_mut(left));
1857            canonicalize_expression(Arc::make_mut(right));
1858        }
1859        ExpressionKind::Arithmetic(left, _, right) => {
1860            canonicalize_expression(Arc::make_mut(left));
1861            canonicalize_expression(Arc::make_mut(right));
1862        }
1863        ExpressionKind::Comparison(left, _, right) => {
1864            canonicalize_expression(Arc::make_mut(left));
1865            canonicalize_expression(Arc::make_mut(right));
1866        }
1867        ExpressionKind::UnitConversion(expression, _) => {
1868            canonicalize_expression(Arc::make_mut(expression));
1869        }
1870        ExpressionKind::LogicalNegation(expression, _) => {
1871            canonicalize_expression(Arc::make_mut(expression));
1872        }
1873        ExpressionKind::MathematicalComputation(_, expression)
1874        | ExpressionKind::RangeBound(_, expression) => {
1875            canonicalize_expression(Arc::make_mut(expression));
1876        }
1877        ExpressionKind::Veto(_) => {}
1878        ExpressionKind::ResultIsVeto(expression) => {
1879            canonicalize_expression(Arc::make_mut(expression));
1880        }
1881    }
1882}
1883
1884pub(crate) fn canonicalize_unless_clause(unless_clause: &mut UnlessClause) {
1885    canonicalize_expression(&mut unless_clause.condition);
1886    canonicalize_expression(&mut unless_clause.result);
1887}
1888
1889pub(crate) fn canonicalize_data_value(data_value: &mut DataValue) {
1890    match data_value {
1891        DataValue::Definition {
1892            base,
1893            constraints,
1894            value,
1895        } => {
1896            if let Some(base) = base {
1897                canonicalize_parent_type(base);
1898            }
1899            if let Some(constraints) = constraints {
1900                canonicalize_constraints(constraints);
1901            }
1902            if let Some(value) = value {
1903                canonicalize_value(value);
1904            }
1905        }
1906        DataValue::Import { spec_ref, bindings } => {
1907            canonicalize_spec_ref(spec_ref);
1908            for binding in bindings {
1909                canonicalize_reference(&mut binding.path);
1910                match &mut binding.rhs {
1911                    WithRhs::Literal(value) => canonicalize_value(value),
1912                    WithRhs::Reference { target } => canonicalize_reference(target),
1913                }
1914            }
1915        }
1916    }
1917}
1918
1919pub(crate) fn canonicalize_lemma_data(data: &mut LemmaData) {
1920    canonicalize_reference(&mut data.reference);
1921    canonicalize_data_value(&mut data.value);
1922}
1923
1924pub(crate) fn canonicalize_lemma_rule(rule: &mut LemmaRule) {
1925    rule.name = ascii_lowercase_logical_name(std::mem::take(&mut rule.name));
1926    canonicalize_expression(&mut rule.expression);
1927    for unless_clause in &mut rule.unless_clauses {
1928        canonicalize_unless_clause(unless_clause);
1929    }
1930}
1931
1932pub(crate) fn canonicalize_lemma_spec(spec: &mut LemmaSpec) {
1933    spec.name = ascii_lowercase_logical_name(std::mem::take(&mut spec.name));
1934    for meta in &mut spec.meta_fields {
1935        meta.key = ascii_lowercase_logical_name(std::mem::take(&mut meta.key));
1936    }
1937    for data in &mut spec.data {
1938        canonicalize_lemma_data(data);
1939    }
1940    for rule in &mut spec.rules {
1941        canonicalize_lemma_rule(rule);
1942    }
1943}
1944
1945pub(crate) fn canonicalize_repository(repository: &mut LemmaRepository) {
1946    if let Some(name) = repository.name.take() {
1947        repository.name = Some(ascii_lowercase_logical_name(name));
1948    }
1949}
1950
1951#[cfg(test)]
1952mod tests {
1953    use super::*;
1954    use crate::literals::DateGranularity;
1955
1956    #[test]
1957    fn test_conversion_target_display() {
1958        assert_eq!(
1959            format!("{}", ConversionTarget::Type(PrimitiveKind::Number)),
1960            "number"
1961        );
1962    }
1963
1964    #[test]
1965    fn test_value_number_with_unit_ratio_display() {
1966        use rust_decimal::Decimal;
1967        use std::str::FromStr;
1968        let percent =
1969            Value::NumberWithUnit(Decimal::from_str("10").unwrap(), "percent".to_string());
1970        assert_eq!(format!("{}", percent), "10%");
1971        let permille =
1972            Value::NumberWithUnit(Decimal::from_str("5").unwrap(), "permille".to_string());
1973        assert_eq!(format!("{}", permille), "5%%");
1974    }
1975
1976    #[test]
1977    fn test_datetime_value_display() {
1978        let dt = DateTimeValue {
1979            year: 2024,
1980            month: 12,
1981            day: 25,
1982            hour: 14,
1983            minute: 30,
1984            second: 45,
1985            microsecond: 0,
1986            timezone: Some(TimezoneValue {
1987                offset_hours: 1,
1988                offset_minutes: 0,
1989            }),
1990
1991            granularity: DateGranularity::DateTime,
1992        };
1993        assert_eq!(format!("{}", dt), "2024-12-25T14:30:45+01:00");
1994    }
1995
1996    #[test]
1997    fn test_datetime_value_display_date_only() {
1998        let dt = DateTimeValue {
1999            year: 2026,
2000            month: 3,
2001            day: 4,
2002            hour: 0,
2003            minute: 0,
2004            second: 0,
2005            microsecond: 0,
2006            timezone: None,
2007
2008            granularity: DateGranularity::Full,
2009        };
2010        assert_eq!(format!("{}", dt), "2026-03-04");
2011    }
2012
2013    #[test]
2014    fn test_datetime_value_display_microseconds() {
2015        let dt = DateTimeValue {
2016            year: 2026,
2017            month: 2,
2018            day: 23,
2019            hour: 14,
2020            minute: 30,
2021            second: 45,
2022            microsecond: 123456,
2023            timezone: Some(TimezoneValue {
2024                offset_hours: 0,
2025                offset_minutes: 0,
2026            }),
2027
2028            granularity: DateGranularity::DateTime,
2029        };
2030        assert_eq!(format!("{}", dt), "2026-02-23T14:30:45.123456Z");
2031    }
2032
2033    #[test]
2034    fn test_datetime_microsecond_in_ordering() {
2035        let a = DateTimeValue {
2036            year: 2026,
2037            month: 1,
2038            day: 1,
2039            hour: 0,
2040            minute: 0,
2041            second: 0,
2042            microsecond: 100,
2043            timezone: None,
2044
2045            granularity: DateGranularity::DateTime,
2046        };
2047        let b = DateTimeValue {
2048            year: 2026,
2049            month: 1,
2050            day: 1,
2051            hour: 0,
2052            minute: 0,
2053            second: 0,
2054            microsecond: 200,
2055            timezone: None,
2056
2057            granularity: DateGranularity::DateTime,
2058        };
2059        assert!(a < b);
2060    }
2061
2062    #[test]
2063    fn test_datetime_parse_iso_week() {
2064        let dt: DateTimeValue = "2026-W01".parse().unwrap();
2065        assert_eq!(dt.year, 2025);
2066        assert_eq!(dt.month, 12);
2067        assert_eq!(dt.day, 29);
2068        assert_eq!(dt.microsecond, 0);
2069        assert_eq!(dt.to_string(), "2026-W01");
2070        assert!(matches!(
2071            dt.granularity,
2072            DateGranularity::IsoWeek {
2073                iso_year: 2026,
2074                week: 1
2075            }
2076        ));
2077    }
2078
2079    #[test]
2080    fn test_negation_types() {
2081        let json = serde_json::to_string(&NegationType::Not).expect("serialize NegationType");
2082        let decoded: NegationType = serde_json::from_str(&json).expect("deserialize NegationType");
2083        assert_eq!(decoded, NegationType::Not);
2084    }
2085
2086    #[test]
2087    fn parent_type_primitive_serde_externally_tagged() {
2088        let p = ParentType::Primitive {
2089            primitive: PrimitiveKind::Number,
2090        };
2091        let json = serde_json::to_string(&p).expect("ParentType::Primitive must serialize");
2092        assert!(json.contains("\"Primitive\"") && json.contains("\"primitive\""));
2093        let back: ParentType = serde_json::from_str(&json).expect("deserialize");
2094        assert_eq!(back, p);
2095    }
2096
2097    // =====================================================================
2098    // DataValue Display — constraint formatting
2099    // =====================================================================
2100
2101    fn text_arg(s: &str) -> CommandArg {
2102        CommandArg::Literal(crate::literals::Value::Text(s.to_string()))
2103    }
2104
2105    fn number_arg(s: &str) -> CommandArg {
2106        let d: rust_decimal::Decimal = s.parse().expect("decimal");
2107        CommandArg::Literal(crate::literals::Value::Number(d))
2108    }
2109
2110    fn boolean_arg(b: BooleanValue) -> CommandArg {
2111        CommandArg::Literal(crate::literals::Value::Boolean(b))
2112    }
2113
2114    fn measure_arg(value: &str, unit: &str) -> CommandArg {
2115        let d: rust_decimal::Decimal = value.parse().expect("decimal");
2116        CommandArg::Literal(crate::literals::Value::NumberWithUnit(d, unit.to_string()))
2117    }
2118
2119    fn duration_arg(value: &str, unit: &str) -> CommandArg {
2120        let d: rust_decimal::Decimal = value.parse().expect("decimal");
2121        CommandArg::Literal(crate::literals::Value::NumberWithUnit(d, unit.to_string()))
2122    }
2123
2124    #[test]
2125    fn as_lemma_source_text_default_is_quoted() {
2126        let fv = DataValue::Definition {
2127            base: Some(ParentType::Primitive {
2128                primitive: PrimitiveKind::Text,
2129            }),
2130            constraints: Some(vec![test_constraint(
2131                TypeConstraintCommand::Suggest,
2132                vec![text_arg("single")],
2133            )]),
2134            value: None,
2135        };
2136        assert_eq!(
2137            format!("{}", AsLemmaSource(&fv)),
2138            "text -> suggest \"single\""
2139        );
2140    }
2141
2142    #[test]
2143    fn as_lemma_source_number_default_not_quoted() {
2144        let fv = DataValue::Definition {
2145            base: Some(ParentType::Primitive {
2146                primitive: PrimitiveKind::Number,
2147            }),
2148            constraints: Some(vec![test_constraint(
2149                TypeConstraintCommand::Suggest,
2150                vec![number_arg("10")],
2151            )]),
2152            value: None,
2153        };
2154        assert_eq!(format!("{}", AsLemmaSource(&fv)), "number -> suggest 10");
2155    }
2156
2157    #[test]
2158    fn as_lemma_source_help_always_quoted() {
2159        let fv = DataValue::Definition {
2160            base: Some(ParentType::Primitive {
2161                primitive: PrimitiveKind::Number,
2162            }),
2163            constraints: Some(vec![test_constraint(
2164                TypeConstraintCommand::Help,
2165                vec![text_arg("Enter a measure")],
2166            )]),
2167            value: None,
2168        };
2169        assert_eq!(
2170            format!("{}", AsLemmaSource(&fv)),
2171            "number -> help \"Enter a measure\""
2172        );
2173    }
2174
2175    #[test]
2176    fn as_lemma_source_text_option_quoted() {
2177        let fv = DataValue::Definition {
2178            base: Some(ParentType::Primitive {
2179                primitive: PrimitiveKind::Text,
2180            }),
2181            constraints: Some(vec![
2182                test_constraint(TypeConstraintCommand::Option, vec![text_arg("active")]),
2183                test_constraint(TypeConstraintCommand::Option, vec![text_arg("inactive")]),
2184            ]),
2185            value: None,
2186        };
2187        assert_eq!(
2188            format!("{}", AsLemmaSource(&fv)),
2189            "text -> option \"active\" -> option \"inactive\""
2190        );
2191    }
2192
2193    #[test]
2194    fn as_lemma_source_measure_unit_not_quoted() {
2195        let fv = DataValue::Definition {
2196            base: Some(ParentType::Primitive {
2197                primitive: PrimitiveKind::Measure,
2198            }),
2199            constraints: Some(vec![
2200                test_constraint(
2201                    TypeConstraintCommand::Unit,
2202                    vec![
2203                        CommandArg::Label("eur".to_string()),
2204                        CommandArg::UnitExpr(UnitArg::Factor(decimal("1.00"))),
2205                    ],
2206                ),
2207                test_constraint(
2208                    TypeConstraintCommand::Unit,
2209                    vec![
2210                        CommandArg::Label("usd".to_string()),
2211                        CommandArg::UnitExpr(UnitArg::Factor(decimal("0.91"))),
2212                    ],
2213                ),
2214            ]),
2215            value: None,
2216        };
2217        assert_eq!(
2218            format!("{}", AsLemmaSource(&fv)),
2219            "measure -> unit eur: 1.00 -> unit usd: 0.91"
2220        );
2221    }
2222
2223    #[test]
2224    fn as_lemma_source_measure_minimum_with_unit() {
2225        let fv = DataValue::Definition {
2226            base: Some(ParentType::Primitive {
2227                primitive: PrimitiveKind::Measure,
2228            }),
2229            constraints: Some(vec![test_constraint(
2230                TypeConstraintCommand::Minimum,
2231                vec![measure_arg("0", "eur")],
2232            )]),
2233            value: None,
2234        };
2235        assert_eq!(
2236            format!("{}", AsLemmaSource(&fv)),
2237            "measure -> minimum 0 eur"
2238        );
2239    }
2240
2241    #[test]
2242    fn as_lemma_source_boolean_default() {
2243        let fv = DataValue::Definition {
2244            base: Some(ParentType::Primitive {
2245                primitive: PrimitiveKind::Boolean,
2246            }),
2247            constraints: Some(vec![test_constraint(
2248                TypeConstraintCommand::Suggest,
2249                vec![boolean_arg(BooleanValue::True)],
2250            )]),
2251            value: None,
2252        };
2253        assert_eq!(format!("{}", AsLemmaSource(&fv)), "boolean -> suggest true");
2254    }
2255
2256    #[test]
2257    fn as_lemma_source_duration_default() {
2258        let fv = DataValue::Definition {
2259            base: Some(ParentType::Custom {
2260                name: "duration".to_string(),
2261            }),
2262            constraints: Some(vec![test_constraint(
2263                TypeConstraintCommand::Suggest,
2264                vec![duration_arg("40", "hour")],
2265            )]),
2266            value: None,
2267        };
2268        assert_eq!(
2269            format!("{}", AsLemmaSource(&fv)),
2270            "duration -> suggest 40 hour"
2271        );
2272    }
2273
2274    #[test]
2275    fn as_lemma_source_named_type_default_quoted() {
2276        // Named types (user-defined): the parser produces a typed Text literal for
2277        // quoted suggestion values like `suggest "single"`.
2278        let fv = DataValue::Definition {
2279            base: Some(ParentType::Custom {
2280                name: "filing_status_type".to_string(),
2281            }),
2282            constraints: Some(vec![test_constraint(
2283                TypeConstraintCommand::Suggest,
2284                vec![text_arg("single")],
2285            )]),
2286            value: None,
2287        };
2288        assert_eq!(
2289            format!("{}", AsLemmaSource(&fv)),
2290            "filing_status_type -> suggest \"single\""
2291        );
2292    }
2293
2294    #[test]
2295    fn as_lemma_source_help_escapes_quotes() {
2296        let fv = DataValue::Definition {
2297            base: Some(ParentType::Primitive {
2298                primitive: PrimitiveKind::Text,
2299            }),
2300            constraints: Some(vec![test_constraint(
2301                TypeConstraintCommand::Help,
2302                vec![text_arg("say \"hello\"")],
2303            )]),
2304            value: None,
2305        };
2306        assert_eq!(
2307            format!("{}", AsLemmaSource(&fv)),
2308            "text -> help \"say \\\"hello\\\"\""
2309        );
2310    }
2311
2312    fn unit_arg_expr(prefix: Decimal, factors: &[(&str, i32)]) -> UnitArg {
2313        UnitArg::Expr(
2314            prefix,
2315            factors
2316                .iter()
2317                .map(|(measure_ref, exp)| UnitFactor {
2318                    measure_ref: (*measure_ref).to_string(),
2319                    exp: *exp,
2320                })
2321                .collect(),
2322        )
2323    }
2324
2325    #[test]
2326    fn unit_arg_display_metre_per_second() {
2327        let arg = unit_arg_expr(Decimal::ONE, &[("meter", 1), ("second", -1)]);
2328        assert_eq!(format!("{arg}"), "meter/second");
2329        assert!(
2330            !format!("{arg}").contains("second^-1"),
2331            "must not print denominator as negative exponent"
2332        );
2333    }
2334
2335    #[test]
2336    fn unit_arg_display_meter_per_second_squared() {
2337        let arg = unit_arg_expr(Decimal::ONE, &[("meter", 1), ("second", -2)]);
2338        assert_eq!(format!("{arg}"), "meter/second^2");
2339    }
2340
2341    #[test]
2342    fn unit_arg_display_kg_times_mps2() {
2343        let arg = unit_arg_expr(Decimal::ONE, &[("kg", 1), ("mps2", 1)]);
2344        assert_eq!(format!("{arg}"), "kg * mps2");
2345    }
2346
2347    #[test]
2348    fn unit_arg_display_numeric_prefix_metre_per_second() {
2349        use std::str::FromStr;
2350        let prefix = Decimal::from_str("3.6").expect("decimal");
2351        let arg = unit_arg_expr(prefix, &[("meter", 1), ("second", -1)]);
2352        assert_eq!(format!("{arg}"), "3.6 meter/second");
2353    }
2354
2355    #[test]
2356    fn unit_arg_display_metre_per_second_times_kg() {
2357        let arg = unit_arg_expr(Decimal::ONE, &[("meter", 1), ("second", -1), ("kg", 1)]);
2358        assert_eq!(format!("{arg}"), "meter/second * kg");
2359    }
2360
2361    #[test]
2362    fn unit_arg_display_kg_meter_per_second_squared() {
2363        let arg = unit_arg_expr(Decimal::ONE, &[("kg", 1), ("meter", 1), ("second", -2)]);
2364        assert_eq!(format!("{arg}"), "kg * meter/second^2");
2365    }
2366
2367    // ─── Assignment continuation formatting (red until formatter lands) ───────
2368
2369    #[test]
2370    fn format_constraint_as_source_unit_factor_uses_assignment_colon() {
2371        let args = vec![
2372            CommandArg::Label("eur".to_string()),
2373            CommandArg::UnitExpr(UnitArg::Factor(decimal("1"))),
2374        ];
2375        assert_eq!(
2376            format_constraint_as_source(&TypeConstraintCommand::Unit, &args),
2377            "unit eur: 1"
2378        );
2379    }
2380
2381    #[test]
2382    fn format_constraint_as_source_unit_compound_uses_assignment_colon() {
2383        let arg = unit_arg_expr(decimal("3.6"), &[("meter", 1), ("second", -1)]);
2384        let args = vec![
2385            CommandArg::Label("kmh".to_string()),
2386            CommandArg::UnitExpr(arg),
2387        ];
2388        assert_eq!(
2389            format_constraint_as_source(&TypeConstraintCommand::Unit, &args),
2390            "unit kmh: 3.6 meter/second"
2391        );
2392    }
2393
2394    #[test]
2395    fn format_constraint_as_source_unit_factor_one_decimal_uses_assignment_colon() {
2396        let args = vec![
2397            CommandArg::Label("eur".to_string()),
2398            CommandArg::UnitExpr(UnitArg::Factor(decimal("1.00"))),
2399        ];
2400        assert_eq!(
2401            format_constraint_as_source(&TypeConstraintCommand::Unit, &args),
2402            "unit eur: 1.00"
2403        );
2404    }
2405
2406    #[test]
2407    fn as_lemma_source_measure_unit_uses_assignment_colon() {
2408        let fv = DataValue::Definition {
2409            base: Some(ParentType::Primitive {
2410                primitive: PrimitiveKind::Measure,
2411            }),
2412            constraints: Some(vec![
2413                test_constraint(
2414                    TypeConstraintCommand::Unit,
2415                    vec![
2416                        CommandArg::Label("eur".to_string()),
2417                        CommandArg::UnitExpr(UnitArg::Factor(decimal("1.00"))),
2418                    ],
2419                ),
2420                test_constraint(
2421                    TypeConstraintCommand::Unit,
2422                    vec![
2423                        CommandArg::Label("usd".to_string()),
2424                        CommandArg::UnitExpr(UnitArg::Factor(decimal("0.91"))),
2425                    ],
2426                ),
2427            ]),
2428            value: None,
2429        };
2430        assert_eq!(
2431            format!("{}", AsLemmaSource(&fv)),
2432            "measure -> unit eur: 1.00 -> unit usd: 0.91"
2433        );
2434    }
2435
2436    fn decimal(value: &str) -> rust_decimal::Decimal {
2437        use std::str::FromStr;
2438        rust_decimal::Decimal::from_str(value).expect("decimal literal in test")
2439    }
2440}