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