Skip to main content

lemma/parsing/
ast.rs

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