Skip to main content

lemma/parsing/
ast.rs

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