Skip to main content

lemma/parsing/
ast.rs

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