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