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//! [`MetaValue`], [`DataValue`], and [`CommandArg`] use human-oriented
8//! `Display` (stable for `to_string()`, logs, APIs). [`Expression`] and
9//! [`LemmaRule`]/[`LemmaSpec`] use canonical Lemma source for literals via
10//! [`AsLemmaSource`] around [`Value`]. Wrap [`MetaValue`]/[`DataValue`]
11//! in [`AsLemmaSource`] when emitting 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 workspace-global anonymous grouping. 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 workspace-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 anonymous repositories.
158    pub name: Option<String>,
159    /// Dependency provenance: `None` for workspace 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/// `name` is always the bare spec set name (no `@`, no dots, no slashes). The
254/// owning repository — and, transitively, whether the spec is loaded from a registry
255/// bundle — is preserved through the structural relationship in
256/// [`crate::engine::Context`], not via fields on this structure.
257///
258/// `LemmaSpec` has **no global identity**. There is no `PartialEq`, `Eq`, `Ord`,
259/// or `Hash` implementation. Within one [`crate::engine::Context`], planning
260/// compares Context-owned rows by address (`std::ptr::eq`) or by
261/// `(repository, name, EffectiveDate)`. Outside a live Context, key by that
262/// composite triple.
263#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
264pub struct LemmaSpec {
265    pub name: String,
266    pub effective_from: EffectiveDate,
267    pub source_type: Option<crate::parsing::source::SourceType>,
268    pub start_line: usize,
269    pub commentary: Option<String>,
270    pub data: Vec<LemmaData>,
271    pub rules: Vec<LemmaRule>,
272    pub meta_fields: Vec<MetaField>,
273}
274
275#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
276pub struct MetaField {
277    pub key: String,
278    pub value: MetaValue,
279    pub source_location: Source,
280}
281
282#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
283#[serde(rename_all = "snake_case")]
284pub enum MetaValue {
285    Literal(Value),
286    Unquoted(String),
287}
288
289impl fmt::Display for MetaValue {
290    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291        match self {
292            MetaValue::Literal(v) => write!(f, "{}", v),
293            MetaValue::Unquoted(s) => write!(f, "{}", s),
294        }
295    }
296}
297
298impl fmt::Display for MetaField {
299    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300        write!(f, "meta {}: {}", self.key, self.value)
301    }
302}
303
304#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
305pub struct LemmaData {
306    pub reference: Reference,
307    pub value: DataValue,
308    pub source_location: Source,
309}
310
311/// An unless clause that provides an alternative result
312///
313/// Unless clauses are evaluated in order, and the last matching condition wins.
314/// This matches natural language: "X unless A then Y, unless B then Z" - if both
315/// A and B are true, Z is returned (the last match).
316#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
317pub struct UnlessClause {
318    pub condition: Expression,
319    pub result: Expression,
320    pub source_location: Source,
321}
322
323/// A rule with a single expression and optional unless clauses
324#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
325pub struct LemmaRule {
326    pub name: String,
327    pub expression: Expression,
328    pub unless_clauses: Vec<UnlessClause>,
329    pub source_location: Source,
330}
331
332/// An expression that can be evaluated, with source location
333///
334/// Expressions use semantic equality - two expressions with the same
335/// structure (kind) are equal regardless of source location.
336/// Hash is not implemented for AST Expression; use planning::semantics::Expression as map keys.
337#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
338pub struct Expression {
339    pub kind: ExpressionKind,
340    pub source_location: Option<Source>,
341}
342
343impl Expression {
344    /// Create a new expression with kind and source location
345    #[must_use]
346    pub fn new(kind: ExpressionKind, source_location: Source) -> Self {
347        Self {
348            kind,
349            source_location: Some(source_location),
350        }
351    }
352}
353
354/// Semantic equality - compares expressions by structure only, ignoring source location
355impl PartialEq for Expression {
356    fn eq(&self, other: &Self) -> bool {
357        self.kind == other.kind
358    }
359}
360
361impl Eq for Expression {}
362
363/// Whether a date is relative to `now` in the past or future direction.
364#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
365#[serde(rename_all = "snake_case")]
366pub enum DateRelativeKind {
367    InPast,
368    InFuture,
369}
370
371/// Calendar-period membership checks.
372#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
373#[serde(rename_all = "snake_case")]
374pub enum DateCalendarKind {
375    Current,
376    Past,
377    Future,
378    NotIn,
379}
380
381/// Granularity of a calendar-period check.
382#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
383#[serde(rename_all = "snake_case")]
384pub enum CalendarPeriodUnit {
385    Year,
386    Month,
387    Week,
388}
389
390impl CalendarPeriodUnit {
391    #[must_use]
392    pub fn from_keyword(s: &str) -> Option<Self> {
393        match s.trim().to_lowercase().as_str() {
394            "year" => Some(Self::Year),
395            "month" => Some(Self::Month),
396            "week" => Some(Self::Week),
397            _ => None,
398        }
399    }
400}
401
402impl fmt::Display for DateRelativeKind {
403    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404        match self {
405            DateRelativeKind::InPast => write!(f, "in past"),
406            DateRelativeKind::InFuture => write!(f, "in future"),
407        }
408    }
409}
410
411impl fmt::Display for DateCalendarKind {
412    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
413        match self {
414            DateCalendarKind::Current => write!(f, "in calendar"),
415            DateCalendarKind::Past => write!(f, "in past calendar"),
416            DateCalendarKind::Future => write!(f, "in future calendar"),
417            DateCalendarKind::NotIn => write!(f, "not in calendar"),
418        }
419    }
420}
421
422impl fmt::Display for CalendarPeriodUnit {
423    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
424        match self {
425            CalendarPeriodUnit::Year => write!(f, "year"),
426            CalendarPeriodUnit::Month => write!(f, "month"),
427            CalendarPeriodUnit::Week => write!(f, "week"),
428        }
429    }
430}
431
432/// The kind/type of expression
433#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
434#[serde(rename_all = "snake_case")]
435pub enum ExpressionKind {
436    /// Parse-time literal value (type will be resolved during planning)
437    Literal(Value),
438    /// Unresolved reference (identifier or dot path). Resolved during planning to DataPath or RulePath.
439    Reference(Reference),
440    /// The `now` keyword — resolves to the evaluation datetime (= effective).
441    Now,
442    /// Date-relative sugar: `<date_expr> in past` / `<date_expr> in future`
443    /// Fields: (kind, date_expression)
444    DateRelative(DateRelativeKind, Arc<Expression>),
445    /// Calendar-period sugar: `<date_expr> in [past|future] calendar year|month|week`
446    /// Fields: (kind, unit, date_expression)
447    DateCalendar(DateCalendarKind, CalendarPeriodUnit, Arc<Expression>),
448    /// Range literal: `{left_expr}...{right_expr}`
449    RangeLiteral(Arc<Expression>, Arc<Expression>),
450    /// Relative date range: `past 7 day` / `future 30 day`
451    PastFutureRange(DateRelativeKind, Arc<Expression>),
452    /// Range containment: `{value_expr} in {range_expr}`
453    RangeContainment(Arc<Expression>, Arc<Expression>),
454    LogicalAnd(Arc<Expression>, Arc<Expression>),
455    Arithmetic(Arc<Expression>, ArithmeticComputation, Arc<Expression>),
456    Comparison(Arc<Expression>, ComparisonComputation, Arc<Expression>),
457    UnitConversion(Arc<Expression>, ConversionTarget),
458    LogicalNegation(Arc<Expression>, NegationType),
459    MathematicalComputation(MathematicalComputation, Arc<Expression>),
460    Veto(VetoExpression),
461    /// `expr is veto` / `veto is expr` — boolean: whether evaluating `expr` yields `OperationResult::Veto`.
462    ResultIsVeto(Arc<Expression>),
463}
464
465/// Unresolved reference from parser
466///
467/// Reference to a data or rule (identifier or dot path).
468///
469/// Used in expressions and in LemmaData. During planning, references
470/// are resolved to DataPath or RulePath (semantics layer).
471/// Examples:
472/// - Local "age": segments=[], name="age"
473/// - Cross-spec "employee.salary": segments=["employee"], name="salary"
474#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
475pub struct Reference {
476    pub segments: Vec<String>,
477    pub name: String,
478}
479
480impl Reference {
481    #[must_use]
482    pub fn local(name: String) -> Self {
483        Self {
484            segments: Vec::new(),
485            name: ascii_lowercase_logical_name(name),
486        }
487    }
488
489    #[must_use]
490    pub fn from_path(path: Vec<String>) -> Self {
491        if path.is_empty() {
492            Self {
493                segments: Vec::new(),
494                name: String::new(),
495            }
496        } else {
497            // Safe: path is non-empty.
498            let name = ascii_lowercase_logical_name(path[path.len() - 1].clone());
499            let segments = path[..path.len() - 1]
500                .iter()
501                .map(|segment| ascii_lowercase_logical_name(segment.clone()))
502                .collect();
503            Self { segments, name }
504        }
505    }
506
507    #[must_use]
508    pub fn is_local(&self) -> bool {
509        self.segments.is_empty()
510    }
511
512    #[must_use]
513    pub fn full_path(&self) -> Vec<String> {
514        let mut path = self.segments.clone();
515        path.push(self.name.clone());
516        path
517    }
518}
519
520impl fmt::Display for Reference {
521    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
522        for segment in &self.segments {
523            write!(f, "{}.", segment)?;
524        }
525        write!(f, "{}", self.name)
526    }
527}
528
529/// Arithmetic computations
530#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
531#[serde(rename_all = "snake_case")]
532pub enum ArithmeticComputation {
533    Add,
534    Subtract,
535    Multiply,
536    Divide,
537    Modulo,
538    Power,
539}
540
541/// Comparison computations
542#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
543#[serde(rename_all = "snake_case")]
544pub enum ComparisonComputation {
545    GreaterThan,
546    LessThan,
547    GreaterThanOrEqual,
548    LessThanOrEqual,
549    Is,
550    IsNot,
551}
552
553impl ComparisonComputation {
554    /// Check if this is an equality comparison (`is`)
555    #[must_use]
556    pub fn is_equal(&self) -> bool {
557        matches!(self, ComparisonComputation::Is)
558    }
559
560    /// Check if this is an inequality comparison (`is not`)
561    #[must_use]
562    pub fn is_not_equal(&self) -> bool {
563        matches!(self, ComparisonComputation::IsNot)
564    }
565}
566
567/// The target type for `as` cast expressions (e.g. `as number`, `as eur`).
568#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
569#[serde(rename_all = "snake_case")]
570pub enum ConversionTarget {
571    Type(PrimitiveKind),
572    Unit { unit_name: String },
573}
574
575/// Types of logical negation
576#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
577#[serde(rename_all = "snake_case")]
578pub enum NegationType {
579    Not,
580}
581
582/// A veto expression that prohibits any valid verdict from the rule
583///
584/// A veto prevents the rule from producing any valid result. This is used for
585/// validation and constraint enforcement — distinct from boolean `false`.
586///
587/// Example: `veto "Must be over 18"` - blocks the rule entirely with a message
588#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
589pub struct VetoExpression {
590    pub message: Option<String>,
591}
592
593/// Mathematical computations
594#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
595#[serde(rename_all = "snake_case")]
596pub enum MathematicalComputation {
597    Sqrt,
598    Sin,
599    Cos,
600    Tan,
601    Asin,
602    Acos,
603    Atan,
604    Log,
605    Exp,
606    Abs,
607    Floor,
608    Ceil,
609    Round,
610}
611
612/// A spec reference written in source.
613///
614/// `name` is the bare spec name (no `@`, no dots, no slashes).
615/// [`SpecRef::repository`] is `None` for same-repository references, or
616/// `Some(RepositoryQualifier)` when a repository qualifier was written before the spec name.
617/// `effective` carries an optional explicit pin written next to the spec name.
618#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
619pub struct SpecRef {
620    /// Optional explicit repository qualifier. `None` means the reference resolves against
621    /// the consumer spec's own repository.
622    pub repository: Option<RepositoryQualifier>,
623    /// The spec name.
624    pub name: String,
625    /// Optional explicit effective datetime pin written in source.
626    pub effective: Option<DateTimeValue>,
627    /// Source span of the repository qualifier (when `repository` is present).
628    #[serde(default, skip_serializing_if = "Option::is_none")]
629    pub repository_span: Option<Span>,
630    /// Source span of `name` and optional `effective`.
631    #[serde(default, skip_serializing_if = "Option::is_none")]
632    pub target_span: Option<Span>,
633}
634
635impl std::fmt::Display for SpecRef {
636    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
637        if let Some(qualifier) = &self.repository {
638            write!(f, "{} ", qualifier)?;
639        }
640        write!(f, "{}", self.name)?;
641        if let Some(d) = &self.effective {
642            write!(f, " {}", d)?;
643        }
644        Ok(())
645    }
646}
647
648impl SpecRef {
649    /// Same-repository reference: resolution uses the consumer's repository.
650    pub fn same_repository(name: impl Into<String>) -> Self {
651        Self {
652            name: ascii_lowercase_logical_name(name.into()),
653            repository: None,
654            effective: None,
655            repository_span: None,
656            target_span: None,
657        }
658    }
659
660    /// Cross-repository reference with an explicit repository qualifier.
661    pub fn cross_repository(name: impl Into<String>, qualifier: RepositoryQualifier) -> Self {
662        Self {
663            name: ascii_lowercase_logical_name(name.into()),
664            repository: Some(qualifier),
665            effective: None,
666            repository_span: None,
667            target_span: None,
668        }
669    }
670
671    /// Resolve the effective instant for this reference given the planning slice's `effective`.
672    /// Explicit qualifier on the reference wins; otherwise inherits the slice instant.
673    pub fn at(&self, effective: &EffectiveDate) -> EffectiveDate {
674        self.effective
675            .clone()
676            .map_or_else(|| effective.clone(), EffectiveDate::DateTimeValue)
677    }
678
679    /// Concrete instant for evaluation or navigation: explicit ref pin wins, else consumer bound.
680    /// Returns `None` when neither side names a concrete instant (consumer at Origin, no ref pin).
681    pub fn resolved_instant(
682        &self,
683        consumer_effective_from: Option<&DateTimeValue>,
684    ) -> Option<DateTimeValue> {
685        self.effective
686            .clone()
687            .or_else(|| consumer_effective_from.cloned())
688    }
689}
690
691/// A single factor in a compound unit expression.
692///
693/// `measure_ref` is the name of the referenced unit (e.g. `"meter"`, `"second"`).
694/// `exp` is the integer exponent, positive for numerator and negative for denominator.
695/// For example `meter/second^2` produces:
696/// - `UnitFactor { measure_ref: "meter", exp: 1 }`
697/// - `UnitFactor { measure_ref: "second", exp: -2 }`
698#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
699pub struct UnitFactor {
700    pub measure_ref: String,
701    pub exp: i32,
702}
703
704/// The argument to a `-> unit <name> ...` command, either a plain numeric
705/// conversion factor or a compound unit expression.
706///
707/// - `Factor(v)` — simple unit: `-> unit meter: 1`, `-> unit kilometer: 1000`
708/// - `Expr(prefix, factors)` — compound unit: `-> unit mps: meter/second`,
709///   `-> unit kmh: 3.6 meter/second`
710///   The `prefix` is an additional scalar multiplier beyond what the unit
711///   factor references contribute; it defaults to `1` when omitted.
712#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
713pub enum UnitArg {
714    Factor(Decimal),
715    Expr(Decimal, Vec<UnitFactor>),
716}
717
718impl fmt::Display for UnitArg {
719    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
720        match self {
721            UnitArg::Factor(v) => write!(f, "{}", v),
722            UnitArg::Expr(prefix, factors) => {
723                if *prefix != Decimal::ONE {
724                    write!(f, "{} ", prefix)?;
725                }
726                for (index, factor) in factors.iter().enumerate() {
727                    if factor.exp == 0 {
728                        unreachable!("BUG: unit factor exponent cannot be zero");
729                    }
730                    if factor.exp > 0 {
731                        if index > 0 {
732                            write!(f, " * ")?;
733                        }
734                        write!(f, "{}", factor.measure_ref)?;
735                        if factor.exp != 1 {
736                            write!(f, "^{}", factor.exp)?;
737                        }
738                    } else {
739                        let denominator_started =
740                            factors[..index].iter().any(|prior| prior.exp < 0);
741                        if denominator_started {
742                            write!(f, " * ")?;
743                        } else {
744                            write!(f, "/")?;
745                        }
746                        write!(f, "{}", factor.measure_ref)?;
747                        let positive_exp = factor
748                            .exp
749                            .checked_neg()
750                            .expect("BUG: negative unit factor exponent");
751                        if positive_exp != 1 {
752                            write!(f, "^{}", positive_exp)?;
753                        }
754                    }
755                }
756                Ok(())
757            }
758        }
759    }
760}
761
762/// A parsed constraint command argument, preserving the literal kind from the
763/// grammar rule `command_arg: { number_literal | boolean_literal | text_literal | label }`.
764///
765/// Three grammatical kinds appear after a constraint command:
766/// - **Literal** — a fully-typed value carrying the literal kind the parser
767///   recognised (`Number`, `Ratio`, `Measure`, `Date`, `Time`,
768///   `Boolean`, `Text`). Stored as the canonical [`crate::literals::Value`]
769///   so downstream consumers match on the variant rather than re-parsing strings.
770/// - **Label** — a bare identifier used as a name (e.g. the unit name `eur`
771///   in `unit eur 1.00`, or a primitive type keyword used as an option label).
772/// - **UnitExpr** — compound unit expression produced by the parser for
773///   `-> unit <name> ...` commands. Only appears as the second argument of a
774///   `Unit` command; the first argument is always the unit name as `Label`.
775///
776/// Planning validates each command's args against the variant kinds it accepts
777/// and rejects mismatches without coercion (a `Text` literal is never a `Number`,
778/// a `Ratio` literal is never a bare `Number`, etc.).
779#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
780#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
781pub enum CommandArg {
782    /// A typed literal value parsed by [`crate::parsing::parser::Parser::parse_literal_value`].
783    Literal(crate::literals::Value),
784    /// An identifier used as a name (unit name, option keyword, etc.).
785    Label(String),
786    /// A unit argument produced by the parser for `-> unit <name> ...` commands.
787    UnitExpr(UnitArg),
788}
789
790impl fmt::Display for CommandArg {
791    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
792        match self {
793            CommandArg::Literal(v) => write!(f, "{}", v),
794            CommandArg::Label(s) => write!(f, "{}", s),
795            CommandArg::UnitExpr(unit_arg) => write!(f, "{}", unit_arg),
796        }
797    }
798}
799
800/// Constraint command for type definitions. Derived from lexer tokens; no string matching.
801#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
802#[serde(rename_all = "snake_case")]
803pub enum TypeConstraintCommand {
804    Help,
805    Suggest,
806    Unit,
807    Trait,
808    Minimum,
809    Maximum,
810    Lower,
811    Upper,
812    Decimals,
813    Option,
814    Options,
815    Length,
816}
817
818impl fmt::Display for TypeConstraintCommand {
819    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
820        let s = match self {
821            TypeConstraintCommand::Help => "help",
822            TypeConstraintCommand::Suggest => "suggest",
823            TypeConstraintCommand::Unit => "unit",
824            TypeConstraintCommand::Trait => "trait",
825            TypeConstraintCommand::Minimum => "minimum",
826            TypeConstraintCommand::Maximum => "maximum",
827            TypeConstraintCommand::Lower => "lower",
828            TypeConstraintCommand::Upper => "upper",
829            TypeConstraintCommand::Decimals => "decimals",
830            TypeConstraintCommand::Option => "option",
831            TypeConstraintCommand::Options => "options",
832            TypeConstraintCommand::Length => "length",
833        };
834        write!(f, "{}", s)
835    }
836}
837
838/// Parses a constraint command name. Returns None for unknown (parser returns error).
839#[must_use]
840pub fn try_parse_type_constraint_command(s: &str) -> Option<TypeConstraintCommand> {
841    match s.trim().to_lowercase().as_str() {
842        "help" => Some(TypeConstraintCommand::Help),
843        "suggest" => Some(TypeConstraintCommand::Suggest),
844        "unit" => Some(TypeConstraintCommand::Unit),
845        "trait" => Some(TypeConstraintCommand::Trait),
846        "minimum" => Some(TypeConstraintCommand::Minimum),
847        "maximum" => Some(TypeConstraintCommand::Maximum),
848        "lower" => Some(TypeConstraintCommand::Lower),
849        "upper" => Some(TypeConstraintCommand::Upper),
850        "decimals" => Some(TypeConstraintCommand::Decimals),
851        "option" => Some(TypeConstraintCommand::Option),
852        "options" => Some(TypeConstraintCommand::Options),
853        "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::Trait
873            | TypeConstraintCommand::Minimum
874            | TypeConstraintCommand::Maximum
875            | TypeConstraintCommand::Lower
876            | TypeConstraintCommand::Upper
877            | TypeConstraintCommand::Decimals
878            | TypeConstraintCommand::Option
879            | TypeConstraintCommand::Options
880            | TypeConstraintCommand::Length => ContinuationShape::SpaceSeparated,
881        }
882    }
883}
884
885/// One `-> command …` row on a [`DataValue::Definition`].
886#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
887pub struct Constraint {
888    pub command: TypeConstraintCommand,
889    pub args: Vec<CommandArg>,
890    pub source_location: crate::parsing::source::Source,
891    /// Parsed from deprecated `unit name value` without colon (removed in a future release).
892    #[serde(default, skip_serializing_if = "is_false")]
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    #[serde(default, skip_serializing_if = "is_false")]
946    pub deprecated_standalone_with: bool,
947}
948
949fn is_false(value: &bool) -> bool {
950    !*value
951}
952
953/// Prefix import alias onto a relative binding path (`pricing.tax_rate` under alias `line` → `line.pricing.tax_rate`).
954#[must_use]
955pub fn prefix_reference(alias: &str, relative: &Reference) -> Reference {
956    let mut segments = vec![ascii_lowercase_logical_name(alias.to_string())];
957    segments.extend(relative.segments.iter().cloned());
958    Reference {
959        segments,
960        name: relative.name.clone(),
961    }
962}
963
964#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
965#[serde(rename_all = "snake_case")]
966/// Parse-time data value (before type resolution)
967pub enum DataValue {
968    /// Declares data: optional explicit parent type, optional constraints (`-> ...`),
969    /// and optional literal value.
970    ///
971    /// Examples:
972    /// - `data x: 3.14` → `base: None`, `value: Some(Number)`
973    /// - `data x: number -> minimum 0` → `base: Some(Number)`, `constraints: Some(...)`
974    /// - `data x: finance.money` → `base: Some(Qualified { spec_alias: "finance", inner: Custom("money") })`
975    Definition {
976        #[serde(default, skip_serializing_if = "Option::is_none")]
977        base: Option<ParentType>,
978        constraints: Option<Vec<Constraint>>,
979        #[serde(default, skip_serializing_if = "Option::is_none")]
980        value: Option<Value>,
981    },
982    /// Import from another spec (surface syntax is `uses`; alias is [`LemmaData::reference`]).
983    Import {
984        spec_ref: SpecRef,
985        bindings: Vec<UsesBinding>,
986    },
987}
988
989impl DataValue {
990    #[must_use]
991    pub fn import(spec_ref: SpecRef) -> Self {
992        Self::Import {
993            spec_ref,
994            bindings: Vec::new(),
995        }
996    }
997
998    /// Whether this is only a literal RHS (`data x: 3.14`), valid as a binding value.
999    #[must_use]
1000    pub fn is_definition_literal_only(&self) -> bool {
1001        matches!(
1002            self,
1003            DataValue::Definition {
1004                base: None,
1005                constraints: None,
1006                value: Some(_),
1007            }
1008        )
1009    }
1010
1011    /// Whether planning must resolve this [`LemmaData`] row through the type resolver / named types.
1012    #[must_use]
1013    pub fn definition_needs_type_resolution(&self) -> bool {
1014        match self {
1015            DataValue::Definition { base: Some(_), .. }
1016            | DataValue::Definition {
1017                constraints: Some(_),
1018                ..
1019            } => true,
1020            DataValue::Definition {
1021                base: None,
1022                constraints: None,
1023                value: Some(v),
1024            } => !matches!(v, Value::NumberWithUnit(_, _)),
1025            DataValue::Import { .. } | DataValue::Definition { .. } => false,
1026        }
1027    }
1028}
1029
1030/// Render a chain of `-> command args ...` constraints for display purposes.
1031/// Shared between [`DataValue::Definition`] constraint chains.
1032fn format_constraint_chain(constraints: &[Constraint]) -> String {
1033    constraints
1034        .iter()
1035        .map(|row| format_constraint_as_source(&row.command, &row.args))
1036        .collect::<Vec<_>>()
1037        .join(" -> ")
1038}
1039
1040impl fmt::Display for DataValue {
1041    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1042        match self {
1043            DataValue::Definition {
1044                base,
1045                constraints,
1046                value,
1047            } => {
1048                if base.is_none() && constraints.is_none() {
1049                    return match value {
1050                        Some(v) => write!(f, "{}", v),
1051                        None => Ok(()),
1052                    };
1053                }
1054                let base_str = match base.as_ref() {
1055                    Some(b) => format!("{b}"),
1056                    None => match value {
1057                        Some(v) => {
1058                            if let Some(ref constraints_vec) = constraints {
1059                                let constraint_str = format_constraint_chain(constraints_vec);
1060                                return write!(f, "{v} -> {constraint_str}");
1061                            }
1062                            return write!(f, "{v}");
1063                        }
1064                        None => String::new(),
1065                    },
1066                };
1067                if let Some(ref constraints_vec) = constraints {
1068                    let constraint_str = format_constraint_chain(constraints_vec);
1069                    write!(f, "{base_str} -> {constraint_str}")
1070                } else {
1071                    write!(f, "{base_str}")
1072                }
1073            }
1074            DataValue::Import {
1075                spec_ref,
1076                bindings: _,
1077            } => {
1078                write!(f, "uses {}", spec_ref)
1079            }
1080        }
1081    }
1082}
1083
1084impl LemmaData {
1085    #[must_use]
1086    pub fn new(reference: Reference, value: DataValue, source_location: Source) -> Self {
1087        Self {
1088            reference,
1089            value,
1090            source_location,
1091        }
1092    }
1093}
1094
1095impl LemmaSpec {
1096    #[must_use]
1097    pub fn new(name: String) -> Self {
1098        Self {
1099            name: ascii_lowercase_logical_name(name),
1100            effective_from: EffectiveDate::Origin,
1101            source_type: None,
1102            start_line: 1,
1103            commentary: None,
1104            data: Vec::new(),
1105            rules: Vec::new(),
1106            meta_fields: Vec::new(),
1107        }
1108    }
1109
1110    /// Temporal range start. Origin (None) means −∞.
1111    pub fn effective_from(&self) -> Option<&DateTimeValue> {
1112        self.effective_from.as_ref()
1113    }
1114
1115    #[must_use]
1116    pub fn with_source_type(mut self, source_type: crate::parsing::source::SourceType) -> Self {
1117        self.source_type = Some(source_type);
1118        self
1119    }
1120
1121    #[must_use]
1122    pub fn with_start_line(mut self, start_line: usize) -> Self {
1123        self.start_line = start_line;
1124        self
1125    }
1126
1127    #[must_use]
1128    pub fn set_commentary(mut self, commentary: String) -> Self {
1129        self.commentary = Some(commentary);
1130        self
1131    }
1132
1133    #[must_use]
1134    pub fn add_data(mut self, data: LemmaData) -> Self {
1135        self.data.push(data);
1136        self
1137    }
1138
1139    #[must_use]
1140    pub fn add_rule(mut self, rule: LemmaRule) -> Self {
1141        self.rules.push(rule);
1142        self
1143    }
1144
1145    #[must_use]
1146    pub fn add_meta_field(mut self, meta: MetaField) -> Self {
1147        self.meta_fields.push(meta);
1148        self
1149    }
1150}
1151
1152impl fmt::Display for LemmaSpec {
1153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1154        write!(f, "spec {}", self.name)?;
1155        if let EffectiveDate::DateTimeValue(ref af) = self.effective_from {
1156            write!(f, " {}", af)?;
1157        }
1158        writeln!(f)?;
1159
1160        if let Some(ref commentary) = self.commentary {
1161            writeln!(f, "\"\"\"")?;
1162            writeln!(f, "{}", commentary)?;
1163            writeln!(f, "\"\"\"")?;
1164        }
1165
1166        if !self.data.is_empty() {
1167            writeln!(f)?;
1168            for data in &self.data {
1169                write!(f, "{}", data)?;
1170            }
1171        }
1172
1173        if !self.rules.is_empty() {
1174            writeln!(f)?;
1175            for (index, rule) in self.rules.iter().enumerate() {
1176                if index > 0 {
1177                    writeln!(f)?;
1178                }
1179                write!(f, "{}", rule)?;
1180            }
1181        }
1182
1183        if !self.meta_fields.is_empty() {
1184            writeln!(f)?;
1185            for meta in &self.meta_fields {
1186                writeln!(f, "{}", meta)?;
1187            }
1188        }
1189
1190        Ok(())
1191    }
1192}
1193
1194impl fmt::Display for LemmaData {
1195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1196        writeln!(f, "data {}: {}", self.reference, self.value)
1197    }
1198}
1199
1200impl fmt::Display for LemmaRule {
1201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1202        write!(f, "rule {}: {}", self.name, self.expression)?;
1203        for unless_clause in &self.unless_clauses {
1204            write!(
1205                f,
1206                "\n  unless {} then {}",
1207                unless_clause.condition, unless_clause.result
1208            )?;
1209        }
1210        writeln!(f)?;
1211        Ok(())
1212    }
1213}
1214
1215/// Precedence level for an expression kind.
1216///
1217/// Higher values bind tighter. Used by `Expression::Display` and the formatter
1218/// to insert parentheses only where needed.
1219///
1220/// `RangeLiteral` (type construction via `...`) binds above all arithmetic; only atoms bind
1221/// above range. Parser climb in [`crate::parsing::parser::Parser`] must match this table.
1222pub fn expression_precedence(kind: &ExpressionKind) -> u8 {
1223    match kind {
1224        ExpressionKind::LogicalAnd(..) => 2,
1225        ExpressionKind::LogicalNegation(..) => 3,
1226        ExpressionKind::Comparison(..) | ExpressionKind::ResultIsVeto(..) => 4,
1227        ExpressionKind::RangeContainment(..) => 4,
1228        ExpressionKind::DateRelative(..) | ExpressionKind::DateCalendar(..) => 4,
1229        ExpressionKind::Arithmetic(_, op, _) => arithmetic_precedence(op),
1230        ExpressionKind::UnitConversion(..) => 8,
1231        ExpressionKind::RangeLiteral(..) => 9,
1232        ExpressionKind::MathematicalComputation(..) => 10,
1233        ExpressionKind::PastFutureRange(..) => 10,
1234        ExpressionKind::Literal(..)
1235        | ExpressionKind::Reference(..)
1236        | ExpressionKind::Now
1237        | ExpressionKind::Veto(..) => 10,
1238    }
1239}
1240
1241/// Precedence for an arithmetic operator. Must match [`expression_precedence`].
1242pub fn arithmetic_precedence(op: &ArithmeticComputation) -> u8 {
1243    match op {
1244        ArithmeticComputation::Add | ArithmeticComputation::Subtract => 5,
1245        ArithmeticComputation::Multiply
1246        | ArithmeticComputation::Divide
1247        | ArithmeticComputation::Modulo => 6,
1248        ArithmeticComputation::Power => 7,
1249    }
1250}
1251
1252/// Operand position under a parent operator.
1253#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1254pub enum OperandSide {
1255    Left,
1256    Right,
1257}
1258
1259/// Associativity of a binary (or n-ary chain) operator.
1260#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1261pub enum Associativity {
1262    Left,
1263    Right,
1264}
1265
1266/// Whether a child expression must be wrapped in parentheses when printed under a parent.
1267///
1268/// - `parent_assoc == None`: unary / prefix parent — wrap only when `child_prec < parent_prec`.
1269/// - `Some(Left)`: wrap looser children, and same-prec **right** children.
1270/// - `Some(Right)`: wrap looser children, and same-prec **left** children.
1271pub fn operand_needs_parentheses(
1272    child_prec: u8,
1273    parent_prec: u8,
1274    side: OperandSide,
1275    parent_assoc: Option<Associativity>,
1276) -> bool {
1277    if child_prec < parent_prec {
1278        return true;
1279    }
1280    if child_prec > parent_prec {
1281        return false;
1282    }
1283    match parent_assoc {
1284        None => false,
1285        Some(Associativity::Left) => matches!(side, OperandSide::Right),
1286        Some(Associativity::Right) => matches!(side, OperandSide::Left),
1287    }
1288}
1289
1290pub fn arithmetic_associativity(op: &ArithmeticComputation) -> Associativity {
1291    match op {
1292        ArithmeticComputation::Power => Associativity::Right,
1293        ArithmeticComputation::Add
1294        | ArithmeticComputation::Subtract
1295        | ArithmeticComputation::Multiply
1296        | ArithmeticComputation::Divide
1297        | ArithmeticComputation::Modulo => Associativity::Left,
1298    }
1299}
1300
1301fn write_expression_child(
1302    f: &mut fmt::Formatter<'_>,
1303    child: &Expression,
1304    parent_prec: u8,
1305    side: OperandSide,
1306    parent_assoc: Option<Associativity>,
1307) -> fmt::Result {
1308    let child_prec = expression_precedence(&child.kind);
1309    if operand_needs_parentheses(child_prec, parent_prec, side, parent_assoc) {
1310        write!(f, "({})", child)
1311    } else {
1312        write!(f, "{}", child)
1313    }
1314}
1315
1316impl fmt::Display for Expression {
1317    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1318        match &self.kind {
1319            ExpressionKind::Literal(lit) => write!(f, "{}", AsLemmaSource(lit)),
1320            ExpressionKind::Reference(r) => write!(f, "{}", r),
1321            ExpressionKind::Arithmetic(left, op, right) => {
1322                let my_prec = expression_precedence(&self.kind);
1323                let assoc = Some(arithmetic_associativity(op));
1324                write_expression_child(f, left, my_prec, OperandSide::Left, assoc)?;
1325                write!(f, " {} ", op)?;
1326                write_expression_child(f, right, my_prec, OperandSide::Right, assoc)
1327            }
1328            ExpressionKind::Comparison(left, op, right) => {
1329                let my_prec = expression_precedence(&self.kind);
1330                write_expression_child(f, left, my_prec, OperandSide::Left, None)?;
1331                write!(f, " {} ", op)?;
1332                write_expression_child(f, right, my_prec, OperandSide::Right, None)
1333            }
1334            ExpressionKind::UnitConversion(value, target) => {
1335                let my_prec = expression_precedence(&self.kind);
1336                write_expression_child(f, value, my_prec, OperandSide::Left, None)?;
1337                write!(f, " as {}", target)
1338            }
1339            ExpressionKind::LogicalNegation(expr, negation) => {
1340                if let (NegationType::Not, ExpressionKind::ResultIsVeto(operand)) =
1341                    (negation, &expr.kind)
1342                {
1343                    let my_prec = expression_precedence(&self.kind);
1344                    write_expression_child(f, operand, my_prec, OperandSide::Left, None)?;
1345                    write!(f, " is not veto")
1346                } else {
1347                    let my_prec = expression_precedence(&self.kind);
1348                    write!(f, "not ")?;
1349                    write_expression_child(f, expr, my_prec, OperandSide::Right, None)
1350                }
1351            }
1352            ExpressionKind::ResultIsVeto(operand) => {
1353                let my_prec = expression_precedence(&self.kind);
1354                write_expression_child(f, operand, my_prec, OperandSide::Left, None)?;
1355                write!(f, " is veto")
1356            }
1357            ExpressionKind::LogicalAnd(left, right) => {
1358                let my_prec = expression_precedence(&self.kind);
1359                let assoc = Some(Associativity::Left);
1360                write_expression_child(f, left, my_prec, OperandSide::Left, assoc)?;
1361                write!(f, " and ")?;
1362                write_expression_child(f, right, my_prec, OperandSide::Right, assoc)
1363            }
1364            ExpressionKind::MathematicalComputation(op, operand) => {
1365                let my_prec = expression_precedence(&self.kind);
1366                write!(f, "{} ", op)?;
1367                write_expression_child(f, operand, my_prec, OperandSide::Right, None)
1368            }
1369            ExpressionKind::Veto(veto) => match &veto.message {
1370                Some(msg) => write!(f, "veto {}", quote_lemma_text(msg)),
1371                None => write!(f, "veto"),
1372            },
1373            ExpressionKind::Now => write!(f, "now"),
1374            ExpressionKind::DateRelative(kind, date_expr) => {
1375                write!(f, "{} {}", date_expr, kind)?;
1376                Ok(())
1377            }
1378            ExpressionKind::DateCalendar(kind, unit, date_expr) => {
1379                write!(f, "{} {} {}", date_expr, kind, unit)
1380            }
1381            ExpressionKind::RangeLiteral(left, right) => {
1382                let my_prec = expression_precedence(&self.kind);
1383                write_expression_child(f, left, my_prec, OperandSide::Left, None)?;
1384                write!(f, "...")?;
1385                write_expression_child(f, right, my_prec, OperandSide::Right, None)
1386            }
1387            ExpressionKind::PastFutureRange(kind, offset_expr) => {
1388                match kind {
1389                    DateRelativeKind::InPast => write!(f, "past ")?,
1390                    DateRelativeKind::InFuture => write!(f, "future ")?,
1391                }
1392                let my_prec = expression_precedence(&self.kind);
1393                write_expression_child(f, offset_expr, my_prec, OperandSide::Right, None)
1394            }
1395            ExpressionKind::RangeContainment(value, range) => {
1396                let my_prec = expression_precedence(&self.kind);
1397                write_expression_child(f, value, my_prec, OperandSide::Left, None)?;
1398                write!(f, " in ")?;
1399                write_expression_child(f, range, my_prec, OperandSide::Right, None)
1400            }
1401        }
1402    }
1403}
1404
1405impl fmt::Display for ConversionTarget {
1406    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1407        match self {
1408            ConversionTarget::Type(kind) => write!(f, "{kind}"),
1409            ConversionTarget::Unit { unit_name } => write!(f, "{unit_name}"),
1410        }
1411    }
1412}
1413
1414impl fmt::Display for ArithmeticComputation {
1415    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1416        match self {
1417            ArithmeticComputation::Add => write!(f, "+"),
1418            ArithmeticComputation::Subtract => write!(f, "-"),
1419            ArithmeticComputation::Multiply => write!(f, "*"),
1420            ArithmeticComputation::Divide => write!(f, "/"),
1421            ArithmeticComputation::Modulo => write!(f, "%"),
1422            ArithmeticComputation::Power => write!(f, "^"),
1423        }
1424    }
1425}
1426
1427impl fmt::Display for ComparisonComputation {
1428    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1429        match self {
1430            ComparisonComputation::GreaterThan => write!(f, ">"),
1431            ComparisonComputation::LessThan => write!(f, "<"),
1432            ComparisonComputation::GreaterThanOrEqual => write!(f, ">="),
1433            ComparisonComputation::LessThanOrEqual => write!(f, "<="),
1434            ComparisonComputation::Is => write!(f, "is"),
1435            ComparisonComputation::IsNot => write!(f, "is not"),
1436        }
1437    }
1438}
1439
1440impl fmt::Display for MathematicalComputation {
1441    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1442        match self {
1443            MathematicalComputation::Sqrt => write!(f, "sqrt"),
1444            MathematicalComputation::Sin => write!(f, "sin"),
1445            MathematicalComputation::Cos => write!(f, "cos"),
1446            MathematicalComputation::Tan => write!(f, "tan"),
1447            MathematicalComputation::Asin => write!(f, "asin"),
1448            MathematicalComputation::Acos => write!(f, "acos"),
1449            MathematicalComputation::Atan => write!(f, "atan"),
1450            MathematicalComputation::Log => write!(f, "log"),
1451            MathematicalComputation::Exp => write!(f, "exp"),
1452            MathematicalComputation::Abs => write!(f, "abs"),
1453            MathematicalComputation::Floor => write!(f, "floor"),
1454            MathematicalComputation::Ceil => write!(f, "ceil"),
1455            MathematicalComputation::Round => write!(f, "round"),
1456        }
1457    }
1458}
1459
1460// -----------------------------------------------------------------------------
1461// Primitive type kinds and parent type references
1462// -----------------------------------------------------------------------------
1463
1464/// Built-in primitive type kind. Single source of truth for type keywords.
1465#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1466#[serde(rename_all = "snake_case")]
1467pub enum PrimitiveKind {
1468    Boolean,
1469    Measure,
1470    MeasureRange,
1471    Number,
1472    NumberRange,
1473    Ratio,
1474    RatioRange,
1475    Text,
1476    Date,
1477    DateRange,
1478    Time,
1479    TimeRange,
1480}
1481
1482impl std::fmt::Display for PrimitiveKind {
1483    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1484        let s = match self {
1485            PrimitiveKind::Boolean => "boolean",
1486            PrimitiveKind::Measure => "measure",
1487            PrimitiveKind::MeasureRange => "measure range",
1488            PrimitiveKind::Number => "number",
1489            PrimitiveKind::NumberRange => "number range",
1490            PrimitiveKind::Ratio => "ratio",
1491            PrimitiveKind::RatioRange => "ratio range",
1492            PrimitiveKind::Text => "text",
1493            PrimitiveKind::Date => "date",
1494            PrimitiveKind::DateRange => "date range",
1495            PrimitiveKind::Time => "time",
1496            PrimitiveKind::TimeRange => "time range",
1497        };
1498        write!(f, "{}", s)
1499    }
1500}
1501
1502/// Parent type in a type definition: built-in primitive or custom type name.
1503///
1504/// `name` is the declared type name (the data name that introduces this type).
1505/// For `data temperature: measure`, name = "temperature", primitive = Measure.
1506#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1507#[serde(tag = "kind", rename_all = "snake_case")]
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: MetaValue, DataValue (formatter / round-trip) ---
1722
1723impl<'a> fmt::Display for AsLemmaSource<'a, MetaValue> {
1724    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1725        match self.0 {
1726            MetaValue::Literal(v) => write!(f, "{}", AsLemmaSource(v)),
1727            MetaValue::Unquoted(s) => write!(f, "{}", s),
1728        }
1729    }
1730}
1731
1732impl<'a> fmt::Display for AsLemmaSource<'a, DataValue> {
1733    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1734        match self.0 {
1735            DataValue::Definition {
1736                base,
1737                constraints,
1738                value,
1739            } => {
1740                if base.is_none() && constraints.is_none() {
1741                    if let Some(v) = value {
1742                        return write!(f, "{}", AsLemmaSource(v));
1743                    }
1744                }
1745                let base_str = match base.as_ref() {
1746                    Some(b) => format!("{}", b),
1747                    None => match value {
1748                        Some(v) => {
1749                            if let Some(ref constraints_vec) = constraints {
1750                                let constraint_str =
1751                                    format_constraints_as_source(constraints_vec, " -> ");
1752                                return write!(f, "{} -> {}", AsLemmaSource(v), constraint_str);
1753                            }
1754                            return write!(f, "{}", AsLemmaSource(v));
1755                        }
1756                        None => String::new(),
1757                    },
1758                };
1759                if let Some(ref constraints_vec) = constraints {
1760                    let constraint_str = format_constraints_as_source(constraints_vec, " -> ");
1761                    write!(f, "{} -> {}", base_str, constraint_str)
1762                } else {
1763                    write!(f, "{}", base_str)
1764                }
1765            }
1766            DataValue::Import {
1767                spec_ref,
1768                bindings: _,
1769            } => {
1770                write!(f, "uses {}", spec_ref)
1771            }
1772        }
1773    }
1774}
1775
1776pub(crate) fn canonicalize_value(value: &mut Value) {
1777    if let Value::NumberWithUnit(_, unit) = value {
1778        *unit = ascii_lowercase_logical_name(std::mem::take(unit));
1779    }
1780}
1781
1782pub(crate) fn canonicalize_reference(reference: &mut Reference) {
1783    for segment in &mut reference.segments {
1784        *segment = ascii_lowercase_logical_name(std::mem::take(segment));
1785    }
1786    reference.name = ascii_lowercase_logical_name(std::mem::take(&mut reference.name));
1787}
1788
1789pub(crate) fn canonicalize_spec_ref(spec_ref: &mut SpecRef) {
1790    spec_ref.name = ascii_lowercase_logical_name(std::mem::take(&mut spec_ref.name));
1791    if let Some(qualifier) = spec_ref.repository.as_mut() {
1792        qualifier.name = ascii_lowercase_logical_name(std::mem::take(&mut qualifier.name));
1793    }
1794}
1795
1796pub(crate) fn canonicalize_parent_type(parent: &mut ParentType) {
1797    match parent {
1798        ParentType::Custom { name } => {
1799            *name = ascii_lowercase_logical_name(std::mem::take(name));
1800        }
1801        ParentType::Qualified { spec_alias, inner } => {
1802            *spec_alias = ascii_lowercase_logical_name(std::mem::take(spec_alias));
1803            canonicalize_parent_type(inner);
1804        }
1805        ParentType::Ranged { inner } => {
1806            canonicalize_parent_type(inner);
1807        }
1808        ParentType::Primitive { .. } => {}
1809    }
1810}
1811
1812pub(crate) fn canonicalize_unit_factor(factor: &mut UnitFactor) {
1813    factor.measure_ref = ascii_lowercase_logical_name(std::mem::take(&mut factor.measure_ref));
1814}
1815
1816pub(crate) fn canonicalize_unit_arg(unit_arg: &mut UnitArg) {
1817    if let UnitArg::Expr(_, factors) = unit_arg {
1818        for factor in factors {
1819            canonicalize_unit_factor(factor);
1820        }
1821    }
1822}
1823
1824pub(crate) fn canonicalize_command_arg(command_arg: &mut CommandArg) {
1825    match command_arg {
1826        CommandArg::Literal(value) => canonicalize_value(value),
1827        CommandArg::Label(label) => {
1828            *label = ascii_lowercase_logical_name(std::mem::take(label));
1829        }
1830        CommandArg::UnitExpr(unit_arg) => canonicalize_unit_arg(unit_arg),
1831    }
1832}
1833
1834pub(crate) fn canonicalize_constraints(constraints: &mut [Constraint]) {
1835    for row in constraints {
1836        for arg in &mut row.args {
1837            canonicalize_command_arg(arg);
1838        }
1839    }
1840}
1841
1842pub(crate) fn canonicalize_expression(expression: &mut Expression) {
1843    match &mut expression.kind {
1844        ExpressionKind::Literal(value) => canonicalize_value(value),
1845        ExpressionKind::Reference(reference) => canonicalize_reference(reference),
1846        ExpressionKind::Now => {}
1847        ExpressionKind::DateRelative(_, expression) => {
1848            canonicalize_expression(Arc::make_mut(expression));
1849        }
1850        ExpressionKind::DateCalendar(_, _, expression) => {
1851            canonicalize_expression(Arc::make_mut(expression));
1852        }
1853        ExpressionKind::RangeLiteral(left, right) => {
1854            canonicalize_expression(Arc::make_mut(left));
1855            canonicalize_expression(Arc::make_mut(right));
1856        }
1857        ExpressionKind::PastFutureRange(_, expression) => {
1858            canonicalize_expression(Arc::make_mut(expression));
1859        }
1860        ExpressionKind::RangeContainment(value, range) => {
1861            canonicalize_expression(Arc::make_mut(value));
1862            canonicalize_expression(Arc::make_mut(range));
1863        }
1864        ExpressionKind::LogicalAnd(left, right) => {
1865            canonicalize_expression(Arc::make_mut(left));
1866            canonicalize_expression(Arc::make_mut(right));
1867        }
1868        ExpressionKind::Arithmetic(left, _, right) => {
1869            canonicalize_expression(Arc::make_mut(left));
1870            canonicalize_expression(Arc::make_mut(right));
1871        }
1872        ExpressionKind::Comparison(left, _, right) => {
1873            canonicalize_expression(Arc::make_mut(left));
1874            canonicalize_expression(Arc::make_mut(right));
1875        }
1876        ExpressionKind::UnitConversion(expression, _) => {
1877            canonicalize_expression(Arc::make_mut(expression));
1878        }
1879        ExpressionKind::LogicalNegation(expression, _) => {
1880            canonicalize_expression(Arc::make_mut(expression));
1881        }
1882        ExpressionKind::MathematicalComputation(_, expression) => {
1883            canonicalize_expression(Arc::make_mut(expression));
1884        }
1885        ExpressionKind::Veto(_) => {}
1886        ExpressionKind::ResultIsVeto(expression) => {
1887            canonicalize_expression(Arc::make_mut(expression));
1888        }
1889    }
1890}
1891
1892pub(crate) fn canonicalize_unless_clause(unless_clause: &mut UnlessClause) {
1893    canonicalize_expression(&mut unless_clause.condition);
1894    canonicalize_expression(&mut unless_clause.result);
1895}
1896
1897pub(crate) fn canonicalize_data_value(data_value: &mut DataValue) {
1898    match data_value {
1899        DataValue::Definition {
1900            base,
1901            constraints,
1902            value,
1903        } => {
1904            if let Some(base) = base {
1905                canonicalize_parent_type(base);
1906            }
1907            if let Some(constraints) = constraints {
1908                canonicalize_constraints(constraints);
1909            }
1910            if let Some(value) = value {
1911                canonicalize_value(value);
1912            }
1913        }
1914        DataValue::Import { spec_ref, bindings } => {
1915            canonicalize_spec_ref(spec_ref);
1916            for binding in bindings {
1917                canonicalize_reference(&mut binding.path);
1918                match &mut binding.rhs {
1919                    WithRhs::Literal(value) => canonicalize_value(value),
1920                    WithRhs::Reference { target } => canonicalize_reference(target),
1921                }
1922            }
1923        }
1924    }
1925}
1926
1927pub(crate) fn canonicalize_lemma_data(data: &mut LemmaData) {
1928    canonicalize_reference(&mut data.reference);
1929    canonicalize_data_value(&mut data.value);
1930}
1931
1932pub(crate) fn canonicalize_lemma_rule(rule: &mut LemmaRule) {
1933    rule.name = ascii_lowercase_logical_name(std::mem::take(&mut rule.name));
1934    canonicalize_expression(&mut rule.expression);
1935    for unless_clause in &mut rule.unless_clauses {
1936        canonicalize_unless_clause(unless_clause);
1937    }
1938}
1939
1940pub(crate) fn canonicalize_lemma_spec(spec: &mut LemmaSpec) {
1941    spec.name = ascii_lowercase_logical_name(std::mem::take(&mut spec.name));
1942    for meta in &mut spec.meta_fields {
1943        meta.key = ascii_lowercase_logical_name(std::mem::take(&mut meta.key));
1944    }
1945    for data in &mut spec.data {
1946        canonicalize_lemma_data(data);
1947    }
1948    for rule in &mut spec.rules {
1949        canonicalize_lemma_rule(rule);
1950    }
1951}
1952
1953pub(crate) fn canonicalize_repository(repository: &mut LemmaRepository) {
1954    if let Some(name) = repository.name.take() {
1955        repository.name = Some(ascii_lowercase_logical_name(name));
1956    }
1957}
1958
1959#[cfg(test)]
1960mod tests {
1961    use super::*;
1962    use crate::literals::DateGranularity;
1963
1964    #[test]
1965    fn test_conversion_target_display() {
1966        assert_eq!(
1967            format!("{}", ConversionTarget::Type(PrimitiveKind::Number)),
1968            "number"
1969        );
1970    }
1971
1972    #[test]
1973    fn test_value_number_with_unit_ratio_display() {
1974        use rust_decimal::Decimal;
1975        use std::str::FromStr;
1976        let percent =
1977            Value::NumberWithUnit(Decimal::from_str("10").unwrap(), "percent".to_string());
1978        assert_eq!(format!("{}", percent), "10%");
1979        let permille =
1980            Value::NumberWithUnit(Decimal::from_str("5").unwrap(), "permille".to_string());
1981        assert_eq!(format!("{}", permille), "5%%");
1982    }
1983
1984    #[test]
1985    fn test_datetime_value_display() {
1986        let dt = DateTimeValue {
1987            year: 2024,
1988            month: 12,
1989            day: 25,
1990            hour: 14,
1991            minute: 30,
1992            second: 45,
1993            microsecond: 0,
1994            timezone: Some(TimezoneValue {
1995                offset_hours: 1,
1996                offset_minutes: 0,
1997            }),
1998
1999            granularity: DateGranularity::DateTime,
2000        };
2001        assert_eq!(format!("{}", dt), "2024-12-25T14:30:45+01:00");
2002    }
2003
2004    #[test]
2005    fn test_datetime_value_display_date_only() {
2006        let dt = DateTimeValue {
2007            year: 2026,
2008            month: 3,
2009            day: 4,
2010            hour: 0,
2011            minute: 0,
2012            second: 0,
2013            microsecond: 0,
2014            timezone: None,
2015
2016            granularity: DateGranularity::Full,
2017        };
2018        assert_eq!(format!("{}", dt), "2026-03-04");
2019    }
2020
2021    #[test]
2022    fn test_datetime_value_display_microseconds() {
2023        let dt = DateTimeValue {
2024            year: 2026,
2025            month: 2,
2026            day: 23,
2027            hour: 14,
2028            minute: 30,
2029            second: 45,
2030            microsecond: 123456,
2031            timezone: Some(TimezoneValue {
2032                offset_hours: 0,
2033                offset_minutes: 0,
2034            }),
2035
2036            granularity: DateGranularity::DateTime,
2037        };
2038        assert_eq!(format!("{}", dt), "2026-02-23T14:30:45.123456Z");
2039    }
2040
2041    #[test]
2042    fn test_datetime_microsecond_in_ordering() {
2043        let a = DateTimeValue {
2044            year: 2026,
2045            month: 1,
2046            day: 1,
2047            hour: 0,
2048            minute: 0,
2049            second: 0,
2050            microsecond: 100,
2051            timezone: None,
2052
2053            granularity: DateGranularity::DateTime,
2054        };
2055        let b = DateTimeValue {
2056            year: 2026,
2057            month: 1,
2058            day: 1,
2059            hour: 0,
2060            minute: 0,
2061            second: 0,
2062            microsecond: 200,
2063            timezone: None,
2064
2065            granularity: DateGranularity::DateTime,
2066        };
2067        assert!(a < b);
2068    }
2069
2070    #[test]
2071    fn test_datetime_parse_iso_week() {
2072        let dt: DateTimeValue = "2026-W01".parse().unwrap();
2073        assert_eq!(dt.year, 2025);
2074        assert_eq!(dt.month, 12);
2075        assert_eq!(dt.day, 29);
2076        assert_eq!(dt.microsecond, 0);
2077        assert_eq!(dt.to_string(), "2026-W01");
2078        assert!(matches!(
2079            dt.granularity,
2080            DateGranularity::IsoWeek {
2081                iso_year: 2026,
2082                week: 1
2083            }
2084        ));
2085    }
2086
2087    #[test]
2088    fn test_negation_types() {
2089        let json = serde_json::to_string(&NegationType::Not).expect("serialize NegationType");
2090        let decoded: NegationType = serde_json::from_str(&json).expect("deserialize NegationType");
2091        assert_eq!(decoded, NegationType::Not);
2092    }
2093
2094    #[test]
2095    fn parent_type_primitive_serde_internally_tagged() {
2096        let p = ParentType::Primitive {
2097            primitive: PrimitiveKind::Number,
2098        };
2099        let json = serde_json::to_string(&p).expect("ParentType::Primitive must serialize");
2100        assert!(json.contains("\"kind\"") && json.contains("\"primitive\""));
2101        let back: ParentType = serde_json::from_str(&json).expect("deserialize");
2102        assert_eq!(back, p);
2103    }
2104
2105    // =====================================================================
2106    // DataValue Display — constraint formatting
2107    // =====================================================================
2108
2109    fn text_arg(s: &str) -> CommandArg {
2110        CommandArg::Literal(crate::literals::Value::Text(s.to_string()))
2111    }
2112
2113    fn number_arg(s: &str) -> CommandArg {
2114        let d: rust_decimal::Decimal = s.parse().expect("decimal");
2115        CommandArg::Literal(crate::literals::Value::Number(d))
2116    }
2117
2118    fn boolean_arg(b: BooleanValue) -> CommandArg {
2119        CommandArg::Literal(crate::literals::Value::Boolean(b))
2120    }
2121
2122    fn measure_arg(value: &str, unit: &str) -> CommandArg {
2123        let d: rust_decimal::Decimal = value.parse().expect("decimal");
2124        CommandArg::Literal(crate::literals::Value::NumberWithUnit(d, unit.to_string()))
2125    }
2126
2127    fn duration_arg(value: &str, unit: &str) -> CommandArg {
2128        let d: rust_decimal::Decimal = value.parse().expect("decimal");
2129        CommandArg::Literal(crate::literals::Value::NumberWithUnit(d, unit.to_string()))
2130    }
2131
2132    #[test]
2133    fn as_lemma_source_text_default_is_quoted() {
2134        let fv = DataValue::Definition {
2135            base: Some(ParentType::Primitive {
2136                primitive: PrimitiveKind::Text,
2137            }),
2138            constraints: Some(vec![test_constraint(
2139                TypeConstraintCommand::Suggest,
2140                vec![text_arg("single")],
2141            )]),
2142            value: None,
2143        };
2144        assert_eq!(
2145            format!("{}", AsLemmaSource(&fv)),
2146            "text -> suggest \"single\""
2147        );
2148    }
2149
2150    #[test]
2151    fn as_lemma_source_number_default_not_quoted() {
2152        let fv = DataValue::Definition {
2153            base: Some(ParentType::Primitive {
2154                primitive: PrimitiveKind::Number,
2155            }),
2156            constraints: Some(vec![test_constraint(
2157                TypeConstraintCommand::Suggest,
2158                vec![number_arg("10")],
2159            )]),
2160            value: None,
2161        };
2162        assert_eq!(format!("{}", AsLemmaSource(&fv)), "number -> suggest 10");
2163    }
2164
2165    #[test]
2166    fn as_lemma_source_help_always_quoted() {
2167        let fv = DataValue::Definition {
2168            base: Some(ParentType::Primitive {
2169                primitive: PrimitiveKind::Number,
2170            }),
2171            constraints: Some(vec![test_constraint(
2172                TypeConstraintCommand::Help,
2173                vec![text_arg("Enter a measure")],
2174            )]),
2175            value: None,
2176        };
2177        assert_eq!(
2178            format!("{}", AsLemmaSource(&fv)),
2179            "number -> help \"Enter a measure\""
2180        );
2181    }
2182
2183    #[test]
2184    fn as_lemma_source_text_option_quoted() {
2185        let fv = DataValue::Definition {
2186            base: Some(ParentType::Primitive {
2187                primitive: PrimitiveKind::Text,
2188            }),
2189            constraints: Some(vec![
2190                test_constraint(TypeConstraintCommand::Option, vec![text_arg("active")]),
2191                test_constraint(TypeConstraintCommand::Option, vec![text_arg("inactive")]),
2192            ]),
2193            value: None,
2194        };
2195        assert_eq!(
2196            format!("{}", AsLemmaSource(&fv)),
2197            "text -> option \"active\" -> option \"inactive\""
2198        );
2199    }
2200
2201    #[test]
2202    fn as_lemma_source_measure_unit_not_quoted() {
2203        let fv = DataValue::Definition {
2204            base: Some(ParentType::Primitive {
2205                primitive: PrimitiveKind::Measure,
2206            }),
2207            constraints: Some(vec![
2208                test_constraint(
2209                    TypeConstraintCommand::Unit,
2210                    vec![
2211                        CommandArg::Label("eur".to_string()),
2212                        CommandArg::UnitExpr(UnitArg::Factor(decimal("1.00"))),
2213                    ],
2214                ),
2215                test_constraint(
2216                    TypeConstraintCommand::Unit,
2217                    vec![
2218                        CommandArg::Label("usd".to_string()),
2219                        CommandArg::UnitExpr(UnitArg::Factor(decimal("0.91"))),
2220                    ],
2221                ),
2222            ]),
2223            value: None,
2224        };
2225        assert_eq!(
2226            format!("{}", AsLemmaSource(&fv)),
2227            "measure -> unit eur: 1.00 -> unit usd: 0.91"
2228        );
2229    }
2230
2231    #[test]
2232    fn as_lemma_source_measure_minimum_with_unit() {
2233        let fv = DataValue::Definition {
2234            base: Some(ParentType::Primitive {
2235                primitive: PrimitiveKind::Measure,
2236            }),
2237            constraints: Some(vec![test_constraint(
2238                TypeConstraintCommand::Minimum,
2239                vec![measure_arg("0", "eur")],
2240            )]),
2241            value: None,
2242        };
2243        assert_eq!(
2244            format!("{}", AsLemmaSource(&fv)),
2245            "measure -> minimum 0 eur"
2246        );
2247    }
2248
2249    #[test]
2250    fn as_lemma_source_boolean_default() {
2251        let fv = DataValue::Definition {
2252            base: Some(ParentType::Primitive {
2253                primitive: PrimitiveKind::Boolean,
2254            }),
2255            constraints: Some(vec![test_constraint(
2256                TypeConstraintCommand::Suggest,
2257                vec![boolean_arg(BooleanValue::True)],
2258            )]),
2259            value: None,
2260        };
2261        assert_eq!(format!("{}", AsLemmaSource(&fv)), "boolean -> suggest true");
2262    }
2263
2264    #[test]
2265    fn as_lemma_source_duration_default() {
2266        let fv = DataValue::Definition {
2267            base: Some(ParentType::Custom {
2268                name: "duration".to_string(),
2269            }),
2270            constraints: Some(vec![test_constraint(
2271                TypeConstraintCommand::Suggest,
2272                vec![duration_arg("40", "hour")],
2273            )]),
2274            value: None,
2275        };
2276        assert_eq!(
2277            format!("{}", AsLemmaSource(&fv)),
2278            "duration -> suggest 40 hour"
2279        );
2280    }
2281
2282    #[test]
2283    fn as_lemma_source_named_type_default_quoted() {
2284        // Named types (user-defined): the parser produces a typed Text literal for
2285        // quoted suggestion values like `suggest "single"`.
2286        let fv = DataValue::Definition {
2287            base: Some(ParentType::Custom {
2288                name: "filing_status_type".to_string(),
2289            }),
2290            constraints: Some(vec![test_constraint(
2291                TypeConstraintCommand::Suggest,
2292                vec![text_arg("single")],
2293            )]),
2294            value: None,
2295        };
2296        assert_eq!(
2297            format!("{}", AsLemmaSource(&fv)),
2298            "filing_status_type -> suggest \"single\""
2299        );
2300    }
2301
2302    #[test]
2303    fn as_lemma_source_help_escapes_quotes() {
2304        let fv = DataValue::Definition {
2305            base: Some(ParentType::Primitive {
2306                primitive: PrimitiveKind::Text,
2307            }),
2308            constraints: Some(vec![test_constraint(
2309                TypeConstraintCommand::Help,
2310                vec![text_arg("say \"hello\"")],
2311            )]),
2312            value: None,
2313        };
2314        assert_eq!(
2315            format!("{}", AsLemmaSource(&fv)),
2316            "text -> help \"say \\\"hello\\\"\""
2317        );
2318    }
2319
2320    fn unit_arg_expr(prefix: Decimal, factors: &[(&str, i32)]) -> UnitArg {
2321        UnitArg::Expr(
2322            prefix,
2323            factors
2324                .iter()
2325                .map(|(measure_ref, exp)| UnitFactor {
2326                    measure_ref: (*measure_ref).to_string(),
2327                    exp: *exp,
2328                })
2329                .collect(),
2330        )
2331    }
2332
2333    #[test]
2334    fn unit_arg_display_metre_per_second() {
2335        let arg = unit_arg_expr(Decimal::ONE, &[("meter", 1), ("second", -1)]);
2336        assert_eq!(format!("{arg}"), "meter/second");
2337        assert!(
2338            !format!("{arg}").contains("second^-1"),
2339            "must not print denominator as negative exponent"
2340        );
2341    }
2342
2343    #[test]
2344    fn unit_arg_display_meter_per_second_squared() {
2345        let arg = unit_arg_expr(Decimal::ONE, &[("meter", 1), ("second", -2)]);
2346        assert_eq!(format!("{arg}"), "meter/second^2");
2347    }
2348
2349    #[test]
2350    fn unit_arg_display_kg_times_mps2() {
2351        let arg = unit_arg_expr(Decimal::ONE, &[("kg", 1), ("mps2", 1)]);
2352        assert_eq!(format!("{arg}"), "kg * mps2");
2353    }
2354
2355    #[test]
2356    fn unit_arg_display_numeric_prefix_metre_per_second() {
2357        use std::str::FromStr;
2358        let prefix = Decimal::from_str("3.6").expect("decimal");
2359        let arg = unit_arg_expr(prefix, &[("meter", 1), ("second", -1)]);
2360        assert_eq!(format!("{arg}"), "3.6 meter/second");
2361    }
2362
2363    #[test]
2364    fn unit_arg_display_metre_per_second_times_kg() {
2365        let arg = unit_arg_expr(Decimal::ONE, &[("meter", 1), ("second", -1), ("kg", 1)]);
2366        assert_eq!(format!("{arg}"), "meter/second * kg");
2367    }
2368
2369    #[test]
2370    fn unit_arg_display_kg_meter_per_second_squared() {
2371        let arg = unit_arg_expr(Decimal::ONE, &[("kg", 1), ("meter", 1), ("second", -2)]);
2372        assert_eq!(format!("{arg}"), "kg * meter/second^2");
2373    }
2374
2375    // ─── Assignment continuation formatting (red until formatter lands) ───────
2376
2377    #[test]
2378    fn format_constraint_as_source_unit_factor_uses_assignment_colon() {
2379        let args = vec![
2380            CommandArg::Label("eur".to_string()),
2381            CommandArg::UnitExpr(UnitArg::Factor(decimal("1"))),
2382        ];
2383        assert_eq!(
2384            format_constraint_as_source(&TypeConstraintCommand::Unit, &args),
2385            "unit eur: 1"
2386        );
2387    }
2388
2389    #[test]
2390    fn format_constraint_as_source_unit_compound_uses_assignment_colon() {
2391        let arg = unit_arg_expr(decimal("3.6"), &[("meter", 1), ("second", -1)]);
2392        let args = vec![
2393            CommandArg::Label("kmh".to_string()),
2394            CommandArg::UnitExpr(arg),
2395        ];
2396        assert_eq!(
2397            format_constraint_as_source(&TypeConstraintCommand::Unit, &args),
2398            "unit kmh: 3.6 meter/second"
2399        );
2400    }
2401
2402    #[test]
2403    fn format_constraint_as_source_unit_factor_one_decimal_uses_assignment_colon() {
2404        let args = vec![
2405            CommandArg::Label("eur".to_string()),
2406            CommandArg::UnitExpr(UnitArg::Factor(decimal("1.00"))),
2407        ];
2408        assert_eq!(
2409            format_constraint_as_source(&TypeConstraintCommand::Unit, &args),
2410            "unit eur: 1.00"
2411        );
2412    }
2413
2414    #[test]
2415    fn as_lemma_source_measure_unit_uses_assignment_colon() {
2416        let fv = DataValue::Definition {
2417            base: Some(ParentType::Primitive {
2418                primitive: PrimitiveKind::Measure,
2419            }),
2420            constraints: Some(vec![
2421                test_constraint(
2422                    TypeConstraintCommand::Unit,
2423                    vec![
2424                        CommandArg::Label("eur".to_string()),
2425                        CommandArg::UnitExpr(UnitArg::Factor(decimal("1.00"))),
2426                    ],
2427                ),
2428                test_constraint(
2429                    TypeConstraintCommand::Unit,
2430                    vec![
2431                        CommandArg::Label("usd".to_string()),
2432                        CommandArg::UnitExpr(UnitArg::Factor(decimal("0.91"))),
2433                    ],
2434                ),
2435            ]),
2436            value: None,
2437        };
2438        assert_eq!(
2439            format!("{}", AsLemmaSource(&fv)),
2440            "measure -> unit eur: 1.00 -> unit usd: 0.91"
2441        );
2442    }
2443
2444    fn decimal(value: &str) -> rust_decimal::Decimal {
2445        use std::str::FromStr;
2446        rust_decimal::Decimal::from_str(value).expect("decimal literal in test")
2447    }
2448}