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    /// Struct variant (`primitive` field) so `#[serde(tag = "kind")]` merges with a JSON object.
1067    Primitive { primitive: PrimitiveKind },
1068    /// Struct variant so `#[serde(tag = "kind")]` merges with a JSON object.
1069    Custom { name: String },
1070}
1071
1072impl std::fmt::Display for ParentType {
1073    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1074        match self {
1075            ParentType::Primitive { primitive: k } => write!(f, "{}", k),
1076            ParentType::Custom { name } => write!(f, "{}", name),
1077        }
1078    }
1079}
1080
1081// -----------------------------------------------------------------------------
1082// Type definition (named, import, or inline)
1083// -----------------------------------------------------------------------------
1084
1085/// Type definition (named, import, or inline).
1086/// Applying constraints to produce TypeSpecification is done in planning (semantics).
1087#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1088#[serde(tag = "kind", rename_all = "snake_case")]
1089pub enum TypeDef {
1090    Regular {
1091        source_location: Source,
1092        name: String,
1093        parent: ParentType,
1094        constraints: Option<Vec<Constraint>>,
1095    },
1096    Import {
1097        source_location: Source,
1098        name: String,
1099        source_type: String,
1100        from: SpecRef,
1101        constraints: Option<Vec<Constraint>>,
1102    },
1103    Inline {
1104        source_location: Source,
1105        parent: ParentType,
1106        constraints: Option<Vec<Constraint>>,
1107        fact_ref: Reference,
1108        from: Option<SpecRef>,
1109    },
1110}
1111
1112impl TypeDef {
1113    pub fn source_location(&self) -> &Source {
1114        match self {
1115            TypeDef::Regular {
1116                source_location, ..
1117            }
1118            | TypeDef::Import {
1119                source_location, ..
1120            }
1121            | TypeDef::Inline {
1122                source_location, ..
1123            } => source_location,
1124        }
1125    }
1126
1127    pub fn name(&self) -> String {
1128        match self {
1129            TypeDef::Regular { name, .. } | TypeDef::Import { name, .. } => name.clone(),
1130            TypeDef::Inline { parent, .. } => format!("{}", parent),
1131        }
1132    }
1133}
1134
1135impl fmt::Display for TypeDef {
1136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1137        match self {
1138            TypeDef::Regular {
1139                name,
1140                parent,
1141                constraints,
1142                ..
1143            } => {
1144                write!(f, "type {}: {}", name, parent)?;
1145                if let Some(constraints) = constraints {
1146                    for (cmd, args) in constraints {
1147                        write!(f, "\n  -> {}", cmd)?;
1148                        for arg in args {
1149                            write!(f, " {}", arg.value())?;
1150                        }
1151                    }
1152                }
1153                Ok(())
1154            }
1155            TypeDef::Import {
1156                name,
1157                from,
1158                constraints,
1159                ..
1160            } => {
1161                write!(f, "type {} from {}", name, from)?;
1162                if let Some(constraints) = constraints {
1163                    for (cmd, args) in constraints {
1164                        write!(f, " -> {}", cmd)?;
1165                        for arg in args {
1166                            write!(f, " {}", arg.value())?;
1167                        }
1168                    }
1169                }
1170                Ok(())
1171            }
1172            TypeDef::Inline { .. } => Ok(()),
1173        }
1174    }
1175}
1176
1177// =============================================================================
1178// AsLemmaSource — wrapper for valid, round-trippable Lemma source output
1179// =============================================================================
1180
1181/// Wrapper that selects the "emit valid Lemma source" `Display` implementation.
1182///
1183/// The regular `Display` on AST types is for human-readable output. Wrap a
1184/// reference in `AsLemmaSource` when you need syntactically valid Lemma that
1185/// can be parsed back (round-trip).
1186///
1187/// # Example
1188/// ```ignore
1189/// let s = format!("{}", AsLemmaSource(&fact_value));
1190/// ```
1191pub struct AsLemmaSource<'a, T: ?Sized>(pub &'a T);
1192
1193/// Escape a string and wrap it in double quotes for Lemma source output.
1194/// Handles `\` and `"` escaping.
1195pub fn quote_lemma_text(s: &str) -> String {
1196    let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
1197    format!("\"{}\"", escaped)
1198}
1199
1200/// Format a Decimal for Lemma source: normalize, remove trailing zeros,
1201/// strip the fractional part when it is zero (e.g. `100.00` → `"100"`),
1202/// and insert underscore separators in the integer part when it has 4+
1203/// digits (e.g. `30000000.50` → `"30_000_000.50"`).
1204fn format_decimal_source(n: &Decimal) -> String {
1205    let norm = n.normalize();
1206    let raw = if norm.fract().is_zero() {
1207        norm.trunc().to_string()
1208    } else {
1209        norm.to_string()
1210    };
1211    group_digits(&raw)
1212}
1213
1214/// Insert `_` every 3 digits in the integer part of a numeric string.
1215/// Handles optional leading `-`/`+` sign and optional fractional part.
1216/// Only groups when the integer part has 4 or more digits.
1217fn group_digits(s: &str) -> String {
1218    let (sign, rest) = if s.starts_with('-') || s.starts_with('+') {
1219        (&s[..1], &s[1..])
1220    } else {
1221        ("", s)
1222    };
1223
1224    let (int_part, frac_part) = match rest.find('.') {
1225        Some(pos) => (&rest[..pos], &rest[pos..]),
1226        None => (rest, ""),
1227    };
1228
1229    if int_part.len() < 4 {
1230        return s.to_string();
1231    }
1232
1233    let mut grouped = String::with_capacity(int_part.len() + int_part.len() / 3);
1234    for (i, ch) in int_part.chars().enumerate() {
1235        let digits_remaining = int_part.len() - i;
1236        if i > 0 && digits_remaining % 3 == 0 {
1237            grouped.push('_');
1238        }
1239        grouped.push(ch);
1240    }
1241
1242    format!("{}{}{}", sign, grouped, frac_part)
1243}
1244
1245// -- Display for AsLemmaSource<CommandArg> ------------------------------------
1246
1247impl<'a> fmt::Display for AsLemmaSource<'a, CommandArg> {
1248    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1249        match self.0 {
1250            CommandArg::Text(s) => write!(f, "{}", quote_lemma_text(s)),
1251            CommandArg::Number(s) => {
1252                let clean: String = s.chars().filter(|c| *c != '_' && *c != ',').collect();
1253                write!(f, "{}", group_digits(&clean))
1254            }
1255            CommandArg::Boolean(bv) => write!(f, "{}", bv),
1256            CommandArg::Label(s) => write!(f, "{}", s),
1257        }
1258    }
1259}
1260
1261/// Format a single constraint command and its args as valid Lemma source.
1262///
1263/// Each `CommandArg` already knows its literal kind (from parsing), so formatting
1264/// is simply delegated to `AsLemmaSource<CommandArg>` — no lookup table needed.
1265fn format_constraint_as_source(cmd: &TypeConstraintCommand, args: &[CommandArg]) -> String {
1266    if args.is_empty() {
1267        cmd.to_string()
1268    } else {
1269        let args_str: Vec<String> = args
1270            .iter()
1271            .map(|a| format!("{}", AsLemmaSource(a)))
1272            .collect();
1273        format!("{} {}", cmd, args_str.join(" "))
1274    }
1275}
1276
1277/// Format a constraint list as valid Lemma source.
1278/// Returns the `cmd arg -> cmd arg` portion joined by `separator`.
1279fn format_constraints_as_source(constraints: &[Constraint], separator: &str) -> String {
1280    constraints
1281        .iter()
1282        .map(|(cmd, args)| format_constraint_as_source(cmd, args))
1283        .collect::<Vec<_>>()
1284        .join(separator)
1285}
1286
1287// -- Display for AsLemmaSource<FactValue> ------------------------------------
1288
1289impl<'a> fmt::Display for AsLemmaSource<'a, FactValue> {
1290    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1291        match self.0 {
1292            FactValue::Literal(v) => write!(f, "{}", AsLemmaSource(v)),
1293            FactValue::SpecReference(spec_ref) => {
1294                write!(f, "spec {}", spec_ref)
1295            }
1296            FactValue::TypeDeclaration {
1297                base,
1298                constraints,
1299                from,
1300            } => {
1301                let base_str = if let Some(from_spec) = from {
1302                    format!("{} from {}", base, from_spec)
1303                } else {
1304                    format!("{}", base)
1305                };
1306                if let Some(ref constraints_vec) = constraints {
1307                    let constraint_str = format_constraints_as_source(constraints_vec, " -> ");
1308                    write!(f, "[{} -> {}]", base_str, constraint_str)
1309                } else {
1310                    write!(f, "[{}]", base_str)
1311                }
1312            }
1313        }
1314    }
1315}
1316
1317// -- Display for AsLemmaSource<Value> ----------------------------------------
1318
1319impl<'a> fmt::Display for AsLemmaSource<'a, Value> {
1320    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1321        match self.0 {
1322            Value::Number(n) => write!(f, "{}", format_decimal_source(n)),
1323            Value::Text(s) => write!(f, "{}", quote_lemma_text(s)),
1324            Value::Date(dt) => {
1325                let is_date_only =
1326                    dt.hour == 0 && dt.minute == 0 && dt.second == 0 && dt.timezone.is_none();
1327                if is_date_only {
1328                    write!(f, "{:04}-{:02}-{:02}", dt.year, dt.month, dt.day)
1329                } else {
1330                    write!(
1331                        f,
1332                        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
1333                        dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second
1334                    )?;
1335                    if let Some(tz) = &dt.timezone {
1336                        write!(f, "{}", tz)?;
1337                    }
1338                    Ok(())
1339                }
1340            }
1341            Value::Time(t) => {
1342                write!(f, "{:02}:{:02}:{:02}", t.hour, t.minute, t.second)?;
1343                if let Some(tz) = &t.timezone {
1344                    write!(f, "{}", tz)?;
1345                }
1346                Ok(())
1347            }
1348            Value::Boolean(b) => write!(f, "{}", b),
1349            Value::Scale(n, u) => write!(f, "{} {}", format_decimal_source(n), u),
1350            Value::Duration(n, u) => write!(f, "{} {}", format_decimal_source(n), u),
1351            Value::Ratio(n, unit) => match unit.as_deref() {
1352                Some("percent") => {
1353                    let display_value = *n * Decimal::from(100);
1354                    write!(f, "{}%", format_decimal_source(&display_value))
1355                }
1356                Some("permille") => {
1357                    let display_value = *n * Decimal::from(1000);
1358                    write!(f, "{}%%", format_decimal_source(&display_value))
1359                }
1360                Some(unit_name) => write!(f, "{} {}", format_decimal_source(n), unit_name),
1361                None => write!(f, "{}", format_decimal_source(n)),
1362            },
1363        }
1364    }
1365}
1366
1367// -- Display for AsLemmaSource<MetaValue> ------------------------------------
1368
1369impl<'a> fmt::Display for AsLemmaSource<'a, MetaValue> {
1370    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1371        match self.0 {
1372            MetaValue::Literal(v) => write!(f, "{}", AsLemmaSource(v)),
1373            MetaValue::Unquoted(s) => write!(f, "{}", s),
1374        }
1375    }
1376}
1377
1378// -- Display for AsLemmaSource<TypeDef> --------------------------------------
1379
1380impl<'a> fmt::Display for AsLemmaSource<'a, TypeDef> {
1381    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1382        match self.0 {
1383            TypeDef::Regular {
1384                name,
1385                parent,
1386                constraints,
1387                ..
1388            } => {
1389                write!(f, "type {}: {}", name, parent)?;
1390                if let Some(constraints) = constraints {
1391                    for (cmd, args) in constraints {
1392                        write!(f, "\n  -> {}", format_constraint_as_source(cmd, args))?;
1393                    }
1394                }
1395                Ok(())
1396            }
1397            TypeDef::Import {
1398                name,
1399                from,
1400                constraints,
1401                ..
1402            } => {
1403                write!(f, "type {} from {}", name, from)?;
1404                if let Some(constraints) = constraints {
1405                    for (cmd, args) in constraints {
1406                        write!(f, " -> {}", format_constraint_as_source(cmd, args))?;
1407                    }
1408                }
1409                Ok(())
1410            }
1411            TypeDef::Inline { .. } => Ok(()),
1412        }
1413    }
1414}
1415
1416#[cfg(test)]
1417mod tests {
1418    use super::*;
1419
1420    #[test]
1421    fn test_duration_unit_display() {
1422        assert_eq!(format!("{}", DurationUnit::Second), "seconds");
1423        assert_eq!(format!("{}", DurationUnit::Minute), "minutes");
1424        assert_eq!(format!("{}", DurationUnit::Hour), "hours");
1425        assert_eq!(format!("{}", DurationUnit::Day), "days");
1426        assert_eq!(format!("{}", DurationUnit::Week), "weeks");
1427        assert_eq!(format!("{}", DurationUnit::Millisecond), "milliseconds");
1428        assert_eq!(format!("{}", DurationUnit::Microsecond), "microseconds");
1429    }
1430
1431    #[test]
1432    fn test_conversion_target_display() {
1433        assert_eq!(
1434            format!("{}", ConversionTarget::Duration(DurationUnit::Hour)),
1435            "hours"
1436        );
1437        assert_eq!(
1438            format!("{}", ConversionTarget::Unit("usd".to_string())),
1439            "usd"
1440        );
1441    }
1442
1443    #[test]
1444    fn test_value_ratio_display() {
1445        use rust_decimal::Decimal;
1446        use std::str::FromStr;
1447        let percent = Value::Ratio(
1448            Decimal::from_str("0.10").unwrap(),
1449            Some("percent".to_string()),
1450        );
1451        assert_eq!(format!("{}", percent), "10%");
1452        let permille = Value::Ratio(
1453            Decimal::from_str("0.005").unwrap(),
1454            Some("permille".to_string()),
1455        );
1456        assert_eq!(format!("{}", permille), "5%%");
1457    }
1458
1459    #[test]
1460    fn test_datetime_value_display() {
1461        let dt = DateTimeValue {
1462            year: 2024,
1463            month: 12,
1464            day: 25,
1465            hour: 14,
1466            minute: 30,
1467            second: 45,
1468            microsecond: 0,
1469            timezone: Some(TimezoneValue {
1470                offset_hours: 1,
1471                offset_minutes: 0,
1472            }),
1473        };
1474        assert_eq!(format!("{}", dt), "2024-12-25T14:30:45+01:00");
1475    }
1476
1477    #[test]
1478    fn test_datetime_value_display_date_only() {
1479        let dt = DateTimeValue {
1480            year: 2026,
1481            month: 3,
1482            day: 4,
1483            hour: 0,
1484            minute: 0,
1485            second: 0,
1486            microsecond: 0,
1487            timezone: None,
1488        };
1489        assert_eq!(format!("{}", dt), "2026-03-04");
1490    }
1491
1492    #[test]
1493    fn test_datetime_value_display_microseconds() {
1494        let dt = DateTimeValue {
1495            year: 2026,
1496            month: 2,
1497            day: 23,
1498            hour: 14,
1499            minute: 30,
1500            second: 45,
1501            microsecond: 123456,
1502            timezone: Some(TimezoneValue {
1503                offset_hours: 0,
1504                offset_minutes: 0,
1505            }),
1506        };
1507        assert_eq!(format!("{}", dt), "2026-02-23T14:30:45.123456Z");
1508    }
1509
1510    #[test]
1511    fn test_datetime_microsecond_in_ordering() {
1512        let a = DateTimeValue {
1513            year: 2026,
1514            month: 1,
1515            day: 1,
1516            hour: 0,
1517            minute: 0,
1518            second: 0,
1519            microsecond: 100,
1520            timezone: None,
1521        };
1522        let b = DateTimeValue {
1523            year: 2026,
1524            month: 1,
1525            day: 1,
1526            hour: 0,
1527            minute: 0,
1528            second: 0,
1529            microsecond: 200,
1530            timezone: None,
1531        };
1532        assert!(a < b);
1533    }
1534
1535    #[test]
1536    fn test_datetime_parse_iso_week() {
1537        let dt: DateTimeValue = "2026-W01".parse().unwrap();
1538        assert_eq!(dt.year, 2025);
1539        assert_eq!(dt.month, 12);
1540        assert_eq!(dt.day, 29);
1541        assert_eq!(dt.microsecond, 0);
1542    }
1543
1544    #[test]
1545    fn test_time_value_display() {
1546        let time = TimeValue {
1547            hour: 14,
1548            minute: 30,
1549            second: 45,
1550            timezone: Some(TimezoneValue {
1551                offset_hours: -5,
1552                offset_minutes: 30,
1553            }),
1554        };
1555        let display = format!("{}", time);
1556        assert!(display.contains("14"));
1557        assert!(display.contains("30"));
1558        assert!(display.contains("45"));
1559    }
1560
1561    #[test]
1562    fn test_timezone_value() {
1563        let tz_positive = TimezoneValue {
1564            offset_hours: 5,
1565            offset_minutes: 30,
1566        };
1567        assert_eq!(tz_positive.offset_hours, 5);
1568        assert_eq!(tz_positive.offset_minutes, 30);
1569
1570        let tz_negative = TimezoneValue {
1571            offset_hours: -8,
1572            offset_minutes: 0,
1573        };
1574        assert_eq!(tz_negative.offset_hours, -8);
1575    }
1576
1577    #[test]
1578    fn test_negation_types() {
1579        let json = serde_json::to_string(&NegationType::Not).expect("serialize NegationType");
1580        let decoded: NegationType = serde_json::from_str(&json).expect("deserialize NegationType");
1581        assert_eq!(decoded, NegationType::Not);
1582    }
1583
1584    #[test]
1585    fn parent_type_primitive_serde_internally_tagged() {
1586        let p = ParentType::Primitive {
1587            primitive: PrimitiveKind::Number,
1588        };
1589        let json = serde_json::to_string(&p).expect("ParentType::Primitive must serialize");
1590        assert!(json.contains("\"kind\"") && json.contains("\"primitive\""));
1591        let back: ParentType = serde_json::from_str(&json).expect("deserialize");
1592        assert_eq!(back, p);
1593    }
1594
1595    #[test]
1596    fn parent_type_custom_serde_internally_tagged() {
1597        let p = ParentType::Custom {
1598            name: "money".to_string(),
1599        };
1600        let json = serde_json::to_string(&p).expect("ParentType::Custom must serialize");
1601        assert!(json.contains("\"kind\"") && json.contains("\"name\""));
1602        let back: ParentType = serde_json::from_str(&json).expect("deserialize");
1603        assert_eq!(back, p);
1604    }
1605
1606    #[test]
1607    fn test_veto_expression() {
1608        let veto_with_message = VetoExpression {
1609            message: Some("Must be over 18".to_string()),
1610        };
1611        assert_eq!(
1612            veto_with_message.message,
1613            Some("Must be over 18".to_string())
1614        );
1615
1616        let veto_without_message = VetoExpression { message: None };
1617        assert!(veto_without_message.message.is_none());
1618    }
1619
1620    // test_expression_get_source_text_with_location (uses Value instead of LiteralValue now)
1621    // test_expression_get_source_text_no_location (uses Value instead of LiteralValue now)
1622    // test_expression_get_source_text_source_not_found (uses Value instead of LiteralValue now)
1623
1624    // =====================================================================
1625    // AsLemmaSource — constraint formatting tests
1626    // =====================================================================
1627
1628    #[test]
1629    fn as_lemma_source_text_default_is_quoted() {
1630        let fv = FactValue::TypeDeclaration {
1631            base: ParentType::Primitive {
1632                primitive: PrimitiveKind::Text,
1633            },
1634            constraints: Some(vec![(
1635                TypeConstraintCommand::Default,
1636                vec![CommandArg::Text("single".to_string())],
1637            )]),
1638            from: None,
1639        };
1640        assert_eq!(
1641            format!("{}", AsLemmaSource(&fv)),
1642            "[text -> default \"single\"]"
1643        );
1644    }
1645
1646    #[test]
1647    fn as_lemma_source_number_default_not_quoted() {
1648        let fv = FactValue::TypeDeclaration {
1649            base: ParentType::Primitive {
1650                primitive: PrimitiveKind::Number,
1651            },
1652            constraints: Some(vec![(
1653                TypeConstraintCommand::Default,
1654                vec![CommandArg::Number("10".to_string())],
1655            )]),
1656            from: None,
1657        };
1658        assert_eq!(format!("{}", AsLemmaSource(&fv)), "[number -> default 10]");
1659    }
1660
1661    #[test]
1662    fn as_lemma_source_help_always_quoted() {
1663        let fv = FactValue::TypeDeclaration {
1664            base: ParentType::Primitive {
1665                primitive: PrimitiveKind::Number,
1666            },
1667            constraints: Some(vec![(
1668                TypeConstraintCommand::Help,
1669                vec![CommandArg::Text("Enter a quantity".to_string())],
1670            )]),
1671            from: None,
1672        };
1673        assert_eq!(
1674            format!("{}", AsLemmaSource(&fv)),
1675            "[number -> help \"Enter a quantity\"]"
1676        );
1677    }
1678
1679    #[test]
1680    fn as_lemma_source_text_option_quoted() {
1681        let fv = FactValue::TypeDeclaration {
1682            base: ParentType::Primitive {
1683                primitive: PrimitiveKind::Text,
1684            },
1685            constraints: Some(vec![
1686                (
1687                    TypeConstraintCommand::Option,
1688                    vec![CommandArg::Text("active".to_string())],
1689                ),
1690                (
1691                    TypeConstraintCommand::Option,
1692                    vec![CommandArg::Text("inactive".to_string())],
1693                ),
1694            ]),
1695            from: None,
1696        };
1697        assert_eq!(
1698            format!("{}", AsLemmaSource(&fv)),
1699            "[text -> option \"active\" -> option \"inactive\"]"
1700        );
1701    }
1702
1703    #[test]
1704    fn as_lemma_source_scale_unit_not_quoted() {
1705        let fv = FactValue::TypeDeclaration {
1706            base: ParentType::Primitive {
1707                primitive: PrimitiveKind::Scale,
1708            },
1709            constraints: Some(vec![
1710                (
1711                    TypeConstraintCommand::Unit,
1712                    vec![
1713                        CommandArg::Label("eur".to_string()),
1714                        CommandArg::Number("1.00".to_string()),
1715                    ],
1716                ),
1717                (
1718                    TypeConstraintCommand::Unit,
1719                    vec![
1720                        CommandArg::Label("usd".to_string()),
1721                        CommandArg::Number("1.10".to_string()),
1722                    ],
1723                ),
1724            ]),
1725            from: None,
1726        };
1727        assert_eq!(
1728            format!("{}", AsLemmaSource(&fv)),
1729            "[scale -> unit eur 1.00 -> unit usd 1.10]"
1730        );
1731    }
1732
1733    #[test]
1734    fn as_lemma_source_scale_minimum_with_unit() {
1735        let fv = FactValue::TypeDeclaration {
1736            base: ParentType::Primitive {
1737                primitive: PrimitiveKind::Scale,
1738            },
1739            constraints: Some(vec![(
1740                TypeConstraintCommand::Minimum,
1741                vec![
1742                    CommandArg::Number("0".to_string()),
1743                    CommandArg::Label("eur".to_string()),
1744                ],
1745            )]),
1746            from: None,
1747        };
1748        assert_eq!(
1749            format!("{}", AsLemmaSource(&fv)),
1750            "[scale -> minimum 0 eur]"
1751        );
1752    }
1753
1754    #[test]
1755    fn as_lemma_source_boolean_default() {
1756        let fv = FactValue::TypeDeclaration {
1757            base: ParentType::Primitive {
1758                primitive: PrimitiveKind::Boolean,
1759            },
1760            constraints: Some(vec![(
1761                TypeConstraintCommand::Default,
1762                vec![CommandArg::Boolean(BooleanValue::True)],
1763            )]),
1764            from: None,
1765        };
1766        assert_eq!(
1767            format!("{}", AsLemmaSource(&fv)),
1768            "[boolean -> default true]"
1769        );
1770    }
1771
1772    #[test]
1773    fn as_lemma_source_duration_default() {
1774        let fv = FactValue::TypeDeclaration {
1775            base: ParentType::Primitive {
1776                primitive: PrimitiveKind::Duration,
1777            },
1778            constraints: Some(vec![(
1779                TypeConstraintCommand::Default,
1780                vec![
1781                    CommandArg::Number("40".to_string()),
1782                    CommandArg::Label("hours".to_string()),
1783                ],
1784            )]),
1785            from: None,
1786        };
1787        assert_eq!(
1788            format!("{}", AsLemmaSource(&fv)),
1789            "[duration -> default 40 hours]"
1790        );
1791    }
1792
1793    #[test]
1794    fn as_lemma_source_named_type_default_quoted() {
1795        // Named types (user-defined): the parser produces CommandArg::Text for
1796        // quoted default values like `default "single"`.
1797        let fv = FactValue::TypeDeclaration {
1798            base: ParentType::Custom {
1799                name: "filing_status_type".to_string(),
1800            },
1801            constraints: Some(vec![(
1802                TypeConstraintCommand::Default,
1803                vec![CommandArg::Text("single".to_string())],
1804            )]),
1805            from: None,
1806        };
1807        assert_eq!(
1808            format!("{}", AsLemmaSource(&fv)),
1809            "[filing_status_type -> default \"single\"]"
1810        );
1811    }
1812
1813    #[test]
1814    fn as_lemma_source_help_escapes_quotes() {
1815        let fv = FactValue::TypeDeclaration {
1816            base: ParentType::Primitive {
1817                primitive: PrimitiveKind::Text,
1818            },
1819            constraints: Some(vec![(
1820                TypeConstraintCommand::Help,
1821                vec![CommandArg::Text("say \"hello\"".to_string())],
1822            )]),
1823            from: None,
1824        };
1825        assert_eq!(
1826            format!("{}", AsLemmaSource(&fv)),
1827            "[text -> help \"say \\\"hello\\\"\"]"
1828        );
1829    }
1830
1831    #[test]
1832    fn as_lemma_source_typedef_regular_options_quoted() {
1833        let td = TypeDef::Regular {
1834            source_location: Source::new(
1835                "test",
1836                Span {
1837                    start: 0,
1838                    end: 0,
1839                    line: 1,
1840                    col: 0,
1841                },
1842            ),
1843            name: "status".to_string(),
1844            parent: ParentType::Primitive {
1845                primitive: PrimitiveKind::Text,
1846            },
1847            constraints: Some(vec![
1848                (
1849                    TypeConstraintCommand::Option,
1850                    vec![CommandArg::Text("active".to_string())],
1851                ),
1852                (
1853                    TypeConstraintCommand::Option,
1854                    vec![CommandArg::Text("inactive".to_string())],
1855                ),
1856            ]),
1857        };
1858        let output = format!("{}", AsLemmaSource(&td));
1859        assert!(output.contains("option \"active\""), "got: {}", output);
1860        assert!(output.contains("option \"inactive\""), "got: {}", output);
1861    }
1862
1863    #[test]
1864    fn as_lemma_source_typedef_scale_units_not_quoted() {
1865        let td = TypeDef::Regular {
1866            source_location: Source::new(
1867                "test",
1868                Span {
1869                    start: 0,
1870                    end: 0,
1871                    line: 1,
1872                    col: 0,
1873                },
1874            ),
1875            name: "money".to_string(),
1876            parent: ParentType::Primitive {
1877                primitive: PrimitiveKind::Scale,
1878            },
1879            constraints: Some(vec![
1880                (
1881                    TypeConstraintCommand::Unit,
1882                    vec![
1883                        CommandArg::Label("eur".to_string()),
1884                        CommandArg::Number("1.00".to_string()),
1885                    ],
1886                ),
1887                (
1888                    TypeConstraintCommand::Decimals,
1889                    vec![CommandArg::Number("2".to_string())],
1890                ),
1891                (
1892                    TypeConstraintCommand::Minimum,
1893                    vec![CommandArg::Number("0".to_string())],
1894                ),
1895            ]),
1896        };
1897        let output = format!("{}", AsLemmaSource(&td));
1898        assert!(output.contains("unit eur 1.00"), "got: {}", output);
1899        assert!(output.contains("decimals 2"), "got: {}", output);
1900        assert!(output.contains("minimum 0"), "got: {}", output);
1901    }
1902}