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