Skip to main content

lemma/parsing/
ast.rs

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