Skip to main content

lemma/parsing/
ast.rs

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