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