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