Skip to main content

lemma/parsing/
ast.rs

1//! AST types
2//!
3//! Infrastructure (Span, DepthTracker) and spec/data/rule/expression/value types from parsing.
4//!
5//! # Human `Display` vs canonical `AsLemmaSource`
6//!
7//! [`MetaValue`], [`DataValue`], and [`CommandArg`] use human-oriented
8//! `Display` (stable for `to_string()`, logs, APIs). [`Expression`] and
9//! [`LemmaRule`]/[`LemmaSpec`] use canonical Lemma source for literals via
10//! [`AsLemmaSource`] around [`Value`]. Wrap [`MetaValue`]/[`DataValue`]
11//! in [`AsLemmaSource`] when emitting round-trippable source (e.g. the formatter).
12
13/// Span representing a location in source code
14#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
15pub struct Span {
16    pub start: usize,
17    pub end: usize,
18    pub line: usize,
19    pub col: usize,
20}
21
22/// Tracks expression nesting depth during parsing to prevent stack overflow
23pub struct DepthTracker {
24    depth: usize,
25    max_depth: usize,
26}
27
28impl DepthTracker {
29    pub fn with_max_depth(max_depth: usize) -> Self {
30        Self {
31            depth: 0,
32            max_depth,
33        }
34    }
35
36    /// Returns Ok(()) if within limits, Err(current_depth) if exceeded.
37    pub fn push_depth(&mut self) -> Result<(), usize> {
38        self.depth += 1;
39        if self.depth > self.max_depth {
40            return Err(self.depth);
41        }
42        Ok(())
43    }
44
45    pub fn pop_depth(&mut self) {
46        if self.depth > 0 {
47            self.depth -= 1;
48        }
49    }
50
51    pub fn max_depth(&self) -> usize {
52        self.max_depth
53    }
54}
55
56impl Default for DepthTracker {
57    fn default() -> Self {
58        Self {
59            depth: 0,
60            max_depth: 5,
61        }
62    }
63}
64
65// -----------------------------------------------------------------------------
66// Spec, data, rule, expression and value types
67// -----------------------------------------------------------------------------
68
69use crate::parsing::source::Source;
70use rust_decimal::Decimal;
71use serde::Serialize;
72use std::cmp::Ordering;
73use std::fmt;
74use std::hash::{Hash, Hasher};
75use std::sync::Arc;
76
77pub use crate::literals::{
78    BooleanValue, DateTimeValue, DurationUnit, TimeValue, TimezoneValue, Value,
79};
80
81#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
82pub enum EffectiveDate {
83    Origin,
84    DateTimeValue(crate::DateTimeValue),
85}
86
87impl EffectiveDate {
88    pub fn as_ref(&self) -> Option<&crate::DateTimeValue> {
89        match self {
90            EffectiveDate::Origin => None,
91            EffectiveDate::DateTimeValue(dt) => Some(dt),
92        }
93    }
94
95    pub fn from_option(opt: Option<crate::DateTimeValue>) -> Self {
96        match opt {
97            None => EffectiveDate::Origin,
98            Some(dt) => EffectiveDate::DateTimeValue(dt),
99        }
100    }
101
102    pub fn to_option(&self) -> Option<crate::DateTimeValue> {
103        match self {
104            EffectiveDate::Origin => None,
105            EffectiveDate::DateTimeValue(dt) => Some(dt.clone()),
106        }
107    }
108
109    pub fn is_origin(&self) -> bool {
110        matches!(self, EffectiveDate::Origin)
111    }
112}
113
114impl PartialOrd for EffectiveDate {
115    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
116        Some(self.cmp(other))
117    }
118}
119
120impl Ord for EffectiveDate {
121    // As ref returns None for Origin, so Origin < DateTimeValue(_).
122    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
123        self.as_ref().cmp(&other.as_ref())
124    }
125}
126
127impl fmt::Display for EffectiveDate {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        match self {
130            EffectiveDate::Origin => Ok(()),
131            EffectiveDate::DateTimeValue(dt) => write!(f, "{}", dt),
132        }
133    }
134}
135
136/// A Lemma spec containing data and rules.
137/// Ordered and compared by (name, effective_from) for use in BTreeSet; Origin < DateTimeValue(_).
138#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
139pub struct LemmaSpec {
140    /// Base spec name. Includes `@` for registry specs.
141    pub name: String,
142    /// `true` when the spec was declared with the `@` qualifier (registry spec).
143    pub from_registry: bool,
144    pub effective_from: EffectiveDate,
145    pub attribute: Option<String>,
146    pub start_line: usize,
147    pub commentary: Option<String>,
148    pub data: Vec<LemmaData>,
149    pub rules: Vec<LemmaRule>,
150    pub meta_fields: Vec<MetaField>,
151}
152
153#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
154pub struct MetaField {
155    pub key: String,
156    pub value: MetaValue,
157    pub source_location: Source,
158}
159
160#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
161#[serde(rename_all = "snake_case")]
162pub enum MetaValue {
163    Literal(Value),
164    Unquoted(String),
165}
166
167impl fmt::Display for MetaValue {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        match self {
170            MetaValue::Literal(v) => write!(f, "{}", v),
171            MetaValue::Unquoted(s) => write!(f, "{}", s),
172        }
173    }
174}
175
176impl fmt::Display for MetaField {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        write!(f, "meta {}: {}", self.key, self.value)
179    }
180}
181
182#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
183pub struct LemmaData {
184    pub reference: Reference,
185    pub value: DataValue,
186    pub source_location: Source,
187}
188
189/// An unless clause that provides an alternative result
190///
191/// Unless clauses are evaluated in order, and the last matching condition wins.
192/// This matches natural language: "X unless A then Y, unless B then Z" - if both
193/// A and B are true, Z is returned (the last match).
194#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
195pub struct UnlessClause {
196    pub condition: Expression,
197    pub result: Expression,
198    pub source_location: Source,
199}
200
201/// A rule with a single expression and optional unless clauses
202#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
203pub struct LemmaRule {
204    pub name: String,
205    pub expression: Expression,
206    pub unless_clauses: Vec<UnlessClause>,
207    pub source_location: Source,
208}
209
210/// An expression that can be evaluated, with source location
211///
212/// Expressions use semantic equality - two expressions with the same
213/// structure (kind) are equal regardless of source location.
214/// Hash is not implemented for AST Expression; use planning::semantics::Expression as map keys.
215#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
216pub struct Expression {
217    pub kind: ExpressionKind,
218    pub source_location: Option<Source>,
219}
220
221impl Expression {
222    /// Create a new expression with kind and source location
223    #[must_use]
224    pub fn new(kind: ExpressionKind, source_location: Source) -> Self {
225        Self {
226            kind,
227            source_location: Some(source_location),
228        }
229    }
230}
231
232/// Semantic equality - compares expressions by structure only, ignoring source location
233impl PartialEq for Expression {
234    fn eq(&self, other: &Self) -> bool {
235        self.kind == other.kind
236    }
237}
238
239impl Eq for Expression {}
240
241/// Whether a date is relative to `now` in the past or future direction.
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
243#[serde(rename_all = "snake_case")]
244pub enum DateRelativeKind {
245    InPast,
246    InFuture,
247}
248
249/// Calendar-period membership checks.
250#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
251#[serde(rename_all = "snake_case")]
252pub enum DateCalendarKind {
253    Current,
254    Past,
255    Future,
256    NotIn,
257}
258
259/// Granularity of a calendar-period check.
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
261#[serde(rename_all = "snake_case")]
262pub enum CalendarUnit {
263    Year,
264    Month,
265    Week,
266}
267
268impl fmt::Display for DateRelativeKind {
269    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270        match self {
271            DateRelativeKind::InPast => write!(f, "in past"),
272            DateRelativeKind::InFuture => write!(f, "in future"),
273        }
274    }
275}
276
277impl fmt::Display for DateCalendarKind {
278    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279        match self {
280            DateCalendarKind::Current => write!(f, "in calendar"),
281            DateCalendarKind::Past => write!(f, "in past calendar"),
282            DateCalendarKind::Future => write!(f, "in future calendar"),
283            DateCalendarKind::NotIn => write!(f, "not in calendar"),
284        }
285    }
286}
287
288impl fmt::Display for CalendarUnit {
289    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290        match self {
291            CalendarUnit::Year => write!(f, "year"),
292            CalendarUnit::Month => write!(f, "month"),
293            CalendarUnit::Week => write!(f, "week"),
294        }
295    }
296}
297
298/// The kind/type of expression
299#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
300#[serde(rename_all = "snake_case")]
301pub enum ExpressionKind {
302    /// Parse-time literal value (type will be resolved during planning)
303    Literal(Value),
304    /// Unresolved reference (identifier or dot path). Resolved during planning to DataPath or RulePath.
305    Reference(Reference),
306    /// Unresolved unit literal from parser (resolved during planning)
307    /// Contains (number, unit_name) - the unit name will be resolved to its type during semantic analysis
308    UnresolvedUnitLiteral(Decimal, String),
309    /// The `now` keyword — resolves to the evaluation datetime (= effective).
310    Now,
311    /// Date-relative sugar: `<date_expr> in past [<duration_expr>]` / `<date_expr> in future [<duration_expr>]`
312    /// Fields: (kind, date_expression, optional_tolerance_expression)
313    DateRelative(DateRelativeKind, Arc<Expression>, Option<Arc<Expression>>),
314    /// Calendar-period sugar: `<date_expr> in [past|future] calendar year|month|week`
315    /// Fields: (kind, unit, date_expression)
316    DateCalendar(DateCalendarKind, CalendarUnit, Arc<Expression>),
317    LogicalAnd(Arc<Expression>, Arc<Expression>),
318    Arithmetic(Arc<Expression>, ArithmeticComputation, Arc<Expression>),
319    Comparison(Arc<Expression>, ComparisonComputation, Arc<Expression>),
320    UnitConversion(Arc<Expression>, ConversionTarget),
321    LogicalNegation(Arc<Expression>, NegationType),
322    MathematicalComputation(MathematicalComputation, Arc<Expression>),
323    Veto(VetoExpression),
324}
325
326/// Unresolved reference from parser
327///
328/// Reference to a data or rule (identifier or dot path).
329///
330/// Used in expressions and in LemmaData. During planning, references
331/// are resolved to DataPath or RulePath (semantics layer).
332/// Examples:
333/// - Local "age": segments=[], name="age"
334/// - Cross-spec "employee.salary": segments=["employee"], name="salary"
335#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
336pub struct Reference {
337    pub segments: Vec<String>,
338    pub name: String,
339}
340
341impl Reference {
342    #[must_use]
343    pub fn local(name: String) -> Self {
344        Self {
345            segments: Vec::new(),
346            name,
347        }
348    }
349
350    #[must_use]
351    pub fn from_path(path: Vec<String>) -> Self {
352        if path.is_empty() {
353            Self {
354                segments: Vec::new(),
355                name: String::new(),
356            }
357        } else {
358            // Safe: path is non-empty.
359            let name = path[path.len() - 1].clone();
360            let segments = path[..path.len() - 1].to_vec();
361            Self { segments, name }
362        }
363    }
364
365    #[must_use]
366    pub fn is_local(&self) -> bool {
367        self.segments.is_empty()
368    }
369
370    #[must_use]
371    pub fn full_path(&self) -> Vec<String> {
372        let mut path = self.segments.clone();
373        path.push(self.name.clone());
374        path
375    }
376}
377
378impl fmt::Display for Reference {
379    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380        for segment in &self.segments {
381            write!(f, "{}.", segment)?;
382        }
383        write!(f, "{}", self.name)
384    }
385}
386
387/// Arithmetic computations
388#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
389#[serde(rename_all = "snake_case")]
390pub enum ArithmeticComputation {
391    Add,
392    Subtract,
393    Multiply,
394    Divide,
395    Modulo,
396    Power,
397}
398
399/// Comparison computations
400#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
401#[serde(rename_all = "snake_case")]
402pub enum ComparisonComputation {
403    GreaterThan,
404    LessThan,
405    GreaterThanOrEqual,
406    LessThanOrEqual,
407    Is,
408    IsNot,
409}
410
411impl ComparisonComputation {
412    /// Check if this is an equality comparison (`is`)
413    #[must_use]
414    pub fn is_equal(&self) -> bool {
415        matches!(self, ComparisonComputation::Is)
416    }
417
418    /// Check if this is an inequality comparison (`is not`)
419    #[must_use]
420    pub fn is_not_equal(&self) -> bool {
421        matches!(self, ComparisonComputation::IsNot)
422    }
423}
424
425/// The target unit for unit conversion expressions.
426/// Non-duration units (e.g. "percent", "eur") are stored as Unit and resolved to ratio or scale during planning via the unit index.
427#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
428#[serde(rename_all = "snake_case")]
429pub enum ConversionTarget {
430    Duration(DurationUnit),
431    Unit(String),
432}
433
434/// Types of logical negation
435#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
436#[serde(rename_all = "snake_case")]
437pub enum NegationType {
438    Not,
439}
440
441/// A veto expression that prohibits any valid verdict from the rule
442///
443/// Unlike `reject` (which is just an alias for boolean `false`), a veto
444/// prevents the rule from producing any valid result. This is used for
445/// validation and constraint enforcement.
446///
447/// Example: `veto "Must be over 18"` - blocks the rule entirely with a message
448#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
449pub struct VetoExpression {
450    pub message: Option<String>,
451}
452
453/// Mathematical computations
454#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
455#[serde(rename_all = "snake_case")]
456pub enum MathematicalComputation {
457    Sqrt,
458    Sin,
459    Cos,
460    Tan,
461    Asin,
462    Acos,
463    Atan,
464    Log,
465    Exp,
466    Abs,
467    Floor,
468    Ceil,
469    Round,
470}
471
472/// A reference to a spec, with optional effective datetime.
473/// For registry references the `name` includes the leading `@` (e.g. `@org/repo/spec`);
474/// for local references it is a plain base name. `from_registry` mirrors whether
475/// the source used the `@` qualifier.
476#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
477pub struct SpecRef {
478    /// Spec name as written in source. Includes `@` for registry references.
479    pub name: String,
480    /// `true` when the source used the `@` qualifier (registry reference).
481    pub from_registry: bool,
482    /// Optional effective datetime for temporal resolution.
483    pub effective: Option<DateTimeValue>,
484}
485
486impl std::fmt::Display for SpecRef {
487    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
488        write!(f, "{}", self.name)?;
489        if let Some(d) = &self.effective {
490            write!(f, " {}", d)?;
491        }
492        Ok(())
493    }
494}
495
496impl SpecRef {
497    /// Create a local (non-registry) spec reference.
498    pub fn local(name: impl Into<String>) -> Self {
499        Self {
500            name: name.into(),
501            from_registry: false,
502            effective: None,
503        }
504    }
505
506    /// Create a registry spec reference.
507    pub fn registry(name: impl Into<String>) -> Self {
508        Self {
509            name: name.into(),
510            from_registry: true,
511            effective: None,
512        }
513    }
514
515    pub fn resolution_key(&self) -> String {
516        self.name.clone()
517    }
518
519    /// Resolve the effective instant for this reference given the planning slice's `effective`.
520    /// Explicit qualifier on the reference wins; otherwise inherits the slice instant.
521    pub fn at(&self, effective: &EffectiveDate) -> EffectiveDate {
522        self.effective
523            .clone()
524            .map_or_else(|| effective.clone(), EffectiveDate::DateTimeValue)
525    }
526}
527
528/// A parsed constraint command argument, preserving the literal kind from the
529/// grammar rule `command_arg: { number_literal | boolean_literal | text_literal | label }`.
530///
531/// Two grammatical kinds appear after a constraint command:
532/// - **Literal** — a fully-typed value carrying the literal kind the parser
533///   recognised (`Number`, `Ratio`, `Scale`, `Duration`, `Date`, `Time`,
534///   `Boolean`, `Text`). Stored as the canonical [`crate::literals::Value`]
535///   so downstream consumers match on the variant rather than re-parsing strings.
536/// - **Label** — a bare identifier used as a name (e.g. the unit name `eur`
537///   in `unit eur 1.00`, or a primitive type keyword used as an option label).
538///
539/// Planning validates each command's args against the variant kinds it accepts
540/// and rejects mismatches without coercion (a `Text` literal is never a `Number`,
541/// a `Ratio` literal is never a bare `Number`, etc.).
542#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
543#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
544pub enum CommandArg {
545    /// A typed literal value parsed by [`crate::parsing::parser::Parser::parse_literal_value`].
546    Literal(crate::literals::Value),
547    /// An identifier used as a name (unit name, option keyword, etc.).
548    Label(String),
549}
550
551impl fmt::Display for CommandArg {
552    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
553        match self {
554            CommandArg::Literal(v) => write!(f, "{}", v),
555            CommandArg::Label(s) => write!(f, "{}", s),
556        }
557    }
558}
559
560/// Constraint command for type definitions. Derived from lexer tokens; no string matching.
561#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
562#[serde(rename_all = "snake_case")]
563pub enum TypeConstraintCommand {
564    Help,
565    Default,
566    Unit,
567    Minimum,
568    Maximum,
569    Decimals,
570    Precision,
571    Option,
572    Options,
573    Length,
574}
575
576impl fmt::Display for TypeConstraintCommand {
577    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
578        let s = match self {
579            TypeConstraintCommand::Help => "help",
580            TypeConstraintCommand::Default => "default",
581            TypeConstraintCommand::Unit => "unit",
582            TypeConstraintCommand::Minimum => "minimum",
583            TypeConstraintCommand::Maximum => "maximum",
584            TypeConstraintCommand::Decimals => "decimals",
585            TypeConstraintCommand::Precision => "precision",
586            TypeConstraintCommand::Option => "option",
587            TypeConstraintCommand::Options => "options",
588            TypeConstraintCommand::Length => "length",
589        };
590        write!(f, "{}", s)
591    }
592}
593
594/// Parses a constraint command name. Returns None for unknown (parser returns error).
595#[must_use]
596pub fn try_parse_type_constraint_command(s: &str) -> Option<TypeConstraintCommand> {
597    match s.trim().to_lowercase().as_str() {
598        "help" => Some(TypeConstraintCommand::Help),
599        "default" => Some(TypeConstraintCommand::Default),
600        "unit" => Some(TypeConstraintCommand::Unit),
601        "minimum" => Some(TypeConstraintCommand::Minimum),
602        "maximum" => Some(TypeConstraintCommand::Maximum),
603        "decimals" => Some(TypeConstraintCommand::Decimals),
604        "precision" => Some(TypeConstraintCommand::Precision),
605        "option" => Some(TypeConstraintCommand::Option),
606        "options" => Some(TypeConstraintCommand::Options),
607        "length" => Some(TypeConstraintCommand::Length),
608        _ => None,
609    }
610}
611
612/// A single constraint command and its typed arguments.
613pub type Constraint = (TypeConstraintCommand, Vec<CommandArg>);
614
615#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
616#[serde(rename_all = "snake_case")]
617/// Parse-time data value (before type resolution)
618pub enum DataValue {
619    /// A literal value (parse-time; type will be resolved during planning)
620    Literal(Value),
621    /// A reference to another spec
622    SpecReference(SpecRef),
623    /// A type declaration: `data x: number -> minimum 5` or `data y: x -> minimum 5`
624    TypeDeclaration {
625        base: ParentType,
626        constraints: Option<Vec<Constraint>>,
627        from: Option<SpecRef>,
628    },
629    /// A value-copy reference to another data or rule, with optional additional constraints.
630    ///
631    /// Two surface forms produce this variant:
632    /// 1. **Dotted RHS** in any position — e.g. `data license2: law.other` or
633    ///    `data license2: law.other -> minimum 5`. A dotted RHS is never a
634    ///    typedef name, so it unambiguously means "copy from this data or rule."
635    /// 2. **Non-dotted RHS in a binding LHS** — e.g. `data license.other: src`.
636    ///    When the LHS has segments (a binding path into another spec) the RHS
637    ///    is read as a value-copy reference to a name in the enclosing spec,
638    ///    not as a typedef.
639    ///
640    /// `data x: someident` (LHS without segments, RHS without dots) is the one
641    /// case that stays a `TypeDeclaration` — `someident` is treated as a typedef
642    /// name. See parser `parse_data_value` for the discriminator.
643    ///
644    /// The target is resolved during planning to either a `DataPath` or a `RulePath`.
645    Reference {
646        target: Reference,
647        constraints: Option<Vec<Constraint>>,
648    },
649}
650
651/// Render a chain of `-> command args ...` constraints for display purposes.
652/// Shared between `DataValue::TypeDeclaration` and `DataValue::Reference`.
653fn format_constraint_chain(constraints: &[Constraint]) -> String {
654    constraints
655        .iter()
656        .map(|(cmd, args)| {
657            let args_str: Vec<String> = args.iter().map(|a| a.to_string()).collect();
658            let joined = args_str.join(" ");
659            if joined.is_empty() {
660                format!("{}", cmd)
661            } else {
662                format!("{} {}", cmd, joined)
663            }
664        })
665        .collect::<Vec<_>>()
666        .join(" -> ")
667}
668
669impl fmt::Display for DataValue {
670    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
671        match self {
672            DataValue::Literal(v) => write!(f, "{}", v),
673            DataValue::SpecReference(spec_ref) => {
674                write!(f, "with {}", spec_ref)
675            }
676            DataValue::TypeDeclaration {
677                base,
678                constraints,
679                from,
680            } => {
681                let base_str = if let Some(from_spec) = from {
682                    format!("{} from {}", base, from_spec)
683                } else {
684                    format!("{}", base)
685                };
686                if let Some(ref constraints_vec) = constraints {
687                    let constraint_str = format_constraint_chain(constraints_vec);
688                    write!(f, "{} -> {}", base_str, constraint_str)
689                } else {
690                    write!(f, "{}", base_str)
691                }
692            }
693            DataValue::Reference {
694                target,
695                constraints,
696            } => {
697                if let Some(ref constraints_vec) = constraints {
698                    let constraint_str = format_constraint_chain(constraints_vec);
699                    write!(f, "{} -> {}", target, constraint_str)
700                } else {
701                    write!(f, "{}", target)
702                }
703            }
704        }
705    }
706}
707
708impl LemmaData {
709    #[must_use]
710    pub fn new(reference: Reference, value: DataValue, source_location: Source) -> Self {
711        Self {
712            reference,
713            value,
714            source_location,
715        }
716    }
717}
718
719impl LemmaSpec {
720    #[must_use]
721    pub fn new(name: String) -> Self {
722        let from_registry = name.starts_with('@');
723        Self {
724            name,
725            from_registry,
726            effective_from: EffectiveDate::Origin,
727            attribute: None,
728            start_line: 1,
729            commentary: None,
730            data: Vec::new(),
731            rules: Vec::new(),
732            meta_fields: Vec::new(),
733        }
734    }
735
736    /// Temporal range start. Origin (None) means −∞.
737    pub fn effective_from(&self) -> Option<&DateTimeValue> {
738        self.effective_from.as_ref()
739    }
740
741    #[must_use]
742    pub fn with_attribute(mut self, attribute: String) -> Self {
743        self.attribute = Some(attribute);
744        self
745    }
746
747    #[must_use]
748    pub fn with_start_line(mut self, start_line: usize) -> Self {
749        self.start_line = start_line;
750        self
751    }
752
753    #[must_use]
754    pub fn set_commentary(mut self, commentary: String) -> Self {
755        self.commentary = Some(commentary);
756        self
757    }
758
759    #[must_use]
760    pub fn add_data(mut self, data: LemmaData) -> Self {
761        self.data.push(data);
762        self
763    }
764
765    #[must_use]
766    pub fn add_rule(mut self, rule: LemmaRule) -> Self {
767        self.rules.push(rule);
768        self
769    }
770
771    #[must_use]
772    pub fn add_meta_field(mut self, meta: MetaField) -> Self {
773        self.meta_fields.push(meta);
774        self
775    }
776}
777
778impl PartialEq for LemmaSpec {
779    fn eq(&self, other: &Self) -> bool {
780        self.name == other.name && self.effective_from() == other.effective_from()
781    }
782}
783
784impl Eq for LemmaSpec {}
785
786impl PartialOrd for LemmaSpec {
787    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
788        Some(self.cmp(other))
789    }
790}
791
792impl Ord for LemmaSpec {
793    fn cmp(&self, other: &Self) -> Ordering {
794        (self.name.as_str(), self.effective_from())
795            .cmp(&(other.name.as_str(), other.effective_from()))
796    }
797}
798
799impl Hash for LemmaSpec {
800    fn hash<H: Hasher>(&self, state: &mut H) {
801        self.name.hash(state);
802        match self.effective_from() {
803            Some(d) => d.hash(state),
804            None => 0u8.hash(state),
805        }
806    }
807}
808
809impl fmt::Display for LemmaSpec {
810    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
811        write!(f, "spec {}", self.name)?;
812        if let EffectiveDate::DateTimeValue(ref af) = self.effective_from {
813            write!(f, " {}", af)?;
814        }
815        writeln!(f)?;
816
817        if let Some(ref commentary) = self.commentary {
818            writeln!(f, "\"\"\"")?;
819            writeln!(f, "{}", commentary)?;
820            writeln!(f, "\"\"\"")?;
821        }
822
823        if !self.data.is_empty() {
824            writeln!(f)?;
825            for data in &self.data {
826                write!(f, "{}", data)?;
827            }
828        }
829
830        if !self.rules.is_empty() {
831            writeln!(f)?;
832            for (index, rule) in self.rules.iter().enumerate() {
833                if index > 0 {
834                    writeln!(f)?;
835                }
836                write!(f, "{}", rule)?;
837            }
838        }
839
840        if !self.meta_fields.is_empty() {
841            writeln!(f)?;
842            for meta in &self.meta_fields {
843                writeln!(f, "{}", meta)?;
844            }
845        }
846
847        Ok(())
848    }
849}
850
851impl fmt::Display for LemmaData {
852    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
853        writeln!(f, "data {}: {}", self.reference, self.value)
854    }
855}
856
857impl fmt::Display for LemmaRule {
858    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
859        write!(f, "rule {}: {}", self.name, self.expression)?;
860        for unless_clause in &self.unless_clauses {
861            write!(
862                f,
863                "\n  unless {} then {}",
864                unless_clause.condition, unless_clause.result
865            )?;
866        }
867        writeln!(f)?;
868        Ok(())
869    }
870}
871
872/// Precedence level for an expression kind.
873///
874/// Higher values bind tighter. Used by `Expression::Display` and the formatter
875/// to insert parentheses only where needed.
876pub fn expression_precedence(kind: &ExpressionKind) -> u8 {
877    match kind {
878        ExpressionKind::LogicalAnd(..) => 2,
879        ExpressionKind::LogicalNegation(..) => 3,
880        ExpressionKind::Comparison(..) => 4,
881        ExpressionKind::UnitConversion(..) => 4,
882        ExpressionKind::Arithmetic(_, op, _) => match op {
883            ArithmeticComputation::Add | ArithmeticComputation::Subtract => 5,
884            ArithmeticComputation::Multiply
885            | ArithmeticComputation::Divide
886            | ArithmeticComputation::Modulo => 6,
887            ArithmeticComputation::Power => 7,
888        },
889        ExpressionKind::MathematicalComputation(..) => 8,
890        ExpressionKind::DateRelative(..) | ExpressionKind::DateCalendar(..) => 4,
891        ExpressionKind::Literal(..)
892        | ExpressionKind::Reference(..)
893        | ExpressionKind::UnresolvedUnitLiteral(..)
894        | ExpressionKind::Now
895        | ExpressionKind::Veto(..) => 10,
896    }
897}
898
899fn write_expression_child(
900    f: &mut fmt::Formatter<'_>,
901    child: &Expression,
902    parent_prec: u8,
903) -> fmt::Result {
904    let child_prec = expression_precedence(&child.kind);
905    if child_prec < parent_prec {
906        write!(f, "({})", child)
907    } else {
908        write!(f, "{}", child)
909    }
910}
911
912impl fmt::Display for Expression {
913    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
914        match &self.kind {
915            ExpressionKind::Literal(lit) => write!(f, "{}", AsLemmaSource(lit)),
916            ExpressionKind::Reference(r) => write!(f, "{}", r),
917            ExpressionKind::Arithmetic(left, op, right) => {
918                let my_prec = expression_precedence(&self.kind);
919                write_expression_child(f, left, my_prec)?;
920                write!(f, " {} ", op)?;
921                write_expression_child(f, right, my_prec)
922            }
923            ExpressionKind::Comparison(left, op, right) => {
924                let my_prec = expression_precedence(&self.kind);
925                write_expression_child(f, left, my_prec)?;
926                write!(f, " {} ", op)?;
927                write_expression_child(f, right, my_prec)
928            }
929            ExpressionKind::UnitConversion(value, target) => {
930                let my_prec = expression_precedence(&self.kind);
931                write_expression_child(f, value, my_prec)?;
932                write!(f, " in {}", target)
933            }
934            ExpressionKind::LogicalNegation(expr, _) => {
935                let my_prec = expression_precedence(&self.kind);
936                write!(f, "not ")?;
937                write_expression_child(f, expr, my_prec)
938            }
939            ExpressionKind::LogicalAnd(left, right) => {
940                let my_prec = expression_precedence(&self.kind);
941                write_expression_child(f, left, my_prec)?;
942                write!(f, " and ")?;
943                write_expression_child(f, right, my_prec)
944            }
945            ExpressionKind::MathematicalComputation(op, operand) => {
946                let my_prec = expression_precedence(&self.kind);
947                write!(f, "{} ", op)?;
948                write_expression_child(f, operand, my_prec)
949            }
950            ExpressionKind::Veto(veto) => match &veto.message {
951                Some(msg) => write!(f, "veto {}", quote_lemma_text(msg)),
952                None => write!(f, "veto"),
953            },
954            ExpressionKind::UnresolvedUnitLiteral(number, unit_name) => {
955                write!(f, "{} {}", format_decimal_source(number), unit_name)
956            }
957            ExpressionKind::Now => write!(f, "now"),
958            ExpressionKind::DateRelative(kind, date_expr, tolerance) => {
959                write!(f, "{} {}", date_expr, kind)?;
960                if let Some(tol) = tolerance {
961                    write!(f, " {}", tol)?;
962                }
963                Ok(())
964            }
965            ExpressionKind::DateCalendar(kind, unit, date_expr) => {
966                write!(f, "{} {} {}", date_expr, kind, unit)
967            }
968        }
969    }
970}
971
972impl fmt::Display for ConversionTarget {
973    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
974        match self {
975            ConversionTarget::Duration(unit) => write!(f, "{}", unit),
976            ConversionTarget::Unit(unit) => write!(f, "{}", unit),
977        }
978    }
979}
980
981impl fmt::Display for ArithmeticComputation {
982    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
983        match self {
984            ArithmeticComputation::Add => write!(f, "+"),
985            ArithmeticComputation::Subtract => write!(f, "-"),
986            ArithmeticComputation::Multiply => write!(f, "*"),
987            ArithmeticComputation::Divide => write!(f, "/"),
988            ArithmeticComputation::Modulo => write!(f, "%"),
989            ArithmeticComputation::Power => write!(f, "^"),
990        }
991    }
992}
993
994impl fmt::Display for ComparisonComputation {
995    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
996        match self {
997            ComparisonComputation::GreaterThan => write!(f, ">"),
998            ComparisonComputation::LessThan => write!(f, "<"),
999            ComparisonComputation::GreaterThanOrEqual => write!(f, ">="),
1000            ComparisonComputation::LessThanOrEqual => write!(f, "<="),
1001            ComparisonComputation::Is => write!(f, "is"),
1002            ComparisonComputation::IsNot => write!(f, "is not"),
1003        }
1004    }
1005}
1006
1007impl fmt::Display for MathematicalComputation {
1008    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1009        match self {
1010            MathematicalComputation::Sqrt => write!(f, "sqrt"),
1011            MathematicalComputation::Sin => write!(f, "sin"),
1012            MathematicalComputation::Cos => write!(f, "cos"),
1013            MathematicalComputation::Tan => write!(f, "tan"),
1014            MathematicalComputation::Asin => write!(f, "asin"),
1015            MathematicalComputation::Acos => write!(f, "acos"),
1016            MathematicalComputation::Atan => write!(f, "atan"),
1017            MathematicalComputation::Log => write!(f, "log"),
1018            MathematicalComputation::Exp => write!(f, "exp"),
1019            MathematicalComputation::Abs => write!(f, "abs"),
1020            MathematicalComputation::Floor => write!(f, "floor"),
1021            MathematicalComputation::Ceil => write!(f, "ceil"),
1022            MathematicalComputation::Round => write!(f, "round"),
1023        }
1024    }
1025}
1026
1027// -----------------------------------------------------------------------------
1028// Primitive type kinds and parent type references
1029// -----------------------------------------------------------------------------
1030
1031/// Built-in primitive type kind. Single source of truth for type keywords.
1032#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1033#[serde(rename_all = "snake_case")]
1034pub enum PrimitiveKind {
1035    Boolean,
1036    Scale,
1037    Number,
1038    Percent,
1039    Ratio,
1040    Text,
1041    Date,
1042    Time,
1043    Duration,
1044}
1045
1046impl std::fmt::Display for PrimitiveKind {
1047    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1048        let s = match self {
1049            PrimitiveKind::Boolean => "boolean",
1050            PrimitiveKind::Scale => "scale",
1051            PrimitiveKind::Number => "number",
1052            PrimitiveKind::Percent => "percent",
1053            PrimitiveKind::Ratio => "ratio",
1054            PrimitiveKind::Text => "text",
1055            PrimitiveKind::Date => "date",
1056            PrimitiveKind::Time => "time",
1057            PrimitiveKind::Duration => "duration",
1058        };
1059        write!(f, "{}", s)
1060    }
1061}
1062
1063/// Parent type in a type definition: built-in primitive or custom type name.
1064///
1065/// `name` is the declared type name (the data name that introduces this type).
1066/// For `data temperature: scale`, name = "temperature", primitive = Scale.
1067#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1068#[serde(tag = "kind", rename_all = "snake_case")]
1069pub enum ParentType {
1070    Primitive { primitive: PrimitiveKind },
1071    Custom { name: String },
1072}
1073
1074impl std::fmt::Display for ParentType {
1075    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1076        match self {
1077            ParentType::Primitive { primitive } => write!(f, "{}", primitive),
1078            ParentType::Custom { name } => write!(f, "{}", name),
1079        }
1080    }
1081}
1082
1083// =============================================================================
1084// AsLemmaSource<Value> — canonical literal formatting
1085// =============================================================================
1086
1087/// Wrap a value to emit canonical Lemma source (round-trippable). See module docs.
1088pub struct AsLemmaSource<'a, T: ?Sized>(pub &'a T);
1089
1090/// Escape a string and wrap it in double quotes for Lemma source output.
1091/// Handles `\` and `"` escaping.
1092pub fn quote_lemma_text(s: &str) -> String {
1093    let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
1094    format!("\"{}\"", escaped)
1095}
1096
1097/// Format a Decimal for Lemma source, preserving precision (trailing zeros).
1098/// Strips the fractional part only when it is zero (e.g. `100` stays `"100"`,
1099/// `1.00` stays `"1.00"`). Inserts underscore separators in the integer part
1100/// when it has 4+ digits (e.g. `30000000.50` → `"30_000_000.50"`).
1101fn format_decimal_source(n: &Decimal) -> String {
1102    let raw = if n.fract().is_zero() {
1103        n.trunc().to_string()
1104    } else {
1105        n.to_string()
1106    };
1107    group_digits(&raw)
1108}
1109
1110/// Insert `_` every 3 digits in the integer part of a numeric string.
1111/// Handles optional leading `-`/`+` sign and optional fractional part.
1112/// Only groups when the integer part has 4 or more digits.
1113fn group_digits(s: &str) -> String {
1114    let (sign, rest) = if s.starts_with('-') || s.starts_with('+') {
1115        (&s[..1], &s[1..])
1116    } else {
1117        ("", s)
1118    };
1119
1120    let (int_part, frac_part) = match rest.find('.') {
1121        Some(pos) => (&rest[..pos], &rest[pos..]),
1122        None => (rest, ""),
1123    };
1124
1125    if int_part.len() < 4 {
1126        return s.to_string();
1127    }
1128
1129    let mut grouped = String::with_capacity(int_part.len() + int_part.len() / 3);
1130    for (i, ch) in int_part.chars().enumerate() {
1131        let digits_remaining = int_part.len() - i;
1132        if i > 0 && digits_remaining % 3 == 0 {
1133            grouped.push('_');
1134        }
1135        grouped.push(ch);
1136    }
1137
1138    format!("{}{}{}", sign, grouped, frac_part)
1139}
1140
1141impl<'a> fmt::Display for AsLemmaSource<'a, CommandArg> {
1142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1143        use crate::literals::Value;
1144        match self.0 {
1145            CommandArg::Literal(Value::Text(s)) => write!(f, "{}", quote_lemma_text(s)),
1146            CommandArg::Literal(Value::Number(d)) => {
1147                write!(f, "{}", group_digits(&d.to_string()))
1148            }
1149            CommandArg::Literal(Value::Boolean(bv)) => write!(f, "{}", bv),
1150            CommandArg::Literal(Value::Scale(d, unit)) => {
1151                write!(f, "{} {}", group_digits(&d.to_string()), unit)
1152            }
1153            CommandArg::Literal(Value::Duration(d, unit)) => {
1154                write!(f, "{} {}", group_digits(&d.to_string()), unit)
1155            }
1156            CommandArg::Literal(value @ Value::Ratio(_, _)) => write!(f, "{}", value),
1157            CommandArg::Literal(Value::Date(dt)) => write!(f, "{}", dt),
1158            CommandArg::Literal(Value::Time(t)) => write!(f, "{}", t),
1159            CommandArg::Label(s) => write!(f, "{}", s),
1160        }
1161    }
1162}
1163
1164/// Format a single constraint command and its args as valid Lemma source.
1165fn format_constraint_as_source(cmd: &TypeConstraintCommand, args: &[CommandArg]) -> String {
1166    if args.is_empty() {
1167        cmd.to_string()
1168    } else {
1169        let args_str: Vec<String> = args
1170            .iter()
1171            .map(|a| format!("{}", AsLemmaSource(a)))
1172            .collect();
1173        format!("{} {}", cmd, args_str.join(" "))
1174    }
1175}
1176
1177/// Format a constraint list as valid Lemma source.
1178/// Returns the `cmd arg -> cmd arg` portion joined by `separator`.
1179fn format_constraints_as_source(constraints: &[Constraint], separator: &str) -> String {
1180    constraints
1181        .iter()
1182        .map(|(cmd, args)| format_constraint_as_source(cmd, args))
1183        .collect::<Vec<_>>()
1184        .join(separator)
1185}
1186
1187// -- Display for AsLemmaSource<Value> ----------------------------------------
1188
1189impl<'a> fmt::Display for AsLemmaSource<'a, Value> {
1190    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1191        match self.0 {
1192            Value::Number(n) => write!(f, "{}", format_decimal_source(n)),
1193            Value::Text(s) => write!(f, "{}", quote_lemma_text(s)),
1194            Value::Date(dt) => {
1195                let is_date_only =
1196                    dt.hour == 0 && dt.minute == 0 && dt.second == 0 && dt.timezone.is_none();
1197                if is_date_only {
1198                    write!(f, "{:04}-{:02}-{:02}", dt.year, dt.month, dt.day)
1199                } else {
1200                    write!(
1201                        f,
1202                        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
1203                        dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second
1204                    )?;
1205                    if let Some(tz) = &dt.timezone {
1206                        write!(f, "{}", tz)?;
1207                    }
1208                    Ok(())
1209                }
1210            }
1211            Value::Time(t) => {
1212                write!(f, "{:02}:{:02}:{:02}", t.hour, t.minute, t.second)?;
1213                if let Some(tz) = &t.timezone {
1214                    write!(f, "{}", tz)?;
1215                }
1216                Ok(())
1217            }
1218            Value::Boolean(b) => write!(f, "{}", b),
1219            Value::Scale(n, u) => write!(f, "{} {}", format_decimal_source(n), u),
1220            Value::Duration(n, u) => write!(f, "{} {}", format_decimal_source(n), u),
1221            Value::Ratio(n, unit) => match unit.as_deref() {
1222                Some("percent") => {
1223                    let display_value = *n * Decimal::from(100);
1224                    write!(f, "{}%", format_decimal_source(&display_value))
1225                }
1226                Some("permille") => {
1227                    let display_value = *n * Decimal::from(1000);
1228                    write!(f, "{}%%", format_decimal_source(&display_value))
1229                }
1230                Some(unit_name) => write!(f, "{} {}", format_decimal_source(n), unit_name),
1231                None => write!(f, "{}", format_decimal_source(n)),
1232            },
1233        }
1234    }
1235}
1236
1237// -- AsLemmaSource: MetaValue, DataValue (formatter / round-trip) ---
1238
1239impl<'a> fmt::Display for AsLemmaSource<'a, MetaValue> {
1240    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1241        match self.0 {
1242            MetaValue::Literal(v) => write!(f, "{}", AsLemmaSource(v)),
1243            MetaValue::Unquoted(s) => write!(f, "{}", s),
1244        }
1245    }
1246}
1247
1248impl<'a> fmt::Display for AsLemmaSource<'a, DataValue> {
1249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1250        match self.0 {
1251            DataValue::Literal(v) => write!(f, "{}", AsLemmaSource(v)),
1252            DataValue::SpecReference(spec_ref) => {
1253                write!(f, "with {}", spec_ref)
1254            }
1255            DataValue::TypeDeclaration {
1256                base,
1257                constraints,
1258                from,
1259            } => {
1260                let base_str = if let Some(from_spec) = from {
1261                    format!("{} from {}", base, from_spec)
1262                } else {
1263                    format!("{}", base)
1264                };
1265                if let Some(ref constraints_vec) = constraints {
1266                    let constraint_str = format_constraints_as_source(constraints_vec, " -> ");
1267                    write!(f, "{} -> {}", base_str, constraint_str)
1268                } else {
1269                    write!(f, "{}", base_str)
1270                }
1271            }
1272            DataValue::Reference {
1273                target,
1274                constraints,
1275            } => {
1276                if let Some(ref constraints_vec) = constraints {
1277                    let constraint_str = format_constraints_as_source(constraints_vec, " -> ");
1278                    write!(f, "{} -> {}", target, constraint_str)
1279                } else {
1280                    write!(f, "{}", target)
1281                }
1282            }
1283        }
1284    }
1285}
1286
1287#[cfg(test)]
1288mod tests {
1289    use super::*;
1290
1291    #[test]
1292    fn test_duration_unit_display() {
1293        assert_eq!(format!("{}", DurationUnit::Second), "seconds");
1294        assert_eq!(format!("{}", DurationUnit::Minute), "minutes");
1295        assert_eq!(format!("{}", DurationUnit::Hour), "hours");
1296        assert_eq!(format!("{}", DurationUnit::Day), "days");
1297        assert_eq!(format!("{}", DurationUnit::Week), "weeks");
1298        assert_eq!(format!("{}", DurationUnit::Millisecond), "milliseconds");
1299        assert_eq!(format!("{}", DurationUnit::Microsecond), "microseconds");
1300    }
1301
1302    #[test]
1303    fn test_conversion_target_display() {
1304        assert_eq!(
1305            format!("{}", ConversionTarget::Duration(DurationUnit::Hour)),
1306            "hours"
1307        );
1308        assert_eq!(
1309            format!("{}", ConversionTarget::Unit("usd".to_string())),
1310            "usd"
1311        );
1312    }
1313
1314    #[test]
1315    fn test_value_ratio_display() {
1316        use rust_decimal::Decimal;
1317        use std::str::FromStr;
1318        let percent = Value::Ratio(
1319            Decimal::from_str("0.10").unwrap(),
1320            Some("percent".to_string()),
1321        );
1322        assert_eq!(format!("{}", percent), "10%");
1323        let permille = Value::Ratio(
1324            Decimal::from_str("0.005").unwrap(),
1325            Some("permille".to_string()),
1326        );
1327        assert_eq!(format!("{}", permille), "5%%");
1328    }
1329
1330    #[test]
1331    fn test_datetime_value_display() {
1332        let dt = DateTimeValue {
1333            year: 2024,
1334            month: 12,
1335            day: 25,
1336            hour: 14,
1337            minute: 30,
1338            second: 45,
1339            microsecond: 0,
1340            timezone: Some(TimezoneValue {
1341                offset_hours: 1,
1342                offset_minutes: 0,
1343            }),
1344        };
1345        assert_eq!(format!("{}", dt), "2024-12-25T14:30:45+01:00");
1346    }
1347
1348    #[test]
1349    fn test_datetime_value_display_date_only() {
1350        let dt = DateTimeValue {
1351            year: 2026,
1352            month: 3,
1353            day: 4,
1354            hour: 0,
1355            minute: 0,
1356            second: 0,
1357            microsecond: 0,
1358            timezone: None,
1359        };
1360        assert_eq!(format!("{}", dt), "2026-03-04");
1361    }
1362
1363    #[test]
1364    fn test_datetime_value_display_microseconds() {
1365        let dt = DateTimeValue {
1366            year: 2026,
1367            month: 2,
1368            day: 23,
1369            hour: 14,
1370            minute: 30,
1371            second: 45,
1372            microsecond: 123456,
1373            timezone: Some(TimezoneValue {
1374                offset_hours: 0,
1375                offset_minutes: 0,
1376            }),
1377        };
1378        assert_eq!(format!("{}", dt), "2026-02-23T14:30:45.123456Z");
1379    }
1380
1381    #[test]
1382    fn test_datetime_microsecond_in_ordering() {
1383        let a = DateTimeValue {
1384            year: 2026,
1385            month: 1,
1386            day: 1,
1387            hour: 0,
1388            minute: 0,
1389            second: 0,
1390            microsecond: 100,
1391            timezone: None,
1392        };
1393        let b = DateTimeValue {
1394            year: 2026,
1395            month: 1,
1396            day: 1,
1397            hour: 0,
1398            minute: 0,
1399            second: 0,
1400            microsecond: 200,
1401            timezone: None,
1402        };
1403        assert!(a < b);
1404    }
1405
1406    #[test]
1407    fn test_datetime_parse_iso_week() {
1408        let dt: DateTimeValue = "2026-W01".parse().unwrap();
1409        assert_eq!(dt.year, 2025);
1410        assert_eq!(dt.month, 12);
1411        assert_eq!(dt.day, 29);
1412        assert_eq!(dt.microsecond, 0);
1413    }
1414
1415    #[test]
1416    fn test_negation_types() {
1417        let json = serde_json::to_string(&NegationType::Not).expect("serialize NegationType");
1418        let decoded: NegationType = serde_json::from_str(&json).expect("deserialize NegationType");
1419        assert_eq!(decoded, NegationType::Not);
1420    }
1421
1422    #[test]
1423    fn parent_type_primitive_serde_internally_tagged() {
1424        let p = ParentType::Primitive {
1425            primitive: PrimitiveKind::Number,
1426        };
1427        let json = serde_json::to_string(&p).expect("ParentType::Primitive must serialize");
1428        assert!(json.contains("\"kind\"") && json.contains("\"primitive\""));
1429        let back: ParentType = serde_json::from_str(&json).expect("deserialize");
1430        assert_eq!(back, p);
1431    }
1432
1433    // =====================================================================
1434    // DataValue Display — constraint formatting
1435    // =====================================================================
1436
1437    fn text_arg(s: &str) -> CommandArg {
1438        CommandArg::Literal(crate::literals::Value::Text(s.to_string()))
1439    }
1440
1441    fn number_arg(s: &str) -> CommandArg {
1442        let d: rust_decimal::Decimal = s.parse().expect("decimal");
1443        CommandArg::Literal(crate::literals::Value::Number(d))
1444    }
1445
1446    fn boolean_arg(b: BooleanValue) -> CommandArg {
1447        CommandArg::Literal(crate::literals::Value::Boolean(b))
1448    }
1449
1450    fn scale_arg(value: &str, unit: &str) -> CommandArg {
1451        let d: rust_decimal::Decimal = value.parse().expect("decimal");
1452        CommandArg::Literal(crate::literals::Value::Scale(d, unit.to_string()))
1453    }
1454
1455    fn duration_arg(value: &str, unit: DurationUnit) -> CommandArg {
1456        let d: rust_decimal::Decimal = value.parse().expect("decimal");
1457        CommandArg::Literal(crate::literals::Value::Duration(d, unit))
1458    }
1459
1460    #[test]
1461    fn as_lemma_source_text_default_is_quoted() {
1462        let fv = DataValue::TypeDeclaration {
1463            base: ParentType::Primitive {
1464                primitive: PrimitiveKind::Text,
1465            },
1466            constraints: Some(vec![(
1467                TypeConstraintCommand::Default,
1468                vec![text_arg("single")],
1469            )]),
1470            from: None,
1471        };
1472        assert_eq!(
1473            format!("{}", AsLemmaSource(&fv)),
1474            "text -> default \"single\""
1475        );
1476    }
1477
1478    #[test]
1479    fn as_lemma_source_number_default_not_quoted() {
1480        let fv = DataValue::TypeDeclaration {
1481            base: ParentType::Primitive {
1482                primitive: PrimitiveKind::Number,
1483            },
1484            constraints: Some(vec![(
1485                TypeConstraintCommand::Default,
1486                vec![number_arg("10")],
1487            )]),
1488            from: None,
1489        };
1490        assert_eq!(format!("{}", AsLemmaSource(&fv)), "number -> default 10");
1491    }
1492
1493    #[test]
1494    fn as_lemma_source_help_always_quoted() {
1495        let fv = DataValue::TypeDeclaration {
1496            base: ParentType::Primitive {
1497                primitive: PrimitiveKind::Number,
1498            },
1499            constraints: Some(vec![(
1500                TypeConstraintCommand::Help,
1501                vec![text_arg("Enter a quantity")],
1502            )]),
1503            from: None,
1504        };
1505        assert_eq!(
1506            format!("{}", AsLemmaSource(&fv)),
1507            "number -> help \"Enter a quantity\""
1508        );
1509    }
1510
1511    #[test]
1512    fn as_lemma_source_text_option_quoted() {
1513        let fv = DataValue::TypeDeclaration {
1514            base: ParentType::Primitive {
1515                primitive: PrimitiveKind::Text,
1516            },
1517            constraints: Some(vec![
1518                (TypeConstraintCommand::Option, vec![text_arg("active")]),
1519                (TypeConstraintCommand::Option, vec![text_arg("inactive")]),
1520            ]),
1521            from: None,
1522        };
1523        assert_eq!(
1524            format!("{}", AsLemmaSource(&fv)),
1525            "text -> option \"active\" -> option \"inactive\""
1526        );
1527    }
1528
1529    #[test]
1530    fn as_lemma_source_scale_unit_not_quoted() {
1531        let fv = DataValue::TypeDeclaration {
1532            base: ParentType::Primitive {
1533                primitive: PrimitiveKind::Scale,
1534            },
1535            constraints: Some(vec![
1536                (
1537                    TypeConstraintCommand::Unit,
1538                    vec![CommandArg::Label("eur".to_string()), number_arg("1.00")],
1539                ),
1540                (
1541                    TypeConstraintCommand::Unit,
1542                    vec![CommandArg::Label("usd".to_string()), number_arg("1.10")],
1543                ),
1544            ]),
1545            from: None,
1546        };
1547        assert_eq!(
1548            format!("{}", AsLemmaSource(&fv)),
1549            "scale -> unit eur 1.00 -> unit usd 1.10"
1550        );
1551    }
1552
1553    #[test]
1554    fn as_lemma_source_scale_minimum_with_unit() {
1555        let fv = DataValue::TypeDeclaration {
1556            base: ParentType::Primitive {
1557                primitive: PrimitiveKind::Scale,
1558            },
1559            constraints: Some(vec![(
1560                TypeConstraintCommand::Minimum,
1561                vec![scale_arg("0", "eur")],
1562            )]),
1563            from: None,
1564        };
1565        assert_eq!(format!("{}", AsLemmaSource(&fv)), "scale -> minimum 0 eur");
1566    }
1567
1568    #[test]
1569    fn as_lemma_source_boolean_default() {
1570        let fv = DataValue::TypeDeclaration {
1571            base: ParentType::Primitive {
1572                primitive: PrimitiveKind::Boolean,
1573            },
1574            constraints: Some(vec![(
1575                TypeConstraintCommand::Default,
1576                vec![boolean_arg(BooleanValue::True)],
1577            )]),
1578            from: None,
1579        };
1580        assert_eq!(format!("{}", AsLemmaSource(&fv)), "boolean -> default true");
1581    }
1582
1583    #[test]
1584    fn as_lemma_source_duration_default() {
1585        let fv = DataValue::TypeDeclaration {
1586            base: ParentType::Primitive {
1587                primitive: PrimitiveKind::Duration,
1588            },
1589            constraints: Some(vec![(
1590                TypeConstraintCommand::Default,
1591                vec![duration_arg("40", DurationUnit::Hour)],
1592            )]),
1593            from: None,
1594        };
1595        assert_eq!(
1596            format!("{}", AsLemmaSource(&fv)),
1597            "duration -> default 40 hours"
1598        );
1599    }
1600
1601    #[test]
1602    fn as_lemma_source_named_type_default_quoted() {
1603        // Named types (user-defined): the parser produces a typed Text literal for
1604        // quoted default values like `default "single"`.
1605        let fv = DataValue::TypeDeclaration {
1606            base: ParentType::Custom {
1607                name: "filing_status_type".to_string(),
1608            },
1609            constraints: Some(vec![(
1610                TypeConstraintCommand::Default,
1611                vec![text_arg("single")],
1612            )]),
1613            from: None,
1614        };
1615        assert_eq!(
1616            format!("{}", AsLemmaSource(&fv)),
1617            "filing_status_type -> default \"single\""
1618        );
1619    }
1620
1621    #[test]
1622    fn as_lemma_source_help_escapes_quotes() {
1623        let fv = DataValue::TypeDeclaration {
1624            base: ParentType::Primitive {
1625                primitive: PrimitiveKind::Text,
1626            },
1627            constraints: Some(vec![(
1628                TypeConstraintCommand::Help,
1629                vec![text_arg("say \"hello\"")],
1630            )]),
1631            from: None,
1632        };
1633        assert_eq!(
1634            format!("{}", AsLemmaSource(&fv)),
1635            "text -> help \"say \\\"hello\\\"\""
1636        );
1637    }
1638}