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, CalendarUnit, DateTimeValue, 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/// Textual repository qualifier as written in source (for example `@lemma/std`).
225/// `name` stores the qualifier verbatim, including a leading `@` when present. The planner
226/// resolves 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 CalendarPeriodUnit {
384    Year,
385    Month,
386    Week,
387}
388
389impl CalendarPeriodUnit {
390    #[must_use]
391    pub fn from_keyword(s: &str) -> Option<Self> {
392        match s.trim().to_lowercase().as_str() {
393            "year" | "years" => Some(Self::Year),
394            "month" | "months" => Some(Self::Month),
395            "week" | "weeks" => Some(Self::Week),
396            _ => None,
397        }
398    }
399}
400
401impl fmt::Display for DateRelativeKind {
402    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403        match self {
404            DateRelativeKind::InPast => write!(f, "in past"),
405            DateRelativeKind::InFuture => write!(f, "in future"),
406        }
407    }
408}
409
410impl fmt::Display for DateCalendarKind {
411    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
412        match self {
413            DateCalendarKind::Current => write!(f, "in calendar"),
414            DateCalendarKind::Past => write!(f, "in past calendar"),
415            DateCalendarKind::Future => write!(f, "in future calendar"),
416            DateCalendarKind::NotIn => write!(f, "not in calendar"),
417        }
418    }
419}
420
421impl fmt::Display for CalendarPeriodUnit {
422    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
423        match self {
424            CalendarPeriodUnit::Year => write!(f, "year"),
425            CalendarPeriodUnit::Month => write!(f, "month"),
426            CalendarPeriodUnit::Week => write!(f, "week"),
427        }
428    }
429}
430
431/// The kind/type of expression
432#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
433#[serde(rename_all = "snake_case")]
434pub enum ExpressionKind {
435    /// Parse-time literal value (type will be resolved during planning)
436    Literal(Value),
437    /// Unresolved reference (identifier or dot path). Resolved during planning to DataPath or RulePath.
438    Reference(Reference),
439    /// The `now` keyword — resolves to the evaluation datetime (= effective).
440    Now,
441    /// Date-relative sugar: `<date_expr> in past` / `<date_expr> in future`
442    /// Fields: (kind, date_expression)
443    DateRelative(DateRelativeKind, Arc<Expression>),
444    /// Calendar-period sugar: `<date_expr> in [past|future] calendar year|month|week`
445    /// Fields: (kind, unit, date_expression)
446    DateCalendar(DateCalendarKind, CalendarPeriodUnit, Arc<Expression>),
447    /// Range literal: `{left_expr}...{right_expr}`
448    RangeLiteral(Arc<Expression>, Arc<Expression>),
449    /// Relative date range: `past 7 days` / `future 30 days`
450    PastFutureRange(DateRelativeKind, Arc<Expression>),
451    /// Range containment: `{value_expr} in {range_expr}`
452    RangeContainment(Arc<Expression>, Arc<Expression>),
453    LogicalAnd(Arc<Expression>, Arc<Expression>),
454    Arithmetic(Arc<Expression>, ArithmeticComputation, Arc<Expression>),
455    Comparison(Arc<Expression>, ComparisonComputation, Arc<Expression>),
456    UnitConversion(Arc<Expression>, ConversionTarget),
457    LogicalNegation(Arc<Expression>, NegationType),
458    MathematicalComputation(MathematicalComputation, Arc<Expression>),
459    Veto(VetoExpression),
460    /// `expr is veto` / `veto is expr` — boolean: whether evaluating `expr` yields `OperationResult::Veto`.
461    ResultIsVeto(Arc<Expression>),
462}
463
464/// Unresolved reference from parser
465///
466/// Reference to a data or rule (identifier or dot path).
467///
468/// Used in expressions and in LemmaData. During planning, references
469/// are resolved to DataPath or RulePath (semantics layer).
470/// Examples:
471/// - Local "age": segments=[], name="age"
472/// - Cross-spec "employee.salary": segments=["employee"], name="salary"
473#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
474pub struct Reference {
475    pub segments: Vec<String>,
476    pub name: String,
477}
478
479impl Reference {
480    #[must_use]
481    pub fn local(name: String) -> Self {
482        Self {
483            segments: Vec::new(),
484            name,
485        }
486    }
487
488    #[must_use]
489    pub fn from_path(path: Vec<String>) -> Self {
490        if path.is_empty() {
491            Self {
492                segments: Vec::new(),
493                name: String::new(),
494            }
495        } else {
496            // Safe: path is non-empty.
497            let name = path[path.len() - 1].clone();
498            let segments = path[..path.len() - 1].to_vec();
499            Self { segments, name }
500        }
501    }
502
503    #[must_use]
504    pub fn is_local(&self) -> bool {
505        self.segments.is_empty()
506    }
507
508    #[must_use]
509    pub fn full_path(&self) -> Vec<String> {
510        let mut path = self.segments.clone();
511        path.push(self.name.clone());
512        path
513    }
514}
515
516impl fmt::Display for Reference {
517    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
518        for segment in &self.segments {
519            write!(f, "{}.", segment)?;
520        }
521        write!(f, "{}", self.name)
522    }
523}
524
525/// Arithmetic computations
526#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
527#[serde(rename_all = "snake_case")]
528pub enum ArithmeticComputation {
529    Add,
530    Subtract,
531    Multiply,
532    Divide,
533    Modulo,
534    Power,
535}
536
537/// Comparison computations
538#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
539#[serde(rename_all = "snake_case")]
540pub enum ComparisonComputation {
541    GreaterThan,
542    LessThan,
543    GreaterThanOrEqual,
544    LessThanOrEqual,
545    Is,
546    IsNot,
547}
548
549impl ComparisonComputation {
550    /// Check if this is an equality comparison (`is`)
551    #[must_use]
552    pub fn is_equal(&self) -> bool {
553        matches!(self, ComparisonComputation::Is)
554    }
555
556    /// Check if this is an inequality comparison (`is not`)
557    #[must_use]
558    pub fn is_not_equal(&self) -> bool {
559        matches!(self, ComparisonComputation::IsNot)
560    }
561}
562
563/// The target unit for unit conversion expressions.
564/// Non-calendar units (for example `percent`, `eur`, `hours`) are stored as [`ConversionTarget::Unit`]
565/// and resolved to ratio or quantity during planning via the unit index.
566/// Type targets (for example `number`) strip units and return a bare value.
567#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
568#[serde(rename_all = "snake_case")]
569pub enum ConversionTarget {
570    Calendar(CalendarUnit),
571    Unit(String),
572    Type(PrimitiveKind),
573}
574
575/// Types of logical negation
576#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
577#[serde(rename_all = "snake_case")]
578pub enum NegationType {
579    Not,
580}
581
582/// A veto expression that prohibits any valid verdict from the rule
583///
584/// Unlike `reject` (which is just an alias for boolean `false`), a veto
585/// prevents the rule from producing any valid result. This is used for
586/// validation and constraint enforcement.
587///
588/// Example: `veto "Must be over 18"` - blocks the rule entirely with a message
589#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
590pub struct VetoExpression {
591    pub message: Option<String>,
592}
593
594/// Mathematical computations
595#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
596#[serde(rename_all = "snake_case")]
597pub enum MathematicalComputation {
598    Sqrt,
599    Sin,
600    Cos,
601    Tan,
602    Asin,
603    Acos,
604    Atan,
605    Log,
606    Exp,
607    Abs,
608    Floor,
609    Ceil,
610    Round,
611}
612
613/// A spec reference written in source.
614///
615/// `name` is the bare spec name (no `@`, no dots, no slashes).
616/// [`SpecRef::repository`] is `None` for same-repository references, or
617/// `Some(RepositoryQualifier)` when a repository qualifier was written before the spec name.
618/// `effective` carries an optional explicit pin written next to the spec name.
619#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
620pub struct SpecRef {
621    /// Optional explicit repository qualifier. `None` means the reference resolves against
622    /// the consumer spec's own repository.
623    pub repository: Option<RepositoryQualifier>,
624    /// The spec name.
625    pub name: String,
626    /// Optional explicit effective datetime pin written in source.
627    pub effective: Option<DateTimeValue>,
628    /// Source span of the repository qualifier (when `repository` is present).
629    #[serde(default, skip_serializing_if = "Option::is_none")]
630    pub repository_span: Option<Span>,
631    /// Source span of `name` and optional `effective`.
632    #[serde(default, skip_serializing_if = "Option::is_none")]
633    pub target_span: Option<Span>,
634}
635
636impl std::fmt::Display for SpecRef {
637    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
638        if let Some(qualifier) = &self.repository {
639            write!(f, "{} ", qualifier)?;
640        }
641        write!(f, "{}", self.name)?;
642        if let Some(d) = &self.effective {
643            write!(f, " {}", d)?;
644        }
645        Ok(())
646    }
647}
648
649impl SpecRef {
650    /// Same-repository reference: resolution uses the consumer's repository.
651    pub fn same_repository(name: impl Into<String>) -> Self {
652        Self {
653            name: name.into(),
654            repository: None,
655            effective: None,
656            repository_span: None,
657            target_span: None,
658        }
659    }
660
661    /// Cross-repository reference with an explicit repository qualifier.
662    pub fn cross_repository(name: impl Into<String>, qualifier: RepositoryQualifier) -> Self {
663        Self {
664            name: name.into(),
665            repository: Some(qualifier),
666            effective: None,
667            repository_span: None,
668            target_span: None,
669        }
670    }
671
672    /// Resolve the effective instant for this reference given the planning slice's `effective`.
673    /// Explicit qualifier on the reference wins; otherwise inherits the slice instant.
674    pub fn at(&self, effective: &EffectiveDate) -> EffectiveDate {
675        self.effective
676            .clone()
677            .map_or_else(|| effective.clone(), EffectiveDate::DateTimeValue)
678    }
679}
680
681/// A single factor in a compound unit expression.
682///
683/// `quantity_ref` is the name of the referenced unit (e.g. `"meter"`, `"second"`).
684/// `exp` is the integer exponent, positive for numerator and negative for denominator.
685/// For example `meter/second^2` produces:
686/// - `UnitFactor { quantity_ref: "meter", exp: 1 }`
687/// - `UnitFactor { quantity_ref: "second", exp: -2 }`
688#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
689pub struct UnitFactor {
690    pub quantity_ref: String,
691    pub exp: i32,
692}
693
694/// The argument to a `-> unit <name> ...` command, either a plain numeric
695/// conversion factor or a compound unit expression.
696///
697/// - `Factor(v)` — simple unit: `-> unit meter 1`, `-> unit kilometer 1000`
698/// - `Expr(prefix, factors)` — compound unit: `-> unit mps meter/second`,
699///   `-> unit kmh 3.6 meter/second`
700///   The `prefix` is an additional scalar multiplier beyond what the unit
701///   factor references contribute; it defaults to `1` when omitted.
702#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
703pub enum UnitArg {
704    Factor(Decimal),
705    Expr(Decimal, Vec<UnitFactor>),
706}
707
708impl fmt::Display for UnitArg {
709    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
710        match self {
711            UnitArg::Factor(v) => write!(f, "{}", v),
712            UnitArg::Expr(prefix, factors) => {
713                if *prefix != Decimal::ONE {
714                    write!(f, "{} ", prefix)?;
715                }
716                for (index, factor) in factors.iter().enumerate() {
717                    if factor.exp == 0 {
718                        unreachable!("BUG: unit factor exponent cannot be zero");
719                    }
720                    if factor.exp > 0 {
721                        if index > 0 {
722                            write!(f, " * ")?;
723                        }
724                        write!(f, "{}", factor.quantity_ref)?;
725                        if factor.exp != 1 {
726                            write!(f, "^{}", factor.exp)?;
727                        }
728                    } else {
729                        let denominator_started =
730                            factors[..index].iter().any(|prior| prior.exp < 0);
731                        if denominator_started {
732                            write!(f, " * ")?;
733                        } else {
734                            write!(f, "/")?;
735                        }
736                        write!(f, "{}", factor.quantity_ref)?;
737                        let positive_exp = factor
738                            .exp
739                            .checked_neg()
740                            .expect("BUG: negative unit factor exponent");
741                        if positive_exp != 1 {
742                            write!(f, "^{}", positive_exp)?;
743                        }
744                    }
745                }
746                Ok(())
747            }
748        }
749    }
750}
751
752/// A parsed constraint command argument, preserving the literal kind from the
753/// grammar rule `command_arg: { number_literal | boolean_literal | text_literal | label }`.
754///
755/// Three grammatical kinds appear after a constraint command:
756/// - **Literal** — a fully-typed value carrying the literal kind the parser
757///   recognised (`Number`, `Ratio`, `Quantity`, `Date`, `Time`,
758///   `Boolean`, `Text`). Stored as the canonical [`crate::literals::Value`]
759///   so downstream consumers match on the variant rather than re-parsing strings.
760/// - **Label** — a bare identifier used as a name (e.g. the unit name `eur`
761///   in `unit eur 1.00`, or a primitive type keyword used as an option label).
762/// - **UnitExpr** — compound unit expression produced by the parser for
763///   `-> unit <name> ...` commands. Only appears as the second argument of a
764///   `Unit` command; the first argument is always the unit name as `Label`.
765///
766/// Planning validates each command's args against the variant kinds it accepts
767/// and rejects mismatches without coercion (a `Text` literal is never a `Number`,
768/// a `Ratio` literal is never a bare `Number`, etc.).
769#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
770#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
771pub enum CommandArg {
772    /// A typed literal value parsed by [`crate::parsing::parser::Parser::parse_literal_value`].
773    Literal(crate::literals::Value),
774    /// An identifier used as a name (unit name, option keyword, etc.).
775    Label(String),
776    /// A unit argument produced by the parser for `-> unit <name> ...` commands.
777    UnitExpr(UnitArg),
778}
779
780impl fmt::Display for CommandArg {
781    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
782        match self {
783            CommandArg::Literal(v) => write!(f, "{}", v),
784            CommandArg::Label(s) => write!(f, "{}", s),
785            CommandArg::UnitExpr(unit_arg) => write!(f, "{}", unit_arg),
786        }
787    }
788}
789
790/// Constraint command for type definitions. Derived from lexer tokens; no string matching.
791#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
792#[serde(rename_all = "snake_case")]
793pub enum TypeConstraintCommand {
794    Help,
795    Default,
796    Unit,
797    Trait,
798    Minimum,
799    Maximum,
800    Decimals,
801    Option,
802    Options,
803    Length,
804}
805
806impl fmt::Display for TypeConstraintCommand {
807    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
808        let s = match self {
809            TypeConstraintCommand::Help => "help",
810            TypeConstraintCommand::Default => "default",
811            TypeConstraintCommand::Unit => "unit",
812            TypeConstraintCommand::Trait => "trait",
813            TypeConstraintCommand::Minimum => "minimum",
814            TypeConstraintCommand::Maximum => "maximum",
815            TypeConstraintCommand::Decimals => "decimals",
816            TypeConstraintCommand::Option => "option",
817            TypeConstraintCommand::Options => "options",
818            TypeConstraintCommand::Length => "length",
819        };
820        write!(f, "{}", s)
821    }
822}
823
824/// Parses a constraint command name. Returns None for unknown (parser returns error).
825#[must_use]
826pub fn try_parse_type_constraint_command(s: &str) -> Option<TypeConstraintCommand> {
827    match s.trim().to_lowercase().as_str() {
828        "help" => Some(TypeConstraintCommand::Help),
829        "default" => Some(TypeConstraintCommand::Default),
830        "unit" => Some(TypeConstraintCommand::Unit),
831        "trait" => Some(TypeConstraintCommand::Trait),
832        "minimum" => Some(TypeConstraintCommand::Minimum),
833        "maximum" => Some(TypeConstraintCommand::Maximum),
834        "decimals" => Some(TypeConstraintCommand::Decimals),
835        "option" => Some(TypeConstraintCommand::Option),
836        "options" => Some(TypeConstraintCommand::Options),
837        "length" => Some(TypeConstraintCommand::Length),
838        _ => None,
839    }
840}
841
842/// A single constraint command and its typed arguments.
843pub type Constraint = (TypeConstraintCommand, Vec<CommandArg>);
844
845/// Right-hand side of a `fill` statement: literal value or reference to copy.
846#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
847#[serde(rename_all = "snake_case")]
848pub enum FillRhs {
849    Literal(Value),
850    Reference { target: Reference },
851}
852
853#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
854#[serde(rename_all = "snake_case")]
855/// Parse-time data value (before type resolution)
856pub enum DataValue {
857    /// Declares data: optional explicit parent type, optional constraints (`-> ...`),
858    /// and optional literal value.
859    ///
860    /// Examples:
861    /// - `data x: 3.14` → `base: None`, `value: Some(Number)`
862    /// - `data x: number -> minimum 0` → `base: Some(Number)`, `constraints: Some(...)`
863    /// - `data x: finance.money` → `base: Some(Qualified { spec_alias: "finance", inner: Custom("money") })`
864    Definition {
865        #[serde(default, skip_serializing_if = "Option::is_none")]
866        base: Option<ParentType>,
867        constraints: Option<Vec<Constraint>>,
868        #[serde(default, skip_serializing_if = "Option::is_none")]
869        value: Option<Value>,
870    },
871    /// Import from another spec (surface syntax is `uses`; alias is [`LemmaData::reference`]).
872    Import(SpecRef),
873    /// Value assignment into an existing data slot (surface syntax is `fill`). Planning folds
874    /// this into resolved slot values; it does not declare a new type row.
875    ///
876    /// `data x: someident` (LHS without segments, RHS without dots) uses [`DataValue::Definition`]
877    /// with `someident` as the parent type name. See parser [`crate::parsing::parser::Parser::parse_data_value`].
878    Fill(FillRhs),
879}
880
881impl DataValue {
882    /// Whether this is only a literal RHS (`data x: 3.14`), valid as a binding value.
883    #[must_use]
884    pub fn is_definition_literal_only(&self) -> bool {
885        matches!(
886            self,
887            DataValue::Definition {
888                base: None,
889                constraints: None,
890                value: Some(_),
891            }
892        )
893    }
894
895    /// Whether planning must resolve this [`LemmaData`] row through the type resolver / named types.
896    #[must_use]
897    pub fn definition_needs_type_resolution(&self) -> bool {
898        match self {
899            DataValue::Definition { base: Some(_), .. }
900            | DataValue::Definition {
901                constraints: Some(_),
902                ..
903            } => true,
904            DataValue::Definition {
905                base: None,
906                constraints: None,
907                value: Some(v),
908            } => !matches!(v, Value::NumberWithUnit(_, _)),
909            DataValue::Import(_) | DataValue::Fill(_) | DataValue::Definition { .. } => false,
910        }
911    }
912}
913
914/// Render a chain of `-> command args ...` constraints for display purposes.
915/// Shared between [`DataValue::Definition`] and [`DataValue::Fill`] reference payloads.
916fn format_constraint_chain(constraints: &[Constraint]) -> String {
917    constraints
918        .iter()
919        .map(|(cmd, args)| {
920            let args_str: Vec<String> = args.iter().map(|a| a.to_string()).collect();
921            let joined = args_str.join(" ");
922            if joined.is_empty() {
923                format!("{}", cmd)
924            } else {
925                format!("{} {}", cmd, joined)
926            }
927        })
928        .collect::<Vec<_>>()
929        .join(" -> ")
930}
931
932impl fmt::Display for DataValue {
933    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
934        match self {
935            DataValue::Definition {
936                base,
937                constraints,
938                value,
939            } => {
940                if base.is_none() && constraints.is_none() {
941                    return match value {
942                        Some(v) => write!(f, "{}", v),
943                        None => Ok(()),
944                    };
945                }
946                let base_str = match base.as_ref() {
947                    Some(b) => format!("{b}"),
948                    None => match value {
949                        Some(v) => {
950                            if let Some(ref constraints_vec) = constraints {
951                                let constraint_str = format_constraint_chain(constraints_vec);
952                                return write!(f, "{v} -> {constraint_str}");
953                            }
954                            return write!(f, "{v}");
955                        }
956                        None => String::new(),
957                    },
958                };
959                if let Some(ref constraints_vec) = constraints {
960                    let constraint_str = format_constraint_chain(constraints_vec);
961                    write!(f, "{base_str} -> {constraint_str}")
962                } else {
963                    write!(f, "{base_str}")
964                }
965            }
966            DataValue::Import(spec_ref) => {
967                write!(f, "with {}", spec_ref)
968            }
969            DataValue::Fill(fill_rhs) => match fill_rhs {
970                FillRhs::Literal(v) => write!(f, "{v}"),
971                FillRhs::Reference { target } => write!(f, "{target}"),
972            },
973        }
974    }
975}
976
977impl LemmaData {
978    #[must_use]
979    pub fn new(reference: Reference, value: DataValue, source_location: Source) -> Self {
980        Self {
981            reference,
982            value,
983            source_location,
984        }
985    }
986}
987
988impl LemmaSpec {
989    #[must_use]
990    pub fn new(name: String) -> Self {
991        Self {
992            name,
993            effective_from: EffectiveDate::Origin,
994            source_type: None,
995            start_line: 1,
996            commentary: None,
997            data: Vec::new(),
998            rules: Vec::new(),
999            meta_fields: Vec::new(),
1000        }
1001    }
1002
1003    /// Temporal range start. Origin (None) means −∞.
1004    pub fn effective_from(&self) -> Option<&DateTimeValue> {
1005        self.effective_from.as_ref()
1006    }
1007
1008    #[must_use]
1009    pub fn with_source_type(mut self, source_type: crate::parsing::source::SourceType) -> Self {
1010        self.source_type = Some(source_type);
1011        self
1012    }
1013
1014    #[must_use]
1015    pub fn with_start_line(mut self, start_line: usize) -> Self {
1016        self.start_line = start_line;
1017        self
1018    }
1019
1020    #[must_use]
1021    pub fn set_commentary(mut self, commentary: String) -> Self {
1022        self.commentary = Some(commentary);
1023        self
1024    }
1025
1026    #[must_use]
1027    pub fn add_data(mut self, data: LemmaData) -> Self {
1028        self.data.push(data);
1029        self
1030    }
1031
1032    #[must_use]
1033    pub fn add_rule(mut self, rule: LemmaRule) -> Self {
1034        self.rules.push(rule);
1035        self
1036    }
1037
1038    #[must_use]
1039    pub fn add_meta_field(mut self, meta: MetaField) -> Self {
1040        self.meta_fields.push(meta);
1041        self
1042    }
1043}
1044
1045impl fmt::Display for LemmaSpec {
1046    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1047        write!(f, "spec {}", self.name)?;
1048        if let EffectiveDate::DateTimeValue(ref af) = self.effective_from {
1049            write!(f, " {}", af)?;
1050        }
1051        writeln!(f)?;
1052
1053        if let Some(ref commentary) = self.commentary {
1054            writeln!(f, "\"\"\"")?;
1055            writeln!(f, "{}", commentary)?;
1056            writeln!(f, "\"\"\"")?;
1057        }
1058
1059        if !self.data.is_empty() {
1060            writeln!(f)?;
1061            for data in &self.data {
1062                write!(f, "{}", data)?;
1063            }
1064        }
1065
1066        if !self.rules.is_empty() {
1067            writeln!(f)?;
1068            for (index, rule) in self.rules.iter().enumerate() {
1069                if index > 0 {
1070                    writeln!(f)?;
1071                }
1072                write!(f, "{}", rule)?;
1073            }
1074        }
1075
1076        if !self.meta_fields.is_empty() {
1077            writeln!(f)?;
1078            for meta in &self.meta_fields {
1079                writeln!(f, "{}", meta)?;
1080            }
1081        }
1082
1083        Ok(())
1084    }
1085}
1086
1087impl fmt::Display for LemmaData {
1088    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1089        writeln!(f, "data {}: {}", self.reference, self.value)
1090    }
1091}
1092
1093impl fmt::Display for LemmaRule {
1094    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1095        write!(f, "rule {}: {}", self.name, self.expression)?;
1096        for unless_clause in &self.unless_clauses {
1097            write!(
1098                f,
1099                "\n  unless {} then {}",
1100                unless_clause.condition, unless_clause.result
1101            )?;
1102        }
1103        writeln!(f)?;
1104        Ok(())
1105    }
1106}
1107
1108/// Precedence level for an expression kind.
1109///
1110/// Higher values bind tighter. Used by `Expression::Display` and the formatter
1111/// to insert parentheses only where needed.
1112///
1113/// `RangeLiteral` (type construction via `...`) binds above all arithmetic; only atoms bind
1114/// above range. Parser climb in [`crate::parsing::parser::Parser`] must match this table.
1115pub fn expression_precedence(kind: &ExpressionKind) -> u8 {
1116    match kind {
1117        ExpressionKind::LogicalAnd(..) => 2,
1118        ExpressionKind::LogicalNegation(..) => 3,
1119        ExpressionKind::Comparison(..) | ExpressionKind::ResultIsVeto(..) => 4,
1120        ExpressionKind::RangeContainment(..) => 4,
1121        ExpressionKind::DateRelative(..) | ExpressionKind::DateCalendar(..) => 4,
1122        ExpressionKind::Arithmetic(_, op, _) => match op {
1123            ArithmeticComputation::Add | ArithmeticComputation::Subtract => 5,
1124            ArithmeticComputation::Multiply
1125            | ArithmeticComputation::Divide
1126            | ArithmeticComputation::Modulo => 6,
1127            ArithmeticComputation::Power => 7,
1128        },
1129        ExpressionKind::UnitConversion(..) => 8,
1130        ExpressionKind::RangeLiteral(..) => 9,
1131        ExpressionKind::MathematicalComputation(..) => 10,
1132        ExpressionKind::PastFutureRange(..) => 10,
1133        ExpressionKind::Literal(..)
1134        | ExpressionKind::Reference(..)
1135        | ExpressionKind::Now
1136        | ExpressionKind::Veto(..) => 10,
1137    }
1138}
1139
1140fn write_expression_child(
1141    f: &mut fmt::Formatter<'_>,
1142    child: &Expression,
1143    parent_prec: u8,
1144) -> fmt::Result {
1145    let child_prec = expression_precedence(&child.kind);
1146    if child_prec < parent_prec {
1147        write!(f, "({})", child)
1148    } else {
1149        write!(f, "{}", child)
1150    }
1151}
1152
1153impl fmt::Display for Expression {
1154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1155        match &self.kind {
1156            ExpressionKind::Literal(lit) => write!(f, "{}", AsLemmaSource(lit)),
1157            ExpressionKind::Reference(r) => write!(f, "{}", r),
1158            ExpressionKind::Arithmetic(left, op, right) => {
1159                let my_prec = expression_precedence(&self.kind);
1160                write_expression_child(f, left, my_prec)?;
1161                write!(f, " {} ", op)?;
1162                write_expression_child(f, right, my_prec)
1163            }
1164            ExpressionKind::Comparison(left, op, right) => {
1165                let my_prec = expression_precedence(&self.kind);
1166                write_expression_child(f, left, my_prec)?;
1167                write!(f, " {} ", op)?;
1168                write_expression_child(f, right, my_prec)
1169            }
1170            ExpressionKind::UnitConversion(value, target) => {
1171                let my_prec = expression_precedence(&self.kind);
1172                write_expression_child(f, value, my_prec)?;
1173                write!(f, " as {}", target)
1174            }
1175            ExpressionKind::LogicalNegation(expr, negation) => {
1176                if let (NegationType::Not, ExpressionKind::ResultIsVeto(operand)) =
1177                    (negation, &expr.kind)
1178                {
1179                    let my_prec = expression_precedence(&self.kind);
1180                    write_expression_child(f, operand, my_prec)?;
1181                    write!(f, " is not veto")
1182                } else {
1183                    let my_prec = expression_precedence(&self.kind);
1184                    write!(f, "not ")?;
1185                    write_expression_child(f, expr, my_prec)
1186                }
1187            }
1188            ExpressionKind::ResultIsVeto(operand) => {
1189                let my_prec = expression_precedence(&self.kind);
1190                write_expression_child(f, operand, my_prec)?;
1191                write!(f, " is veto")
1192            }
1193            ExpressionKind::LogicalAnd(left, right) => {
1194                let my_prec = expression_precedence(&self.kind);
1195                write_expression_child(f, left, my_prec)?;
1196                write!(f, " and ")?;
1197                write_expression_child(f, right, my_prec)
1198            }
1199            ExpressionKind::MathematicalComputation(op, operand) => {
1200                let my_prec = expression_precedence(&self.kind);
1201                write!(f, "{} ", op)?;
1202                write_expression_child(f, operand, my_prec)
1203            }
1204            ExpressionKind::Veto(veto) => match &veto.message {
1205                Some(msg) => write!(f, "veto {}", quote_lemma_text(msg)),
1206                None => write!(f, "veto"),
1207            },
1208            ExpressionKind::Now => write!(f, "now"),
1209            ExpressionKind::DateRelative(kind, date_expr) => {
1210                write!(f, "{} {}", date_expr, kind)?;
1211                Ok(())
1212            }
1213            ExpressionKind::DateCalendar(kind, unit, date_expr) => {
1214                write!(f, "{} {} {}", date_expr, kind, unit)
1215            }
1216            ExpressionKind::RangeLiteral(left, right) => {
1217                let my_prec = expression_precedence(&self.kind);
1218                write_expression_child(f, left, my_prec)?;
1219                write!(f, "...")?;
1220                write_expression_child(f, right, my_prec)
1221            }
1222            ExpressionKind::PastFutureRange(kind, offset_expr) => {
1223                write!(f, "{} ", kind)?;
1224                let my_prec = expression_precedence(&self.kind);
1225                write_expression_child(f, offset_expr, my_prec)
1226            }
1227            ExpressionKind::RangeContainment(value, range) => {
1228                let my_prec = expression_precedence(&self.kind);
1229                write_expression_child(f, value, my_prec)?;
1230                write!(f, " in ")?;
1231                write_expression_child(f, range, my_prec)
1232            }
1233        }
1234    }
1235}
1236
1237impl fmt::Display for ConversionTarget {
1238    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1239        match self {
1240            ConversionTarget::Calendar(unit) => write!(f, "{}", unit),
1241            ConversionTarget::Unit(unit) => write!(f, "{}", unit),
1242            ConversionTarget::Type(kind) => write!(f, "{:?}", kind),
1243        }
1244    }
1245}
1246
1247impl fmt::Display for ArithmeticComputation {
1248    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1249        match self {
1250            ArithmeticComputation::Add => write!(f, "+"),
1251            ArithmeticComputation::Subtract => write!(f, "-"),
1252            ArithmeticComputation::Multiply => write!(f, "*"),
1253            ArithmeticComputation::Divide => write!(f, "/"),
1254            ArithmeticComputation::Modulo => write!(f, "%"),
1255            ArithmeticComputation::Power => write!(f, "^"),
1256        }
1257    }
1258}
1259
1260impl fmt::Display for ComparisonComputation {
1261    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1262        match self {
1263            ComparisonComputation::GreaterThan => write!(f, ">"),
1264            ComparisonComputation::LessThan => write!(f, "<"),
1265            ComparisonComputation::GreaterThanOrEqual => write!(f, ">="),
1266            ComparisonComputation::LessThanOrEqual => write!(f, "<="),
1267            ComparisonComputation::Is => write!(f, "is"),
1268            ComparisonComputation::IsNot => write!(f, "is not"),
1269        }
1270    }
1271}
1272
1273impl fmt::Display for MathematicalComputation {
1274    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1275        match self {
1276            MathematicalComputation::Sqrt => write!(f, "sqrt"),
1277            MathematicalComputation::Sin => write!(f, "sin"),
1278            MathematicalComputation::Cos => write!(f, "cos"),
1279            MathematicalComputation::Tan => write!(f, "tan"),
1280            MathematicalComputation::Asin => write!(f, "asin"),
1281            MathematicalComputation::Acos => write!(f, "acos"),
1282            MathematicalComputation::Atan => write!(f, "atan"),
1283            MathematicalComputation::Log => write!(f, "log"),
1284            MathematicalComputation::Exp => write!(f, "exp"),
1285            MathematicalComputation::Abs => write!(f, "abs"),
1286            MathematicalComputation::Floor => write!(f, "floor"),
1287            MathematicalComputation::Ceil => write!(f, "ceil"),
1288            MathematicalComputation::Round => write!(f, "round"),
1289        }
1290    }
1291}
1292
1293// -----------------------------------------------------------------------------
1294// Primitive type kinds and parent type references
1295// -----------------------------------------------------------------------------
1296
1297/// Built-in primitive type kind. Single source of truth for type keywords.
1298#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1299#[serde(rename_all = "snake_case")]
1300pub enum PrimitiveKind {
1301    Boolean,
1302    Quantity,
1303    QuantityRange,
1304    Number,
1305    NumberRange,
1306    Percent,
1307    Ratio,
1308    RatioRange,
1309    Text,
1310    Date,
1311    DateRange,
1312    Time,
1313    Calendar,
1314    CalendarRange,
1315}
1316
1317impl std::fmt::Display for PrimitiveKind {
1318    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1319        let s = match self {
1320            PrimitiveKind::Boolean => "boolean",
1321            PrimitiveKind::Quantity => "quantity",
1322            PrimitiveKind::QuantityRange => "quantity range",
1323            PrimitiveKind::Number => "number",
1324            PrimitiveKind::NumberRange => "number range",
1325            PrimitiveKind::Percent => "percent",
1326            PrimitiveKind::Ratio => "ratio",
1327            PrimitiveKind::RatioRange => "ratio range",
1328            PrimitiveKind::Text => "text",
1329            PrimitiveKind::Date => "date",
1330            PrimitiveKind::DateRange => "date range",
1331            PrimitiveKind::Time => "time",
1332            PrimitiveKind::Calendar => "calendar",
1333            PrimitiveKind::CalendarRange => "calendar range",
1334        };
1335        write!(f, "{}", s)
1336    }
1337}
1338
1339/// Parent type in a type definition: built-in primitive or custom type name.
1340///
1341/// `name` is the declared type name (the data name that introduces this type).
1342/// For `data temperature: quantity`, name = "temperature", primitive = Quantity.
1343#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1344#[serde(tag = "kind", rename_all = "snake_case")]
1345pub enum ParentType {
1346    Primitive {
1347        primitive: PrimitiveKind,
1348    },
1349    Custom {
1350        name: String,
1351    },
1352    /// Parent type defined in another spec: `spec_alias.inner` (e.g. `data x: finance.money`).
1353    /// `inner` must be [`ParentType::Primitive`] or [`ParentType::Custom`], not nested [`ParentType::Qualified`].
1354    Qualified {
1355        spec_alias: String,
1356        inner: Box<ParentType>,
1357    },
1358}
1359
1360impl std::fmt::Display for ParentType {
1361    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1362        match self {
1363            ParentType::Primitive { primitive } => write!(f, "{}", primitive),
1364            ParentType::Custom { name } => write!(f, "{}", name),
1365            ParentType::Qualified { spec_alias, inner } => {
1366                write!(f, "{spec_alias}.{inner}")
1367            }
1368        }
1369    }
1370}
1371
1372// =============================================================================
1373// AsLemmaSource<Value> — canonical literal formatting
1374// =============================================================================
1375
1376/// Wrap a value to emit canonical Lemma source (round-trippable). See module docs.
1377pub struct AsLemmaSource<'a, T: ?Sized>(pub &'a T);
1378
1379/// Escape a string and wrap it in double quotes for Lemma source output.
1380/// Handles `\` and `"` escaping.
1381pub fn quote_lemma_text(s: &str) -> String {
1382    let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
1383    format!("\"{}\"", escaped)
1384}
1385
1386/// Format a Decimal for Lemma source, preserving precision (trailing zeros).
1387/// Strips the fractional part only when it is zero (e.g. `100` stays `"100"`,
1388/// `1.00` stays `"1.00"`). Inserts underscore separators in the integer part
1389/// when it has 4+ digits (e.g. `30000000.50` → `"30_000_000.50"`).
1390fn format_decimal_source(n: &Decimal) -> String {
1391    let raw = if n.fract().is_zero() {
1392        n.trunc().to_string()
1393    } else {
1394        n.to_string()
1395    };
1396    group_digits(&raw)
1397}
1398
1399/// Insert `_` every 3 digits in the integer part of a numeric string.
1400/// Handles optional leading `-`/`+` sign and optional fractional part.
1401/// Only groups when the integer part has 4 or more digits.
1402fn group_digits(s: &str) -> String {
1403    let (sign, rest) = if s.starts_with('-') || s.starts_with('+') {
1404        (&s[..1], &s[1..])
1405    } else {
1406        ("", s)
1407    };
1408
1409    let (int_part, frac_part) = match rest.find('.') {
1410        Some(pos) => (&rest[..pos], &rest[pos..]),
1411        None => (rest, ""),
1412    };
1413
1414    if int_part.len() < 4 {
1415        return s.to_string();
1416    }
1417
1418    let mut grouped = String::with_capacity(int_part.len() + int_part.len() / 3);
1419    for (i, ch) in int_part.chars().enumerate() {
1420        let digits_remaining = int_part.len() - i;
1421        if i > 0 && digits_remaining % 3 == 0 {
1422            grouped.push('_');
1423        }
1424        grouped.push(ch);
1425    }
1426
1427    format!("{}{}{}", sign, grouped, frac_part)
1428}
1429
1430impl<'a> fmt::Display for AsLemmaSource<'a, CommandArg> {
1431    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1432        use crate::literals::Value;
1433        match self.0 {
1434            CommandArg::Literal(Value::Text(s)) => write!(f, "{}", quote_lemma_text(s)),
1435            CommandArg::Literal(Value::Number(d)) => {
1436                write!(f, "{}", group_digits(&d.to_string()))
1437            }
1438            CommandArg::Literal(Value::Boolean(bv)) => write!(f, "{}", bv),
1439            CommandArg::Literal(Value::NumberWithUnit(d, unit)) => {
1440                write!(f, "{} {}", group_digits(&d.to_string()), unit)
1441            }
1442            CommandArg::Literal(Value::Calendar(d, unit)) => {
1443                write!(f, "{} {}", group_digits(&d.to_string()), unit)
1444            }
1445            CommandArg::Literal(value @ Value::Range(_, _)) => {
1446                write!(f, "{}", AsLemmaSource(value))
1447            }
1448            CommandArg::Literal(Value::Date(dt)) => write!(f, "{}", dt),
1449            CommandArg::Literal(Value::Time(t)) => write!(f, "{}", t),
1450            CommandArg::Label(s) => write!(f, "{}", s),
1451            CommandArg::UnitExpr(unit_arg) => write!(f, "{}", unit_arg),
1452        }
1453    }
1454}
1455
1456/// Format a single constraint command and its args as valid Lemma source.
1457pub(crate) fn format_constraint_as_source(
1458    cmd: &TypeConstraintCommand,
1459    args: &[CommandArg],
1460) -> String {
1461    if args.is_empty() {
1462        cmd.to_string()
1463    } else {
1464        let args_str: Vec<String> = args
1465            .iter()
1466            .map(|a| format!("{}", AsLemmaSource(a)))
1467            .collect();
1468        format!("{} {}", cmd, args_str.join(" "))
1469    }
1470}
1471
1472/// Format a constraint list as valid Lemma source.
1473/// Returns the `cmd arg -> cmd arg` portion joined by `separator`.
1474fn format_constraints_as_source(constraints: &[Constraint], separator: &str) -> String {
1475    constraints
1476        .iter()
1477        .map(|(cmd, args)| format_constraint_as_source(cmd, args))
1478        .collect::<Vec<_>>()
1479        .join(separator)
1480}
1481
1482// -- Display for AsLemmaSource<Value> ----------------------------------------
1483
1484impl<'a> fmt::Display for AsLemmaSource<'a, Value> {
1485    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1486        match self.0 {
1487            Value::Number(n) => write!(f, "{}", format_decimal_source(n)),
1488            Value::Text(s) => write!(f, "{}", quote_lemma_text(s)),
1489            Value::Date(dt) => {
1490                let is_date_only =
1491                    dt.hour == 0 && dt.minute == 0 && dt.second == 0 && dt.timezone.is_none();
1492                if is_date_only {
1493                    write!(f, "{:04}-{:02}-{:02}", dt.year, dt.month, dt.day)
1494                } else {
1495                    write!(
1496                        f,
1497                        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
1498                        dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second
1499                    )?;
1500                    if let Some(tz) = &dt.timezone {
1501                        write!(f, "{}", tz)?;
1502                    }
1503                    Ok(())
1504                }
1505            }
1506            Value::Time(t) => {
1507                write!(f, "{:02}:{:02}:{:02}", t.hour, t.minute, t.second)?;
1508                if let Some(tz) = &t.timezone {
1509                    write!(f, "{}", tz)?;
1510                }
1511                Ok(())
1512            }
1513            Value::Boolean(b) => write!(f, "{}", b),
1514            Value::NumberWithUnit(n, u) => match u.as_str() {
1515                "percent" => write!(f, "{}%", format_decimal_source(n)),
1516                "permille" => write!(f, "{}%%", format_decimal_source(n)),
1517                unit => write!(f, "{} {}", format_decimal_source(n), unit),
1518            },
1519            Value::Calendar(n, u) => write!(f, "{} {}", format_decimal_source(n), u),
1520            Value::Range(left, right) => {
1521                write!(
1522                    f,
1523                    "{}...{}",
1524                    AsLemmaSource(left.as_ref()),
1525                    AsLemmaSource(right.as_ref())
1526                )
1527            }
1528        }
1529    }
1530}
1531
1532// -- AsLemmaSource: MetaValue, DataValue (formatter / round-trip) ---
1533
1534impl<'a> fmt::Display for AsLemmaSource<'a, MetaValue> {
1535    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1536        match self.0 {
1537            MetaValue::Literal(v) => write!(f, "{}", AsLemmaSource(v)),
1538            MetaValue::Unquoted(s) => write!(f, "{}", s),
1539        }
1540    }
1541}
1542
1543impl<'a> fmt::Display for AsLemmaSource<'a, DataValue> {
1544    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1545        match self.0 {
1546            DataValue::Definition {
1547                base,
1548                constraints,
1549                value,
1550            } => {
1551                if base.is_none() && constraints.is_none() {
1552                    if let Some(v) = value {
1553                        return write!(f, "{}", AsLemmaSource(v));
1554                    }
1555                }
1556                let base_str = match base.as_ref() {
1557                    Some(b) => format!("{}", b),
1558                    None => match value {
1559                        Some(v) => {
1560                            if let Some(ref constraints_vec) = constraints {
1561                                let constraint_str =
1562                                    format_constraints_as_source(constraints_vec, " -> ");
1563                                return write!(f, "{} -> {}", AsLemmaSource(v), constraint_str);
1564                            }
1565                            return write!(f, "{}", AsLemmaSource(v));
1566                        }
1567                        None => String::new(),
1568                    },
1569                };
1570                if let Some(ref constraints_vec) = constraints {
1571                    let constraint_str = format_constraints_as_source(constraints_vec, " -> ");
1572                    write!(f, "{} -> {}", base_str, constraint_str)
1573                } else {
1574                    write!(f, "{}", base_str)
1575                }
1576            }
1577            DataValue::Import(spec_ref) => {
1578                write!(f, "with {}", spec_ref)
1579            }
1580            DataValue::Fill(fill_rhs) => match fill_rhs {
1581                FillRhs::Literal(v) => write!(f, "{}", AsLemmaSource(v)),
1582                FillRhs::Reference { target } => write!(f, "{target}"),
1583            },
1584        }
1585    }
1586}
1587
1588#[cfg(test)]
1589mod tests {
1590    use super::*;
1591
1592    #[test]
1593    fn test_conversion_target_display() {
1594        assert_eq!(
1595            format!("{}", ConversionTarget::Unit("hours".to_string())),
1596            "hours"
1597        );
1598        assert_eq!(
1599            format!("{}", ConversionTarget::Unit("usd".to_string())),
1600            "usd"
1601        );
1602    }
1603
1604    #[test]
1605    fn test_value_number_with_unit_ratio_display() {
1606        use rust_decimal::Decimal;
1607        use std::str::FromStr;
1608        let percent =
1609            Value::NumberWithUnit(Decimal::from_str("10").unwrap(), "percent".to_string());
1610        assert_eq!(format!("{}", percent), "10%");
1611        let permille =
1612            Value::NumberWithUnit(Decimal::from_str("5").unwrap(), "permille".to_string());
1613        assert_eq!(format!("{}", permille), "5%%");
1614    }
1615
1616    #[test]
1617    fn test_datetime_value_display() {
1618        let dt = DateTimeValue {
1619            year: 2024,
1620            month: 12,
1621            day: 25,
1622            hour: 14,
1623            minute: 30,
1624            second: 45,
1625            microsecond: 0,
1626            timezone: Some(TimezoneValue {
1627                offset_hours: 1,
1628                offset_minutes: 0,
1629            }),
1630        };
1631        assert_eq!(format!("{}", dt), "2024-12-25T14:30:45+01:00");
1632    }
1633
1634    #[test]
1635    fn test_datetime_value_display_date_only() {
1636        let dt = DateTimeValue {
1637            year: 2026,
1638            month: 3,
1639            day: 4,
1640            hour: 0,
1641            minute: 0,
1642            second: 0,
1643            microsecond: 0,
1644            timezone: None,
1645        };
1646        assert_eq!(format!("{}", dt), "2026-03-04");
1647    }
1648
1649    #[test]
1650    fn test_datetime_value_display_microseconds() {
1651        let dt = DateTimeValue {
1652            year: 2026,
1653            month: 2,
1654            day: 23,
1655            hour: 14,
1656            minute: 30,
1657            second: 45,
1658            microsecond: 123456,
1659            timezone: Some(TimezoneValue {
1660                offset_hours: 0,
1661                offset_minutes: 0,
1662            }),
1663        };
1664        assert_eq!(format!("{}", dt), "2026-02-23T14:30:45.123456Z");
1665    }
1666
1667    #[test]
1668    fn test_datetime_microsecond_in_ordering() {
1669        let a = DateTimeValue {
1670            year: 2026,
1671            month: 1,
1672            day: 1,
1673            hour: 0,
1674            minute: 0,
1675            second: 0,
1676            microsecond: 100,
1677            timezone: None,
1678        };
1679        let b = DateTimeValue {
1680            year: 2026,
1681            month: 1,
1682            day: 1,
1683            hour: 0,
1684            minute: 0,
1685            second: 0,
1686            microsecond: 200,
1687            timezone: None,
1688        };
1689        assert!(a < b);
1690    }
1691
1692    #[test]
1693    fn test_datetime_parse_iso_week() {
1694        let dt: DateTimeValue = "2026-W01".parse().unwrap();
1695        assert_eq!(dt.year, 2025);
1696        assert_eq!(dt.month, 12);
1697        assert_eq!(dt.day, 29);
1698        assert_eq!(dt.microsecond, 0);
1699    }
1700
1701    #[test]
1702    fn test_negation_types() {
1703        let json = serde_json::to_string(&NegationType::Not).expect("serialize NegationType");
1704        let decoded: NegationType = serde_json::from_str(&json).expect("deserialize NegationType");
1705        assert_eq!(decoded, NegationType::Not);
1706    }
1707
1708    #[test]
1709    fn parent_type_primitive_serde_internally_tagged() {
1710        let p = ParentType::Primitive {
1711            primitive: PrimitiveKind::Number,
1712        };
1713        let json = serde_json::to_string(&p).expect("ParentType::Primitive must serialize");
1714        assert!(json.contains("\"kind\"") && json.contains("\"primitive\""));
1715        let back: ParentType = serde_json::from_str(&json).expect("deserialize");
1716        assert_eq!(back, p);
1717    }
1718
1719    // =====================================================================
1720    // DataValue Display — constraint formatting
1721    // =====================================================================
1722
1723    fn text_arg(s: &str) -> CommandArg {
1724        CommandArg::Literal(crate::literals::Value::Text(s.to_string()))
1725    }
1726
1727    fn number_arg(s: &str) -> CommandArg {
1728        let d: rust_decimal::Decimal = s.parse().expect("decimal");
1729        CommandArg::Literal(crate::literals::Value::Number(d))
1730    }
1731
1732    fn boolean_arg(b: BooleanValue) -> CommandArg {
1733        CommandArg::Literal(crate::literals::Value::Boolean(b))
1734    }
1735
1736    fn quantity_arg(value: &str, unit: &str) -> CommandArg {
1737        let d: rust_decimal::Decimal = value.parse().expect("decimal");
1738        CommandArg::Literal(crate::literals::Value::NumberWithUnit(d, unit.to_string()))
1739    }
1740
1741    fn duration_arg(value: &str, unit: &str) -> CommandArg {
1742        let d: rust_decimal::Decimal = value.parse().expect("decimal");
1743        CommandArg::Literal(crate::literals::Value::NumberWithUnit(d, unit.to_string()))
1744    }
1745
1746    #[test]
1747    fn as_lemma_source_text_default_is_quoted() {
1748        let fv = DataValue::Definition {
1749            base: Some(ParentType::Primitive {
1750                primitive: PrimitiveKind::Text,
1751            }),
1752            constraints: Some(vec![(
1753                TypeConstraintCommand::Default,
1754                vec![text_arg("single")],
1755            )]),
1756            value: None,
1757        };
1758        assert_eq!(
1759            format!("{}", AsLemmaSource(&fv)),
1760            "text -> default \"single\""
1761        );
1762    }
1763
1764    #[test]
1765    fn as_lemma_source_number_default_not_quoted() {
1766        let fv = DataValue::Definition {
1767            base: Some(ParentType::Primitive {
1768                primitive: PrimitiveKind::Number,
1769            }),
1770            constraints: Some(vec![(
1771                TypeConstraintCommand::Default,
1772                vec![number_arg("10")],
1773            )]),
1774            value: None,
1775        };
1776        assert_eq!(format!("{}", AsLemmaSource(&fv)), "number -> default 10");
1777    }
1778
1779    #[test]
1780    fn as_lemma_source_help_always_quoted() {
1781        let fv = DataValue::Definition {
1782            base: Some(ParentType::Primitive {
1783                primitive: PrimitiveKind::Number,
1784            }),
1785            constraints: Some(vec![(
1786                TypeConstraintCommand::Help,
1787                vec![text_arg("Enter a quantity")],
1788            )]),
1789            value: None,
1790        };
1791        assert_eq!(
1792            format!("{}", AsLemmaSource(&fv)),
1793            "number -> help \"Enter a quantity\""
1794        );
1795    }
1796
1797    #[test]
1798    fn as_lemma_source_text_option_quoted() {
1799        let fv = DataValue::Definition {
1800            base: Some(ParentType::Primitive {
1801                primitive: PrimitiveKind::Text,
1802            }),
1803            constraints: Some(vec![
1804                (TypeConstraintCommand::Option, vec![text_arg("active")]),
1805                (TypeConstraintCommand::Option, vec![text_arg("inactive")]),
1806            ]),
1807            value: None,
1808        };
1809        assert_eq!(
1810            format!("{}", AsLemmaSource(&fv)),
1811            "text -> option \"active\" -> option \"inactive\""
1812        );
1813    }
1814
1815    #[test]
1816    fn as_lemma_source_quantity_unit_not_quoted() {
1817        let fv = DataValue::Definition {
1818            base: Some(ParentType::Primitive {
1819                primitive: PrimitiveKind::Quantity,
1820            }),
1821            constraints: Some(vec![
1822                (
1823                    TypeConstraintCommand::Unit,
1824                    vec![CommandArg::Label("eur".to_string()), number_arg("1.00")],
1825                ),
1826                (
1827                    TypeConstraintCommand::Unit,
1828                    vec![CommandArg::Label("usd".to_string()), number_arg("0.91")],
1829                ),
1830            ]),
1831            value: None,
1832        };
1833        assert_eq!(
1834            format!("{}", AsLemmaSource(&fv)),
1835            "quantity -> unit eur 1.00 -> unit usd 0.91"
1836        );
1837    }
1838
1839    #[test]
1840    fn as_lemma_source_quantity_minimum_with_unit() {
1841        let fv = DataValue::Definition {
1842            base: Some(ParentType::Primitive {
1843                primitive: PrimitiveKind::Quantity,
1844            }),
1845            constraints: Some(vec![(
1846                TypeConstraintCommand::Minimum,
1847                vec![quantity_arg("0", "eur")],
1848            )]),
1849            value: None,
1850        };
1851        assert_eq!(
1852            format!("{}", AsLemmaSource(&fv)),
1853            "quantity -> minimum 0 eur"
1854        );
1855    }
1856
1857    #[test]
1858    fn as_lemma_source_boolean_default() {
1859        let fv = DataValue::Definition {
1860            base: Some(ParentType::Primitive {
1861                primitive: PrimitiveKind::Boolean,
1862            }),
1863            constraints: Some(vec![(
1864                TypeConstraintCommand::Default,
1865                vec![boolean_arg(BooleanValue::True)],
1866            )]),
1867            value: None,
1868        };
1869        assert_eq!(format!("{}", AsLemmaSource(&fv)), "boolean -> default true");
1870    }
1871
1872    #[test]
1873    fn as_lemma_source_duration_default() {
1874        let fv = DataValue::Definition {
1875            base: Some(ParentType::Custom {
1876                name: "duration".to_string(),
1877            }),
1878            constraints: Some(vec![(
1879                TypeConstraintCommand::Default,
1880                vec![duration_arg("40", "hours")],
1881            )]),
1882            value: None,
1883        };
1884        assert_eq!(
1885            format!("{}", AsLemmaSource(&fv)),
1886            "duration -> default 40 hours"
1887        );
1888    }
1889
1890    #[test]
1891    fn as_lemma_source_named_type_default_quoted() {
1892        // Named types (user-defined): the parser produces a typed Text literal for
1893        // quoted default values like `default "single"`.
1894        let fv = DataValue::Definition {
1895            base: Some(ParentType::Custom {
1896                name: "filing_status_type".to_string(),
1897            }),
1898            constraints: Some(vec![(
1899                TypeConstraintCommand::Default,
1900                vec![text_arg("single")],
1901            )]),
1902            value: None,
1903        };
1904        assert_eq!(
1905            format!("{}", AsLemmaSource(&fv)),
1906            "filing_status_type -> default \"single\""
1907        );
1908    }
1909
1910    #[test]
1911    fn as_lemma_source_help_escapes_quotes() {
1912        let fv = DataValue::Definition {
1913            base: Some(ParentType::Primitive {
1914                primitive: PrimitiveKind::Text,
1915            }),
1916            constraints: Some(vec![(
1917                TypeConstraintCommand::Help,
1918                vec![text_arg("say \"hello\"")],
1919            )]),
1920            value: None,
1921        };
1922        assert_eq!(
1923            format!("{}", AsLemmaSource(&fv)),
1924            "text -> help \"say \\\"hello\\\"\""
1925        );
1926    }
1927
1928    fn unit_arg_expr(prefix: Decimal, factors: &[(&str, i32)]) -> UnitArg {
1929        UnitArg::Expr(
1930            prefix,
1931            factors
1932                .iter()
1933                .map(|(quantity_ref, exp)| UnitFactor {
1934                    quantity_ref: (*quantity_ref).to_string(),
1935                    exp: *exp,
1936                })
1937                .collect(),
1938        )
1939    }
1940
1941    #[test]
1942    fn unit_arg_display_metre_per_second() {
1943        let arg = unit_arg_expr(Decimal::ONE, &[("metre", 1), ("second", -1)]);
1944        assert_eq!(format!("{arg}"), "metre/second");
1945        assert!(
1946            !format!("{arg}").contains("second^-1"),
1947            "must not print denominator as negative exponent"
1948        );
1949    }
1950
1951    #[test]
1952    fn unit_arg_display_meter_per_second_squared() {
1953        let arg = unit_arg_expr(Decimal::ONE, &[("meter", 1), ("second", -2)]);
1954        assert_eq!(format!("{arg}"), "meter/second^2");
1955    }
1956
1957    #[test]
1958    fn unit_arg_display_kg_times_mps2() {
1959        let arg = unit_arg_expr(Decimal::ONE, &[("kg", 1), ("mps2", 1)]);
1960        assert_eq!(format!("{arg}"), "kg * mps2");
1961    }
1962
1963    #[test]
1964    fn unit_arg_display_numeric_prefix_metre_per_second() {
1965        use std::str::FromStr;
1966        let prefix = Decimal::from_str("3.6").expect("decimal");
1967        let arg = unit_arg_expr(prefix, &[("metre", 1), ("second", -1)]);
1968        assert_eq!(format!("{arg}"), "3.6 metre/second");
1969    }
1970
1971    #[test]
1972    fn unit_arg_display_metre_per_second_times_kg() {
1973        let arg = unit_arg_expr(Decimal::ONE, &[("metre", 1), ("second", -1), ("kg", 1)]);
1974        assert_eq!(format!("{arg}"), "metre/second * kg");
1975    }
1976
1977    #[test]
1978    fn unit_arg_display_kg_meter_per_second_squared() {
1979        let arg = unit_arg_expr(Decimal::ONE, &[("kg", 1), ("meter", 1), ("second", -2)]);
1980        assert_eq!(format!("{arg}"), "kg * meter/second^2");
1981    }
1982}