Skip to main content

lemma/parsing/
ast.rs

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