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