Skip to main content

lemma/planning/
semantics.rs

1//! Resolved semantic types for Lemma
2//!
3//! This module contains all types that represent resolved semantics after planning.
4//! These types are created during the planning phase and used by evaluation, inversion, etc.
5
6// Re-exported parsing types: downstream modules (evaluation, inversion, computation,
7// serialization) import these from `planning::semantics`, never from `parsing` directly.
8pub use crate::parsing::ast::{
9    ArithmeticComputation, ComparisonComputation, MathematicalComputation, NegationType, Span,
10    VetoExpression,
11};
12pub use crate::parsing::source::Source;
13
14/// Logical computation operators (defined in semantics, not used by the parser).
15#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum LogicalComputation {
18    And,
19    Or,
20    Not,
21}
22
23/// Returns the logical negation of a comparison (for displaying conditions as true in explanations).
24#[must_use]
25pub fn negated_comparison(op: ComparisonComputation) -> ComparisonComputation {
26    match op {
27        ComparisonComputation::LessThan => ComparisonComputation::GreaterThanOrEqual,
28        ComparisonComputation::LessThanOrEqual => ComparisonComputation::GreaterThan,
29        ComparisonComputation::GreaterThan => ComparisonComputation::LessThanOrEqual,
30        ComparisonComputation::GreaterThanOrEqual => ComparisonComputation::LessThan,
31        ComparisonComputation::Is => ComparisonComputation::IsNot,
32        ComparisonComputation::IsNot => ComparisonComputation::Is,
33    }
34}
35
36// Internal-only parsing imports (used only within this module for value/type resolution).
37use crate::parsing::ast::Constraint;
38use crate::parsing::ast::{
39    BooleanValue, CalendarUnit, CommandArg, ConversionTarget, DateCalendarKind, DateRelativeKind,
40    DateTimeValue, DurationUnit, LemmaSpec, PrimitiveKind, TimeValue, TypeConstraintCommand,
41};
42use crate::Error;
43use rust_decimal::Decimal;
44use serde::{Deserialize, Serialize};
45use std::collections::HashMap;
46use std::fmt;
47use std::hash::Hash;
48use std::sync::{Arc, OnceLock};
49
50// -----------------------------------------------------------------------------
51// Type specification and units (resolved type shape; apply constraints is planning)
52// -----------------------------------------------------------------------------
53
54// Unit tables live in `crate::literals` (no dependency on parsing/ast). Re-exported
55// here so downstream modules importing from `planning::semantics` keep working.
56pub use crate::literals::{RatioUnit, RatioUnits, ScaleUnit, ScaleUnits};
57
58#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
59#[serde(tag = "kind", rename_all = "lowercase")]
60pub enum TypeSpecification {
61    Boolean {
62        help: String,
63    },
64    Scale {
65        minimum: Option<Decimal>,
66        maximum: Option<Decimal>,
67        decimals: Option<u8>,
68        precision: Option<Decimal>,
69        units: ScaleUnits,
70        help: String,
71    },
72    Number {
73        minimum: Option<Decimal>,
74        maximum: Option<Decimal>,
75        decimals: Option<u8>,
76        precision: Option<Decimal>,
77        help: String,
78    },
79    Ratio {
80        minimum: Option<Decimal>,
81        maximum: Option<Decimal>,
82        decimals: Option<u8>,
83        units: RatioUnits,
84        help: String,
85    },
86    Text {
87        length: Option<usize>,
88        options: Vec<String>,
89        help: String,
90    },
91    Date {
92        minimum: Option<DateTimeValue>,
93        maximum: Option<DateTimeValue>,
94        help: String,
95    },
96    Time {
97        minimum: Option<TimeValue>,
98        maximum: Option<TimeValue>,
99        help: String,
100    },
101    Duration {
102        minimum: Option<(Decimal, SemanticDurationUnit)>,
103        maximum: Option<(Decimal, SemanticDurationUnit)>,
104        help: String,
105    },
106    Veto {
107        message: Option<String>,
108    },
109    /// Sentinel used during type inference when the type could not be determined.
110    /// Propagates through expressions without generating cascading errors.
111    /// Must never appear in a successfully validated graph or execution plan.
112    Undetermined,
113}
114
115/// Extract a typed [`Value`] from the first `CommandArg`, requiring `Literal` shape.
116///
117/// `Label` args carry identifiers (unit names, option keywords) and never satisfy a
118/// command position that wants a literal value. Returning a typed `Value` keeps the
119/// caller's match exhaustive over [`Value`] variants — no string coercion path.
120fn require_literal<'a>(
121    args: &'a [CommandArg],
122    cmd: &str,
123) -> Result<&'a crate::literals::Value, String> {
124    let arg = args
125        .first()
126        .ok_or_else(|| format!("{} requires an argument", cmd))?;
127    match arg {
128        CommandArg::Literal(v) => Ok(v),
129        CommandArg::Label(name) => Err(format!(
130            "{} requires a literal value, got identifier '{}'",
131            cmd, name
132        )),
133    }
134}
135
136fn apply_type_help_command(help: &mut String, args: &[CommandArg]) -> Result<(), String> {
137    match require_literal(args, "help")? {
138        crate::literals::Value::Text(s) => {
139            *help = s.clone();
140            Ok(())
141        }
142        other => Err(format!(
143            "help requires a text literal (quoted string), got {}",
144            value_kind_name(other)
145        )),
146    }
147}
148
149/// Human-readable name for a [`Value`] variant — used in mismatch error messages.
150fn value_kind_name(v: &crate::literals::Value) -> &'static str {
151    use crate::literals::Value;
152    match v {
153        Value::Number(_) => "number",
154        Value::Scale(_, _) => "scale",
155        Value::Text(_) => "text",
156        Value::Date(_) => "date",
157        Value::Time(_) => "time",
158        Value::Boolean(_) => "boolean",
159        Value::Duration(_, _) => "duration",
160        Value::Ratio(_, _) => "ratio",
161    }
162}
163
164/// Cast a [`Decimal`] to `u8`, requiring it to be a non-negative whole number that fits.
165fn decimal_to_u8(d: Decimal, ctx: &str) -> Result<u8, String> {
166    use rust_decimal::prelude::ToPrimitive;
167    if !d.fract().is_zero() {
168        return Err(format!(
169            "{} requires a whole number, got fractional value {}",
170            ctx, d
171        ));
172    }
173    d.to_u8()
174        .ok_or_else(|| format!("{} value out of range for u8: {}", ctx, d))
175}
176
177/// Cast a [`Decimal`] to `usize`, requiring it to be a non-negative whole number that fits.
178fn decimal_to_usize(d: Decimal, ctx: &str) -> Result<usize, String> {
179    use rust_decimal::prelude::ToPrimitive;
180    if !d.fract().is_zero() {
181        return Err(format!(
182            "{} requires a whole number, got fractional value {}",
183            ctx, d
184        ));
185    }
186    d.to_usize()
187        .ok_or_else(|| format!("{} value out of range for usize: {}", ctx, d))
188}
189
190/// Extract a bare [`Decimal`] from a [`Value::Number`] literal arg.
191///
192/// Numeric meta-constraints (`decimals`, `precision`, `length`, `minimum`/`maximum`
193/// on `Number` and `Scale`) take a bare decimal — not a ratio, not a scale. Reject
194/// any other variant to honour the no-coercion contract.
195fn require_decimal_literal(args: &[CommandArg], cmd: &str) -> Result<Decimal, String> {
196    match require_literal(args, cmd)? {
197        crate::literals::Value::Number(d) => Ok(*d),
198        other => Err(format!(
199            "{} requires a number literal, got {}",
200            cmd,
201            value_kind_name(other)
202        )),
203    }
204}
205
206/// Resolve a scale constraint arg to a canonical decimal in the scale's base unit.
207///
208/// Accepts:
209/// - `Value::Scale(d, unit)` — looks `unit` up in the scale's `units` table and
210///   multiplies by the unit's conversion factor (`5 eur` with `unit eur 1.00`
211///   becomes `5`). The unit must be defined before the bound is applied;
212///   otherwise the lookup fails.
213/// - `Value::Number(d)` — treated as already in base units.
214fn require_scale_literal(
215    args: &[CommandArg],
216    units: &ScaleUnits,
217    cmd: &str,
218) -> Result<Decimal, String> {
219    use crate::literals::Value;
220    match require_literal(args, cmd)? {
221        Value::Scale(d, unit_name) => {
222            let unit = units.get(unit_name)?;
223            Ok(*d * unit.value)
224        }
225        Value::Number(d) => Ok(*d),
226        other => Err(format!(
227            "{} requires a scale or number literal, got {}",
228            cmd,
229            value_kind_name(other)
230        )),
231    }
232}
233
234/// Resolve a ratio constraint arg to a canonical 0..1 decimal.
235///
236/// Accepts:
237/// - `Value::Ratio(d, _)` — already canonicalised by the parser (`5%` → `0.05`).
238/// - `Value::Number(d)` — bare decimal interpreted as a unit-less ratio (`0.5`).
239///
240/// All other [`Value`] variants are rejected. Unit-named ratios with non-canonical
241/// units (e.g. user-defined `unit basis_point 0.0001`) are not yet representable
242/// at the literal layer and route through the same path once added.
243fn require_ratio_literal(args: &[CommandArg], cmd: &str) -> Result<Decimal, String> {
244    use crate::literals::Value;
245    match require_literal(args, cmd)? {
246        Value::Ratio(d, _) => Ok(*d),
247        Value::Number(d) => Ok(*d),
248        other => Err(format!(
249            "{} requires a ratio or number literal, got {}",
250            cmd,
251            value_kind_name(other)
252        )),
253    }
254}
255
256/// Extract an option name from a single arg.
257///
258/// Both `option red` (bare identifier, parsed as `Label`) and `option "red"`
259/// (quoted text literal) are valid lemma syntax for option enumeration; the
260/// grammar accepts either form. All other variants are rejected.
261fn option_name(arg: &CommandArg, cmd: &str) -> Result<String, String> {
262    match arg {
263        CommandArg::Literal(crate::literals::Value::Text(s)) => Ok(s.clone()),
264        CommandArg::Label(name) => Ok(name.clone()),
265        CommandArg::Literal(other) => Err(format!(
266            "{} requires a text literal or identifier, got {}",
267            cmd,
268            value_kind_name(other)
269        )),
270    }
271}
272
273/// Extract a [`DateTimeValue`] from a [`Value::Date`] literal arg.
274fn require_date_literal(args: &[CommandArg], cmd: &str) -> Result<DateTimeValue, String> {
275    match require_literal(args, cmd)? {
276        crate::literals::Value::Date(dt) => Ok(dt.clone()),
277        other => Err(format!(
278            "{} requires a date literal (e.g. 2024-01-01), got {}",
279            cmd,
280            value_kind_name(other)
281        )),
282    }
283}
284
285/// Extract a [`TimeValue`] from a [`Value::Time`] literal arg.
286fn require_time_literal(args: &[CommandArg], cmd: &str) -> Result<TimeValue, String> {
287    match require_literal(args, cmd)? {
288        crate::literals::Value::Time(t) => Ok(t.clone()),
289        other => Err(format!(
290            "{} requires a time literal (e.g. 12:30:00), got {}",
291            cmd,
292            value_kind_name(other)
293        )),
294    }
295}
296
297/// Extract a `(value, unit)` pair from a [`Value::Duration`] literal arg.
298fn require_duration_literal(
299    args: &[CommandArg],
300    cmd: &str,
301) -> Result<(Decimal, DurationUnit), String> {
302    match require_literal(args, cmd)? {
303        crate::literals::Value::Duration(d, unit) => Ok((*d, unit.clone())),
304        other => Err(format!(
305            "{} requires a duration literal (e.g. 1 day), got {}",
306            cmd,
307            value_kind_name(other)
308        )),
309    }
310}
311
312impl TypeSpecification {
313    pub fn boolean() -> Self {
314        TypeSpecification::Boolean {
315            help: "Values: true, false".to_string(),
316        }
317    }
318    pub fn scale() -> Self {
319        TypeSpecification::Scale {
320            minimum: None,
321            maximum: None,
322            decimals: None,
323            precision: None,
324            units: ScaleUnits::new(),
325            help: "Format: {value} {unit} (e.g. 100 kilograms)".to_string(),
326        }
327    }
328    pub fn number() -> Self {
329        TypeSpecification::Number {
330            minimum: None,
331            maximum: None,
332            decimals: None,
333            precision: None,
334            help: "Numeric value".to_string(),
335        }
336    }
337    pub fn ratio() -> Self {
338        TypeSpecification::Ratio {
339            minimum: None,
340            maximum: None,
341            decimals: None,
342            units: RatioUnits(vec![
343                RatioUnit {
344                    name: "percent".to_string(),
345                    value: Decimal::from(100),
346                },
347                RatioUnit {
348                    name: "permille".to_string(),
349                    value: Decimal::from(1000),
350                },
351            ]),
352            help: "Format: {value} {unit} (e.g. 21 percent)".to_string(),
353        }
354    }
355    pub fn text() -> Self {
356        TypeSpecification::Text {
357            length: None,
358            options: vec![],
359            help: "Text value".to_string(),
360        }
361    }
362    pub fn date() -> Self {
363        TypeSpecification::Date {
364            minimum: None,
365            maximum: None,
366            help: "Format: YYYY-MM-DD (e.g. 2024-01-15)".to_string(),
367        }
368    }
369    pub fn time() -> Self {
370        TypeSpecification::Time {
371            minimum: None,
372            maximum: None,
373            help: "Format: HH:MM:SS (e.g. 14:30:00)".to_string(),
374        }
375    }
376    pub fn duration() -> Self {
377        TypeSpecification::Duration {
378            minimum: None,
379            maximum: None,
380            help: "Format: {value} {unit} (e.g. 40 hours). Units: years, months, weeks, days, hours, minutes, seconds".to_string(),
381        }
382    }
383    pub fn veto() -> Self {
384        TypeSpecification::Veto { message: None }
385    }
386
387    /// Apply a single constraint command to this spec.
388    ///
389    /// The `declared_default` out-parameter receives the default value (if the command
390    /// is `Default`), encoded as [`ValueKind`]. Defaults are owned by the data binding
391    /// or typedef entry, not by the type specification itself; callers thread a single
392    /// `&mut Option<ValueKind>` across all constraint applications for one type so the
393    /// latest `-> default` command wins.
394    pub fn apply_constraint(
395        mut self,
396        command: TypeConstraintCommand,
397        args: &[CommandArg],
398        declared_default: &mut Option<ValueKind>,
399    ) -> Result<Self, String> {
400        match &mut self {
401            TypeSpecification::Boolean { help } => match command {
402                TypeConstraintCommand::Help => {
403                    apply_type_help_command(help, args)?;
404                }
405                TypeConstraintCommand::Default => match require_literal(args, "default")? {
406                    crate::literals::Value::Boolean(bv) => {
407                        *declared_default = Some(ValueKind::Boolean(bool::from(*bv)));
408                    }
409                    other => {
410                        return Err(format!(
411                            "default for boolean type requires a boolean literal (true/false/yes/no/accept/reject), got {}",
412                            value_kind_name(other)
413                        ));
414                    }
415                },
416                other => {
417                    return Err(format!(
418                        "Invalid command '{}' for boolean type. Valid commands: help, default",
419                        other
420                    ));
421                }
422            },
423            TypeSpecification::Scale {
424                decimals,
425                minimum,
426                maximum,
427                precision,
428                units,
429                help,
430            } => match command {
431                TypeConstraintCommand::Decimals => {
432                    let d = require_decimal_literal(args, "decimals")?;
433                    *decimals = Some(decimal_to_u8(d, "decimals")?);
434                }
435                TypeConstraintCommand::Unit => {
436                    let (unit_name, value) = match args {
437                        [CommandArg::Label(name), CommandArg::Literal(crate::literals::Value::Number(v))] => {
438                            (name.clone(), *v)
439                        }
440                        _ => {
441                            return Err(
442                                "unit requires a unit name followed by a numeric conversion factor (e.g., 'unit eur 1.00')"
443                                    .to_string(),
444                            );
445                        }
446                    };
447                    if units.iter().any(|u| u.name == unit_name) {
448                        return Err(format!(
449                            "Unit '{}' is already defined in this scale type.",
450                            unit_name
451                        ));
452                    }
453                    units.0.push(ScaleUnit {
454                        name: unit_name,
455                        value,
456                    });
457                }
458                TypeConstraintCommand::Minimum => {
459                    *minimum = Some(require_scale_literal(args, units, "minimum")?);
460                }
461                TypeConstraintCommand::Maximum => {
462                    *maximum = Some(require_scale_literal(args, units, "maximum")?);
463                }
464                TypeConstraintCommand::Precision => {
465                    *precision = Some(require_scale_literal(args, units, "precision")?);
466                }
467                TypeConstraintCommand::Help => {
468                    apply_type_help_command(help, args)?;
469                }
470                TypeConstraintCommand::Default => match require_literal(args, "default")? {
471                    crate::literals::Value::Scale(value, unit_name) => {
472                        *declared_default = Some(ValueKind::Scale(*value, unit_name.clone()));
473                    }
474                    other => {
475                        return Err(format!(
476                            "default for scale type requires a scale literal '{{value}} {{unit}}' (e.g. '1 kilogram'), got {}",
477                            value_kind_name(other)
478                        ));
479                    }
480                },
481                _ => {
482                    return Err(format!(
483                        "Invalid command '{}' for scale type. Valid commands: unit, minimum, maximum, decimals, precision, help, default",
484                        command
485                    ));
486                }
487            },
488            TypeSpecification::Number {
489                decimals,
490                minimum,
491                maximum,
492                precision,
493                help,
494            } => match command {
495                TypeConstraintCommand::Decimals => {
496                    let d = require_decimal_literal(args, "decimals")?;
497                    *decimals = Some(decimal_to_u8(d, "decimals")?);
498                }
499                TypeConstraintCommand::Unit => {
500                    return Err(
501                        "Invalid command 'unit' for number type. Number types are dimensionless and cannot have units. Use 'scale' type instead.".to_string()
502                    );
503                }
504                TypeConstraintCommand::Minimum => {
505                    *minimum = Some(require_decimal_literal(args, "minimum")?);
506                }
507                TypeConstraintCommand::Maximum => {
508                    *maximum = Some(require_decimal_literal(args, "maximum")?);
509                }
510                TypeConstraintCommand::Precision => {
511                    *precision = Some(require_decimal_literal(args, "precision")?);
512                }
513                TypeConstraintCommand::Help => {
514                    apply_type_help_command(help, args)?;
515                }
516                TypeConstraintCommand::Default => match require_literal(args, "default")? {
517                    crate::literals::Value::Number(d) => {
518                        *declared_default = Some(ValueKind::Number(*d));
519                    }
520                    other => {
521                        return Err(format!(
522                            "default for number type requires a number literal, got {}",
523                            value_kind_name(other)
524                        ));
525                    }
526                },
527                _ => {
528                    return Err(format!(
529                        "Invalid command '{}' for number type. Valid commands: minimum, maximum, decimals, precision, help, default",
530                        command
531                    ));
532                }
533            },
534            TypeSpecification::Ratio {
535                decimals,
536                minimum,
537                maximum,
538                units,
539                help,
540            } => match command {
541                TypeConstraintCommand::Decimals => {
542                    let d = require_decimal_literal(args, "decimals")?;
543                    *decimals = Some(decimal_to_u8(d, "decimals")?);
544                }
545                TypeConstraintCommand::Unit => {
546                    let (unit_name, value) = match args {
547                        [CommandArg::Label(name), CommandArg::Literal(crate::literals::Value::Number(v))] => {
548                            (name.clone(), *v)
549                        }
550                        _ => {
551                            return Err(
552                                "unit requires a unit name followed by a numeric conversion factor (e.g., 'unit percent 100')"
553                                    .to_string(),
554                            );
555                        }
556                    };
557                    if units.iter().any(|u| u.name == unit_name) {
558                        return Err(format!(
559                            "Unit '{}' is already defined in this ratio type.",
560                            unit_name
561                        ));
562                    }
563                    units.0.push(RatioUnit {
564                        name: unit_name,
565                        value,
566                    });
567                }
568                TypeConstraintCommand::Minimum => {
569                    *minimum = Some(require_ratio_literal(args, "minimum")?);
570                }
571                TypeConstraintCommand::Maximum => {
572                    *maximum = Some(require_ratio_literal(args, "maximum")?);
573                }
574                TypeConstraintCommand::Help => {
575                    apply_type_help_command(help, args)?;
576                }
577                TypeConstraintCommand::Default => {
578                    let d = require_ratio_literal(args, "default")?;
579                    *declared_default = Some(ValueKind::Ratio(d, None));
580                }
581                _ => {
582                    return Err(format!(
583                        "Invalid command '{}' for ratio type. Valid commands: unit, minimum, maximum, decimals, help, default",
584                        command
585                    ));
586                }
587            },
588            TypeSpecification::Text {
589                length,
590                options,
591                help,
592            } => match command {
593                TypeConstraintCommand::Option => {
594                    if args.len() != 1 {
595                        return Err("option takes exactly one argument".to_string());
596                    }
597                    options.push(option_name(&args[0], "option")?);
598                }
599                TypeConstraintCommand::Options => {
600                    let mut collected = Vec::with_capacity(args.len());
601                    for arg in args {
602                        collected.push(option_name(arg, "options")?);
603                    }
604                    *options = collected;
605                }
606                TypeConstraintCommand::Length => {
607                    let d = require_decimal_literal(args, "length")?;
608                    *length = Some(decimal_to_usize(d, "length")?);
609                }
610                TypeConstraintCommand::Help => {
611                    apply_type_help_command(help, args)?;
612                }
613                TypeConstraintCommand::Default => match require_literal(args, "default")? {
614                    crate::literals::Value::Text(s) => {
615                        *declared_default = Some(ValueKind::Text(s.clone()));
616                    }
617                    other => {
618                        return Err(format!(
619                            "default for text type requires a text literal (quoted string), got {}",
620                            value_kind_name(other)
621                        ));
622                    }
623                },
624                _ => {
625                    return Err(format!(
626                        "Invalid command '{}' for text type. Valid commands: options, length, help, default",
627                        command
628                    ));
629                }
630            },
631            TypeSpecification::Date {
632                minimum,
633                maximum,
634                help,
635            } => match command {
636                TypeConstraintCommand::Minimum => {
637                    let dt = require_date_literal(args, "minimum")?;
638                    *minimum = Some(dt);
639                }
640                TypeConstraintCommand::Maximum => {
641                    let dt = require_date_literal(args, "maximum")?;
642                    *maximum = Some(dt);
643                }
644                TypeConstraintCommand::Help => {
645                    apply_type_help_command(help, args)?;
646                }
647                TypeConstraintCommand::Default => {
648                    let dt = require_date_literal(args, "default")?;
649                    *declared_default = Some(ValueKind::Date(date_time_to_semantic(&dt)));
650                }
651                _ => {
652                    return Err(format!(
653                        "Invalid command '{}' for date type. Valid commands: minimum, maximum, help, default",
654                        command
655                    ));
656                }
657            },
658            TypeSpecification::Time {
659                minimum,
660                maximum,
661                help,
662            } => match command {
663                TypeConstraintCommand::Minimum => {
664                    let t = require_time_literal(args, "minimum")?;
665                    *minimum = Some(t);
666                }
667                TypeConstraintCommand::Maximum => {
668                    let t = require_time_literal(args, "maximum")?;
669                    *maximum = Some(t);
670                }
671                TypeConstraintCommand::Help => {
672                    apply_type_help_command(help, args)?;
673                }
674                TypeConstraintCommand::Default => {
675                    let t = require_time_literal(args, "default")?;
676                    *declared_default = Some(ValueKind::Time(time_to_semantic(&t)));
677                }
678                _ => {
679                    return Err(format!(
680                        "Invalid command '{}' for time type. Valid commands: minimum, maximum, help, default",
681                        command
682                    ));
683                }
684            },
685            TypeSpecification::Duration {
686                minimum,
687                maximum,
688                help,
689            } => match command {
690                TypeConstraintCommand::Help => {
691                    apply_type_help_command(help, args)?;
692                }
693                TypeConstraintCommand::Minimum => {
694                    let (value, unit) = require_duration_literal(args, "minimum")?;
695                    *minimum = Some((value, duration_unit_to_semantic(&unit)));
696                }
697                TypeConstraintCommand::Maximum => {
698                    let (value, unit) = require_duration_literal(args, "maximum")?;
699                    *maximum = Some((value, duration_unit_to_semantic(&unit)));
700                }
701                TypeConstraintCommand::Default => {
702                    let (value, unit) = require_duration_literal(args, "default")?;
703                    *declared_default =
704                        Some(ValueKind::Duration(value, duration_unit_to_semantic(&unit)));
705                }
706                _ => {
707                    return Err(format!(
708                        "Invalid command '{}' for duration type. Valid commands: minimum, maximum, help, default",
709                        command
710                    ));
711                }
712            },
713            TypeSpecification::Veto { .. } => {
714                return Err(format!(
715                    "Invalid command '{}' for veto type. Veto is not a user-declarable type and cannot have constraints",
716                    command
717                ));
718            }
719            TypeSpecification::Undetermined => {
720                return Err(format!(
721                    "Invalid command '{}' for undetermined sentinel type. Undetermined is an internal type used during type inference and cannot have constraints",
722                    command
723                ));
724            }
725        }
726        Ok(self)
727    }
728}
729
730/// Parse a "number unit" string into a Scale or Ratio value according to the type.
731/// Caller must have obtained the TypeSpecification via unit_index from the unit in the string.
732pub fn parse_number_unit(
733    value_str: &str,
734    type_spec: &TypeSpecification,
735) -> Result<crate::parsing::ast::Value, String> {
736    use crate::literals::{NumberWithUnit, RatioLiteral};
737    use crate::parsing::ast::Value;
738
739    let trimmed = value_str.trim();
740    match type_spec {
741        TypeSpecification::Scale { units, .. } => {
742            if units.is_empty() {
743                unreachable!(
744                    "BUG: Scale type has no units; should have been validated during planning"
745                );
746            }
747            match trimmed.parse::<NumberWithUnit>() {
748                Ok(n) => {
749                    let unit = units.get(&n.1).map_err(|e| e.to_string())?;
750                    Ok(Value::Scale(n.0, unit.name.clone()))
751                }
752                Err(e) => {
753                    if trimmed.split_whitespace().count() == 1 && !trimmed.is_empty() {
754                        let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
755                        let example_unit = units
756                            .iter()
757                            .next()
758                            .expect("BUG: units non-empty after guard")
759                            .name
760                            .as_str();
761                        Err(format!(
762                            "Scale value must include a unit, for example: '{} {}'. Valid units: {}.",
763                            trimmed,
764                            example_unit,
765                            valid.join(", ")
766                        ))
767                    } else {
768                        Err(e)
769                    }
770                }
771            }
772        }
773        TypeSpecification::Ratio { units, .. } => {
774            if units.is_empty() {
775                unreachable!(
776                    "BUG: Ratio type has no units; should have been validated during planning"
777                );
778            }
779            match trimmed.parse::<RatioLiteral>()? {
780                RatioLiteral::Bare(n) => Ok(Value::Ratio(n, None)),
781                // Sigils are language-level constants. Built-in `ratio()` constructor
782                // seeds `percent`=100 and `permille`=1000, and the duplicate-name guard
783                // in `apply_constraint` (TypeConstraintCommand::Unit) rejects user redefinition,
784                // so these unit names are guaranteed present in every Ratio type.
785                RatioLiteral::Percent(n) => {
786                    let unit = units.get("percent").map_err(|e| e.to_string())?;
787                    Ok(Value::Ratio(n, Some(unit.name.clone())))
788                }
789                RatioLiteral::Permille(n) => {
790                    let unit = units.get("permille").map_err(|e| e.to_string())?;
791                    Ok(Value::Ratio(n, Some(unit.name.clone())))
792                }
793                RatioLiteral::Named { value, unit } => {
794                    let resolved = units.get(&unit).map_err(|e| e.to_string())?;
795                    Ok(Value::Ratio(
796                        value / resolved.value,
797                        Some(resolved.name.clone()),
798                    ))
799                }
800            }
801        }
802        _ => Err("parse_number_unit only accepts Scale or Ratio type".to_string()),
803    }
804}
805
806/// Parse a string value according to a TypeSpecification.
807/// Used to parse runtime user input into typed values.
808pub fn parse_value_from_string(
809    value_str: &str,
810    type_spec: &TypeSpecification,
811    source: &Source,
812) -> Result<crate::parsing::ast::Value, Error> {
813    use crate::parsing::ast::Value;
814
815    let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
816
817    match type_spec {
818        TypeSpecification::Text { .. } => value_str
819            .parse::<crate::literals::TextLiteral>()
820            .map(|t| Value::Text(t.0))
821            .map_err(to_err),
822        TypeSpecification::Number { .. } => value_str
823            .parse::<crate::literals::NumberLiteral>()
824            .map(|n| Value::Number(n.0))
825            .map_err(to_err),
826        TypeSpecification::Scale { .. } => {
827            parse_number_unit(value_str, type_spec).map_err(to_err)
828        }
829        TypeSpecification::Boolean { .. } => value_str
830            .parse::<BooleanValue>()
831            .map(Value::Boolean)
832            .map_err(to_err),
833        TypeSpecification::Date { .. } => {
834            let date = value_str.parse::<DateTimeValue>().map_err(to_err)?;
835            Ok(Value::Date(date))
836        }
837        TypeSpecification::Time { .. } => {
838            let time = value_str.parse::<TimeValue>().map_err(to_err)?;
839            Ok(Value::Time(time))
840        }
841        TypeSpecification::Duration { .. } => value_str
842            .parse::<crate::literals::DurationLiteral>()
843            .map(|d| Value::Duration(d.0, d.1))
844            .map_err(to_err),
845        TypeSpecification::Ratio { .. } => {
846            parse_number_unit(value_str, type_spec).map_err(to_err)
847        }
848        TypeSpecification::Veto { .. } => Err(to_err(
849            "Veto type cannot be parsed from string".to_string(),
850        )),
851        TypeSpecification::Undetermined => unreachable!(
852            "BUG: parse_value_from_string called with Undetermined sentinel type; this type exists only during type inference"
853        ),
854    }
855}
856
857// -----------------------------------------------------------------------------
858// Semantic value types (no parser dependency - used by evaluation, inversion, etc.)
859// -----------------------------------------------------------------------------
860
861/// Duration unit for semantic values (duplicated from parser to avoid parser dependency)
862#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
863#[serde(rename_all = "snake_case")]
864pub enum SemanticDurationUnit {
865    Year,
866    Month,
867    Week,
868    Day,
869    Hour,
870    Minute,
871    Second,
872    Millisecond,
873    Microsecond,
874}
875
876impl fmt::Display for SemanticDurationUnit {
877    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
878        let s = match self {
879            SemanticDurationUnit::Year => "years",
880            SemanticDurationUnit::Month => "months",
881            SemanticDurationUnit::Week => "weeks",
882            SemanticDurationUnit::Day => "days",
883            SemanticDurationUnit::Hour => "hours",
884            SemanticDurationUnit::Minute => "minutes",
885            SemanticDurationUnit::Second => "seconds",
886            SemanticDurationUnit::Millisecond => "milliseconds",
887            SemanticDurationUnit::Microsecond => "microseconds",
888        };
889        write!(f, "{}", s)
890    }
891}
892
893/// Target unit for conversion (semantic; used by evaluation/computation).
894/// Planning converts AST ConversionTarget into this so computation does not depend on parsing.
895/// Ratio vs scale is determined by looking up the unit in the type registry's unit index.
896#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
897#[serde(rename_all = "snake_case")]
898pub enum SemanticConversionTarget {
899    Duration(SemanticDurationUnit),
900    ScaleUnit(String),
901    RatioUnit(String),
902}
903
904impl fmt::Display for SemanticConversionTarget {
905    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
906        match self {
907            SemanticConversionTarget::Duration(u) => write!(f, "{}", u),
908            SemanticConversionTarget::ScaleUnit(s) => write!(f, "{}", s),
909            SemanticConversionTarget::RatioUnit(s) => write!(f, "{}", s),
910        }
911    }
912}
913
914/// Timezone for semantic date/time values
915#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
916pub struct SemanticTimezone {
917    pub offset_hours: i8,
918    pub offset_minutes: u8,
919}
920
921impl fmt::Display for SemanticTimezone {
922    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
923        if self.offset_hours == 0 && self.offset_minutes == 0 {
924            write!(f, "Z")
925        } else {
926            let sign = if self.offset_hours >= 0 { "+" } else { "-" };
927            let hours = self.offset_hours.abs();
928            write!(f, "{}{:02}:{:02}", sign, hours, self.offset_minutes)
929        }
930    }
931}
932
933/// Time-of-day for semantic values
934#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
935pub struct SemanticTime {
936    pub hour: u32,
937    pub minute: u32,
938    pub second: u32,
939    pub timezone: Option<SemanticTimezone>,
940}
941
942impl fmt::Display for SemanticTime {
943    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
944        write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)
945    }
946}
947
948/// Date-time for semantic values
949#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
950pub struct SemanticDateTime {
951    pub year: i32,
952    pub month: u32,
953    pub day: u32,
954    pub hour: u32,
955    pub minute: u32,
956    pub second: u32,
957    #[serde(default)]
958    pub microsecond: u32,
959    pub timezone: Option<SemanticTimezone>,
960}
961
962impl fmt::Display for SemanticDateTime {
963    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
964        let has_time = self.hour != 0
965            || self.minute != 0
966            || self.second != 0
967            || self.microsecond != 0
968            || self.timezone.is_some();
969        if !has_time {
970            write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
971        } else {
972            write!(
973                f,
974                "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
975                self.year, self.month, self.day, self.hour, self.minute, self.second
976            )?;
977            if self.microsecond != 0 {
978                write!(f, ".{:06}", self.microsecond)?;
979            }
980            if let Some(tz) = &self.timezone {
981                write!(f, "{}", tz)?;
982            }
983            Ok(())
984        }
985    }
986}
987
988/// Value payload (shape of a literal). No type attached.
989/// Scale unit is required; Ratio unit is optional (see plan ratio-units-optional.md).
990#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
991#[serde(rename_all = "snake_case")]
992pub enum ValueKind {
993    Number(Decimal),
994    /// Scale: value + unit (unit required)
995    Scale(Decimal, String),
996    Text(String),
997    Date(SemanticDateTime),
998    Time(SemanticTime),
999    Boolean(bool),
1000    /// Duration: value + unit
1001    Duration(Decimal, SemanticDurationUnit),
1002    /// Ratio: value + optional unit
1003    Ratio(Decimal, Option<String>),
1004}
1005
1006impl fmt::Display for ValueKind {
1007    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1008        use crate::parsing::ast::Value;
1009        match self {
1010            ValueKind::Number(n) => {
1011                let norm = n.normalize();
1012                let s = if norm.fract().is_zero() {
1013                    norm.trunc().to_string()
1014                } else {
1015                    norm.to_string()
1016                };
1017                write!(f, "{}", s)
1018            }
1019            ValueKind::Scale(n, u) => write!(f, "{}", Value::Scale(*n, u.clone())),
1020            ValueKind::Text(s) => write!(f, "{}", Value::Text(s.clone())),
1021            ValueKind::Ratio(r, u) => write!(f, "{}", Value::Ratio(*r, u.clone())),
1022            ValueKind::Date(dt) => write!(f, "{}", dt),
1023            ValueKind::Time(t) => write!(
1024                f,
1025                "{}",
1026                Value::Time(crate::parsing::ast::TimeValue {
1027                    hour: t.hour as u8,
1028                    minute: t.minute as u8,
1029                    second: t.second as u8,
1030                    timezone: t
1031                        .timezone
1032                        .as_ref()
1033                        .map(|tz| crate::parsing::ast::TimezoneValue {
1034                            offset_hours: tz.offset_hours,
1035                            offset_minutes: tz.offset_minutes,
1036                        }),
1037                })
1038            ),
1039            ValueKind::Boolean(b) => write!(f, "{}", b),
1040            ValueKind::Duration(v, u) => write!(f, "{} {}", v, u),
1041        }
1042    }
1043}
1044
1045// -----------------------------------------------------------------------------
1046// Resolved path types (moved from parsing::ast)
1047// -----------------------------------------------------------------------------
1048
1049/// A single segment in a resolved path traversal
1050///
1051/// Used in both DataPath and RulePath to represent spec traversal.
1052/// Each segment contains a data name that points to a spec.
1053#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1054pub struct PathSegment {
1055    /// The data name in this segment
1056    pub data: String,
1057    /// The spec this data references (resolved during planning)
1058    pub spec: String,
1059}
1060
1061/// Resolved path to a data (created during planning from AST DataReference)
1062///
1063/// Represents a fully resolved path through specs to reach a data.
1064/// All spec references are resolved during planning.
1065#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1066pub struct DataPath {
1067    /// Path segments (each is a spec traversal)
1068    pub segments: Vec<PathSegment>,
1069    /// Final data name
1070    pub data: String,
1071}
1072
1073impl DataPath {
1074    /// Create a data path from segments and data name (matches AST DataReference shape)
1075    pub fn new(segments: Vec<PathSegment>, data: String) -> Self {
1076        Self { segments, data }
1077    }
1078
1079    /// Create a local data path (no spec traversal)
1080    pub fn local(data: String) -> Self {
1081        Self {
1082            segments: vec![],
1083            data,
1084        }
1085    }
1086
1087    /// Dot-separated key used for matching user-provided data values (e.g. `"order.payment_method"`).
1088    /// Unlike `Display`, this omits the resolved spec name.
1089    pub fn input_key(&self) -> String {
1090        let mut s = String::new();
1091        for segment in &self.segments {
1092            s.push_str(&segment.data);
1093            s.push('.');
1094        }
1095        s.push_str(&self.data);
1096        s
1097    }
1098}
1099
1100/// Resolved path to a rule (created during planning from RuleReference)
1101///
1102/// Represents a fully resolved path through specs to reach a rule.
1103#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1104pub struct RulePath {
1105    /// Path segments (each is a spec traversal)
1106    pub segments: Vec<PathSegment>,
1107    /// Final rule name
1108    pub rule: String,
1109}
1110
1111impl RulePath {
1112    /// Create a rule path from segments and rule name (matches AST RuleReference shape)
1113    pub fn new(segments: Vec<PathSegment>, rule: String) -> Self {
1114        Self { segments, rule }
1115    }
1116}
1117
1118// -----------------------------------------------------------------------------
1119// Resolved expression types (created during planning)
1120// -----------------------------------------------------------------------------
1121
1122/// Resolved expression (all references resolved to paths, all literals typed)
1123///
1124/// Created during planning from AST Expression. All unresolved references
1125/// are converted to DataPath/RulePath, and all literals are typed.
1126#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1127pub struct Expression {
1128    pub kind: ExpressionKind,
1129    pub source_location: Option<Source>,
1130}
1131
1132impl Expression {
1133    pub fn new(kind: ExpressionKind, source_location: Source) -> Self {
1134        Self {
1135            kind,
1136            source_location: Some(source_location),
1137        }
1138    }
1139
1140    /// Create an expression with an optional source location
1141    pub fn with_source(kind: ExpressionKind, source_location: Option<Source>) -> Self {
1142        Self {
1143            kind,
1144            source_location,
1145        }
1146    }
1147
1148    /// Collect all DataPath references from this resolved expression tree
1149    pub fn collect_data_paths(&self, data: &mut std::collections::HashSet<DataPath>) {
1150        self.kind.collect_data_paths(data);
1151    }
1152}
1153
1154/// Resolved expression kind (only resolved variants, no unresolved references)
1155#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1156#[serde(rename_all = "snake_case")]
1157pub enum ExpressionKind {
1158    /// Resolved literal with type (boxed to keep enum small)
1159    Literal(Box<LiteralValue>),
1160    /// Resolved data path
1161    DataPath(DataPath),
1162    /// Resolved rule path
1163    RulePath(RulePath),
1164    LogicalAnd(Arc<Expression>, Arc<Expression>),
1165    Arithmetic(Arc<Expression>, ArithmeticComputation, Arc<Expression>),
1166    Comparison(Arc<Expression>, ComparisonComputation, Arc<Expression>),
1167    UnitConversion(Arc<Expression>, SemanticConversionTarget),
1168    LogicalNegation(Arc<Expression>, NegationType),
1169    MathematicalComputation(MathematicalComputation, Arc<Expression>),
1170    Veto(VetoExpression),
1171    /// The `now` keyword — resolved at evaluation to the effective datetime.
1172    Now,
1173    /// Date-relative sugar: `<date_expr> in past [<duration_expr>]` / `in future [...]`
1174    DateRelative(DateRelativeKind, Arc<Expression>, Option<Arc<Expression>>),
1175    /// Calendar-period sugar: `<date_expr> in [past|future] calendar year|month|week`
1176    DateCalendar(DateCalendarKind, CalendarUnit, Arc<Expression>),
1177}
1178
1179impl ExpressionKind {
1180    /// Collect all DataPath references from this expression kind
1181    pub(crate) fn collect_data_paths(&self, data: &mut std::collections::HashSet<DataPath>) {
1182        match self {
1183            ExpressionKind::DataPath(fp) => {
1184                data.insert(fp.clone());
1185            }
1186            ExpressionKind::LogicalAnd(left, right)
1187            | ExpressionKind::Arithmetic(left, _, right)
1188            | ExpressionKind::Comparison(left, _, right) => {
1189                left.collect_data_paths(data);
1190                right.collect_data_paths(data);
1191            }
1192            ExpressionKind::UnitConversion(inner, _)
1193            | ExpressionKind::LogicalNegation(inner, _)
1194            | ExpressionKind::MathematicalComputation(_, inner) => {
1195                inner.collect_data_paths(data);
1196            }
1197            ExpressionKind::DateRelative(_, date_expr, tolerance) => {
1198                date_expr.collect_data_paths(data);
1199                if let Some(tol) = tolerance {
1200                    tol.collect_data_paths(data);
1201                }
1202            }
1203            ExpressionKind::DateCalendar(_, _, date_expr) => {
1204                date_expr.collect_data_paths(data);
1205            }
1206            ExpressionKind::Literal(_)
1207            | ExpressionKind::RulePath(_)
1208            | ExpressionKind::Veto(_)
1209            | ExpressionKind::Now => {}
1210        }
1211    }
1212}
1213
1214// -----------------------------------------------------------------------------
1215// Resolved types and values
1216// -----------------------------------------------------------------------------
1217
1218/// Whether two resolved specs are the same temporal slice (same `name` and `effective_from` as [`LemmaSpec`]'s `PartialEq`).
1219/// Not `Arc` pointer identity: [`Arc`] equality uses the inner value.
1220#[inline]
1221#[must_use]
1222pub fn is_same_spec(left: &LemmaSpec, right: &LemmaSpec) -> bool {
1223    left == right
1224}
1225
1226/// Where the custom extension chain is rooted: same spec as this type, or imported from another resolved spec.
1227#[derive(Clone, Debug, Serialize, Deserialize)]
1228#[serde(tag = "kind", rename_all = "snake_case")]
1229pub enum TypeDefiningSpec {
1230    /// Parent type is defined in the same spec as this type.
1231    Local,
1232    /// Parent type was resolved from types loaded from this dependency.
1233    Import { spec: Arc<LemmaSpec> },
1234}
1235
1236/// What this type extends (primitive built-in or custom type by name).
1237#[derive(Clone, Debug, Serialize, Deserialize)]
1238#[serde(rename_all = "snake_case")]
1239pub enum TypeExtends {
1240    /// Extends a primitive built-in type (number, boolean, text, etc.)
1241    Primitive,
1242    /// Extends a custom type: parent is the immediate parent type name; family is the root of the extension chain (topmost custom type name).
1243    /// `defining_spec` records whether the parent chain is local or imported from another spec; see [`TypeDefiningSpec`].
1244    Custom {
1245        parent: String,
1246        family: String,
1247        defining_spec: TypeDefiningSpec,
1248    },
1249}
1250
1251impl PartialEq for TypeExtends {
1252    fn eq(&self, other: &Self) -> bool {
1253        match (self, other) {
1254            (TypeExtends::Primitive, TypeExtends::Primitive) => true,
1255            (
1256                TypeExtends::Custom {
1257                    parent: lp,
1258                    family: lf,
1259                    defining_spec: ld,
1260                },
1261                TypeExtends::Custom {
1262                    parent: rp,
1263                    family: rf,
1264                    defining_spec: rd,
1265                },
1266            ) => {
1267                lp == rp
1268                    && lf == rf
1269                    && match (ld, rd) {
1270                        (TypeDefiningSpec::Local, TypeDefiningSpec::Local) => true,
1271                        (
1272                            TypeDefiningSpec::Import { spec: left },
1273                            TypeDefiningSpec::Import { spec: right },
1274                        ) => is_same_spec(left, right),
1275                        _ => false,
1276                    }
1277            }
1278            _ => false,
1279        }
1280    }
1281}
1282
1283impl Eq for TypeExtends {}
1284
1285impl TypeExtends {
1286    /// Custom extension in the same spec as the defining type (no cross-spec import for the parent chain).
1287    #[must_use]
1288    pub fn custom_local(parent: String, family: String) -> Self {
1289        TypeExtends::Custom {
1290            parent,
1291            family,
1292            defining_spec: TypeDefiningSpec::Local,
1293        }
1294    }
1295
1296    /// Returns the parent type name if this type extends a custom type.
1297    #[must_use]
1298    pub fn parent_name(&self) -> Option<&str> {
1299        match self {
1300            TypeExtends::Primitive => None,
1301            TypeExtends::Custom { parent, .. } => Some(parent.as_str()),
1302        }
1303    }
1304}
1305
1306/// Resolved type after planning
1307///
1308/// Contains a type specification and optional name. Created during planning
1309/// from TypeSpecification in the AST.
1310#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1311pub struct LemmaType {
1312    /// Optional type name (e.g., "age", "temperature")
1313    pub name: Option<String>,
1314    /// The type specification (Boolean, Number, Scale, etc.).
1315    /// Serialized as a discriminated union: the variant tag appears as
1316    /// `"kind"` alongside `name` and `extends`, and the variant's fields
1317    /// are flattened to the top level.
1318    #[serde(flatten)]
1319    pub specifications: TypeSpecification,
1320    /// What this type extends (primitive or custom from a spec)
1321    pub extends: TypeExtends,
1322}
1323
1324impl LemmaType {
1325    /// Create a new type with a name
1326    pub fn new(name: String, specifications: TypeSpecification, extends: TypeExtends) -> Self {
1327        Self {
1328            name: Some(name),
1329            specifications,
1330            extends,
1331        }
1332    }
1333
1334    /// Create a type without a name (anonymous/inline type)
1335    pub fn without_name(specifications: TypeSpecification, extends: TypeExtends) -> Self {
1336        Self {
1337            name: None,
1338            specifications,
1339            extends,
1340        }
1341    }
1342
1343    /// Create a primitive type (no name, extends Primitive)
1344    pub fn primitive(specifications: TypeSpecification) -> Self {
1345        Self {
1346            name: None,
1347            specifications,
1348            extends: TypeExtends::Primitive,
1349        }
1350    }
1351
1352    /// Get the type name, or a default based on the type specification
1353    pub fn name(&self) -> String {
1354        self.name.clone().unwrap_or_else(|| {
1355            match &self.specifications {
1356                TypeSpecification::Boolean { .. } => "boolean",
1357                TypeSpecification::Scale { .. } => "scale",
1358                TypeSpecification::Number { .. } => "number",
1359                TypeSpecification::Text { .. } => "text",
1360                TypeSpecification::Date { .. } => "date",
1361                TypeSpecification::Time { .. } => "time",
1362                TypeSpecification::Duration { .. } => "duration",
1363                TypeSpecification::Ratio { .. } => "ratio",
1364                TypeSpecification::Veto { .. } => "veto",
1365                TypeSpecification::Undetermined => "undetermined",
1366            }
1367            .to_string()
1368        })
1369    }
1370
1371    /// Check if this type is boolean
1372    pub fn is_boolean(&self) -> bool {
1373        matches!(&self.specifications, TypeSpecification::Boolean { .. })
1374    }
1375
1376    /// Check if this type is scale
1377    pub fn is_scale(&self) -> bool {
1378        matches!(&self.specifications, TypeSpecification::Scale { .. })
1379    }
1380
1381    /// Check if this type is number (dimensionless)
1382    pub fn is_number(&self) -> bool {
1383        matches!(&self.specifications, TypeSpecification::Number { .. })
1384    }
1385
1386    /// Check if this type is numeric (either scale or number)
1387    pub fn is_numeric(&self) -> bool {
1388        matches!(
1389            &self.specifications,
1390            TypeSpecification::Scale { .. } | TypeSpecification::Number { .. }
1391        )
1392    }
1393
1394    /// Check if this type is text
1395    pub fn is_text(&self) -> bool {
1396        matches!(&self.specifications, TypeSpecification::Text { .. })
1397    }
1398
1399    /// Check if this type is date
1400    pub fn is_date(&self) -> bool {
1401        matches!(&self.specifications, TypeSpecification::Date { .. })
1402    }
1403
1404    /// Check if this type is time
1405    pub fn is_time(&self) -> bool {
1406        matches!(&self.specifications, TypeSpecification::Time { .. })
1407    }
1408
1409    /// Check if this type is duration
1410    pub fn is_duration(&self) -> bool {
1411        matches!(&self.specifications, TypeSpecification::Duration { .. })
1412    }
1413
1414    /// Check if this type is ratio
1415    pub fn is_ratio(&self) -> bool {
1416        matches!(&self.specifications, TypeSpecification::Ratio { .. })
1417    }
1418
1419    /// Check if this type is veto
1420    pub fn vetoed(&self) -> bool {
1421        matches!(&self.specifications, TypeSpecification::Veto { .. })
1422    }
1423
1424    /// True if this type is the undetermined sentinel (type could not be inferred).
1425    pub fn is_undetermined(&self) -> bool {
1426        matches!(&self.specifications, TypeSpecification::Undetermined)
1427    }
1428
1429    /// Check if two types have the same base type specification (ignoring constraints)
1430    pub fn has_same_base_type(&self, other: &LemmaType) -> bool {
1431        use TypeSpecification::*;
1432        matches!(
1433            (&self.specifications, &other.specifications),
1434            (Boolean { .. }, Boolean { .. })
1435                | (Number { .. }, Number { .. })
1436                | (Scale { .. }, Scale { .. })
1437                | (Text { .. }, Text { .. })
1438                | (Date { .. }, Date { .. })
1439                | (Time { .. }, Time { .. })
1440                | (Duration { .. }, Duration { .. })
1441                | (Ratio { .. }, Ratio { .. })
1442                | (Veto { .. }, Veto { .. })
1443                | (Undetermined, Undetermined)
1444        )
1445    }
1446
1447    /// For scale types, returns the family name (root of the extension chain). For Custom extends, returns the family field; for Primitive, returns the type's own name (the type is the root). For non-scale types, returns None.
1448    #[must_use]
1449    pub fn scale_family_name(&self) -> Option<&str> {
1450        if !self.is_scale() {
1451            return None;
1452        }
1453        match &self.extends {
1454            TypeExtends::Custom { family, .. } => Some(family.as_str()),
1455            TypeExtends::Primitive => self.name.as_deref(),
1456        }
1457    }
1458
1459    /// Returns true if both types are scale and belong to the same scale family (same family name).
1460    /// Two anonymous primitive scales (no name, no family) are considered compatible.
1461    #[must_use]
1462    pub fn same_scale_family(&self, other: &LemmaType) -> bool {
1463        if !self.is_scale() || !other.is_scale() {
1464            return false;
1465        }
1466        match (self.scale_family_name(), other.scale_family_name()) {
1467            (Some(self_family), Some(other_family)) => self_family == other_family,
1468            // Two anonymous primitive scales are compatible with each other.
1469            (None, None) => true,
1470            _ => false,
1471        }
1472    }
1473
1474    /// Create a Veto LemmaType
1475    pub fn veto_type() -> Self {
1476        Self::primitive(TypeSpecification::veto())
1477    }
1478
1479    /// LemmaType sentinel for undetermined type (used during inference when a type cannot be determined).
1480    /// Propagates through expressions and is never present in a validated graph.
1481    pub fn undetermined_type() -> Self {
1482        Self::primitive(TypeSpecification::Undetermined)
1483    }
1484
1485    /// Decimal places for display (Number, Scale, and Ratio). Used by formatters.
1486    /// Ratio: optional, no default; when None display is normalized (no trailing zeros).
1487    pub fn decimal_places(&self) -> Option<u8> {
1488        match &self.specifications {
1489            TypeSpecification::Number { decimals, .. } => *decimals,
1490            TypeSpecification::Scale { decimals, .. } => *decimals,
1491            TypeSpecification::Ratio { decimals, .. } => *decimals,
1492            _ => None,
1493        }
1494    }
1495
1496    /// Get an example value string for this type, suitable for UI help text
1497    pub fn example_value(&self) -> &'static str {
1498        match &self.specifications {
1499            TypeSpecification::Text { .. } => "\"hello world\"",
1500            TypeSpecification::Scale { .. } => "12.50 eur",
1501            TypeSpecification::Number { .. } => "3.14",
1502            TypeSpecification::Boolean { .. } => "true",
1503            TypeSpecification::Date { .. } => "2023-12-25T14:30:00Z",
1504            TypeSpecification::Veto { .. } => "veto",
1505            TypeSpecification::Time { .. } => "14:30:00",
1506            TypeSpecification::Duration { .. } => "90 minutes",
1507            TypeSpecification::Ratio { .. } => "50%",
1508            TypeSpecification::Undetermined => unreachable!(
1509                "BUG: example_value called on Undetermined sentinel type; this type must never reach user-facing code"
1510            ),
1511        }
1512    }
1513
1514    /// Factor for a unit of this scale type (for unit conversion during evaluation only).
1515    /// Planning must validate conversions first and return Error for invalid units.
1516    /// If called with a non-scale type or unknown unit name, panics (invariant violation).
1517    #[must_use]
1518    pub fn scale_unit_factor(&self, unit_name: &str) -> Decimal {
1519        let units = match &self.specifications {
1520            TypeSpecification::Scale { units, .. } => units,
1521            _ => unreachable!(
1522                "BUG: scale_unit_factor called with non-scale type {}; only call during evaluation after planning validated scale conversion",
1523                self.name()
1524            ),
1525        };
1526        match units
1527            .iter()
1528            .find(|u| u.name.eq_ignore_ascii_case(unit_name))
1529        {
1530            Some(ScaleUnit { value, .. }) => *value,
1531            None => {
1532                let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
1533                unreachable!(
1534                    "BUG: unknown unit '{}' for scale type {} (valid: {}); planning must reject invalid conversions with Error",
1535                    unit_name,
1536                    self.name(),
1537                    valid.join(", ")
1538                );
1539            }
1540        }
1541    }
1542}
1543
1544/// Literal value with type. The single value type in semantics.
1545#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
1546pub struct LiteralValue {
1547    pub value: ValueKind,
1548    pub lemma_type: LemmaType,
1549}
1550
1551impl Serialize for LiteralValue {
1552    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1553    where
1554        S: serde::Serializer,
1555    {
1556        use serde::ser::SerializeStruct;
1557        let mut state = serializer.serialize_struct("LiteralValue", 3)?;
1558        state.serialize_field("value", &self.value)?;
1559        state.serialize_field("lemma_type", &self.lemma_type)?;
1560        state.serialize_field("display_value", &self.display_value())?;
1561        state.end()
1562    }
1563}
1564
1565impl LiteralValue {
1566    pub fn text(s: String) -> Self {
1567        Self {
1568            value: ValueKind::Text(s),
1569            lemma_type: primitive_text().clone(),
1570        }
1571    }
1572
1573    pub fn text_with_type(s: String, lemma_type: LemmaType) -> Self {
1574        Self {
1575            value: ValueKind::Text(s),
1576            lemma_type,
1577        }
1578    }
1579
1580    pub fn number(n: Decimal) -> Self {
1581        Self {
1582            value: ValueKind::Number(n),
1583            lemma_type: primitive_number().clone(),
1584        }
1585    }
1586
1587    pub fn number_with_type(n: Decimal, lemma_type: LemmaType) -> Self {
1588        Self {
1589            value: ValueKind::Number(n),
1590            lemma_type,
1591        }
1592    }
1593
1594    pub fn scale_with_type(n: Decimal, unit: String, lemma_type: LemmaType) -> Self {
1595        Self {
1596            value: ValueKind::Scale(n, unit),
1597            lemma_type,
1598        }
1599    }
1600
1601    /// Number interpreted as a scale value in the given unit (e.g. "3 in usd" where 3 is a number).
1602    /// Creates an anonymous one-unit scale type so computation does not depend on parsing types.
1603    pub fn number_interpreted_as_scale(value: Decimal, unit_name: String) -> Self {
1604        let lemma_type = LemmaType {
1605            name: None,
1606            specifications: TypeSpecification::Scale {
1607                minimum: None,
1608                maximum: None,
1609                decimals: None,
1610                precision: None,
1611                units: ScaleUnits::from(vec![ScaleUnit {
1612                    name: unit_name.clone(),
1613                    value: Decimal::from(1),
1614                }]),
1615                help: "Format: {value} {unit} (e.g. 100 kilograms)".to_string(),
1616            },
1617            extends: TypeExtends::Primitive,
1618        };
1619        Self {
1620            value: ValueKind::Scale(value, unit_name),
1621            lemma_type,
1622        }
1623    }
1624
1625    pub fn from_bool(b: bool) -> Self {
1626        Self {
1627            value: ValueKind::Boolean(b),
1628            lemma_type: primitive_boolean().clone(),
1629        }
1630    }
1631
1632    pub fn date(dt: SemanticDateTime) -> Self {
1633        Self {
1634            value: ValueKind::Date(dt),
1635            lemma_type: primitive_date().clone(),
1636        }
1637    }
1638
1639    pub fn date_with_type(dt: SemanticDateTime, lemma_type: LemmaType) -> Self {
1640        Self {
1641            value: ValueKind::Date(dt),
1642            lemma_type,
1643        }
1644    }
1645
1646    pub fn time(t: SemanticTime) -> Self {
1647        Self {
1648            value: ValueKind::Time(t),
1649            lemma_type: primitive_time().clone(),
1650        }
1651    }
1652
1653    pub fn time_with_type(t: SemanticTime, lemma_type: LemmaType) -> Self {
1654        Self {
1655            value: ValueKind::Time(t),
1656            lemma_type,
1657        }
1658    }
1659
1660    pub fn duration(value: Decimal, unit: SemanticDurationUnit) -> Self {
1661        Self {
1662            value: ValueKind::Duration(value, unit),
1663            lemma_type: primitive_duration().clone(),
1664        }
1665    }
1666
1667    pub fn duration_with_type(
1668        value: Decimal,
1669        unit: SemanticDurationUnit,
1670        lemma_type: LemmaType,
1671    ) -> Self {
1672        Self {
1673            value: ValueKind::Duration(value, unit),
1674            lemma_type,
1675        }
1676    }
1677
1678    pub fn ratio(r: Decimal, unit: Option<String>) -> Self {
1679        Self {
1680            value: ValueKind::Ratio(r, unit),
1681            lemma_type: primitive_ratio().clone(),
1682        }
1683    }
1684
1685    pub fn ratio_with_type(r: Decimal, unit: Option<String>, lemma_type: LemmaType) -> Self {
1686        Self {
1687            value: ValueKind::Ratio(r, unit),
1688            lemma_type,
1689        }
1690    }
1691
1692    /// Get a display string for this value (for UI/output)
1693    pub fn display_value(&self) -> String {
1694        format!("{}", self)
1695    }
1696
1697    /// Approximate byte size for resource limit checks (string representation length)
1698    pub fn byte_size(&self) -> usize {
1699        format!("{}", self).len()
1700    }
1701
1702    /// Get the resolved type of this literal
1703    pub fn get_type(&self) -> &LemmaType {
1704        &self.lemma_type
1705    }
1706}
1707
1708/// Data value: literal, type declaration (resolved type only), or spec reference.
1709#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1710#[serde(rename_all = "snake_case")]
1711pub enum DataValue {
1712    Literal(LiteralValue),
1713    TypeDeclaration { resolved_type: LemmaType },
1714    SpecReference(String),
1715}
1716
1717/// Data: path, value, and source location.
1718#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1719pub struct Data {
1720    pub path: DataPath,
1721    pub value: DataValue,
1722    pub source: Option<Source>,
1723}
1724
1725/// What a [`DataDefinition::Reference`] copies its value from: either another data path
1726/// or a rule whose result becomes this data's value.
1727#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1728#[serde(rename_all = "snake_case", tag = "kind")]
1729pub enum ReferenceTarget {
1730    Data(DataPath),
1731    Rule(RulePath),
1732}
1733
1734/// Resolved data value for the execution plan: aligned with [`DataValue`] but with source per variant.
1735#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1736#[serde(rename_all = "snake_case")]
1737pub enum DataDefinition {
1738    /// Value-holding data: current value (literal or default); type is on the value.
1739    Value { value: LiteralValue, source: Source },
1740    /// Type-only data: schema known, value to be supplied (e.g. via with_values).
1741    /// `declared_default` carries the `-> default ...` payload for this binding or
1742    /// the default inherited from the parent type chain, if any; value-promoting code
1743    /// uses it instead of re-deriving defaults from [`TypeSpecification`].
1744    TypeDeclaration {
1745        resolved_type: LemmaType,
1746        declared_default: Option<ValueKind>,
1747        source: Source,
1748    },
1749    /// Spec reference data: holds the resolved spec.
1750    SpecRef {
1751        spec: Arc<crate::parsing::ast::LemmaSpec>,
1752        source: Source,
1753    },
1754    /// Value-copy reference to another data or a rule result.
1755    ///
1756    /// `resolved_type` is the merged type that the copied value must satisfy at
1757    /// evaluation time. Merging folds together: (1) the LHS's own declared type,
1758    /// if any; (2) the target's type (data schema type or rule return type);
1759    /// (3) any `local_constraints` written after the `->` on the reference itself.
1760    /// Merging happens in a dedicated pass once all data and rule types are
1761    /// known; before that pass, `resolved_type` holds a provisional value and
1762    /// must not be consumed for type checking.
1763    ///
1764    /// `local_constraints` preserves the raw constraint list from the reference's
1765    /// `-> ...` tail (e.g. `minimum 5` in `data license2: law.other -> minimum 5`)
1766    /// for that merging pass. It is `None` when the reference has no trailing
1767    /// constraints.
1768    ///
1769    /// `local_default` carries any `default <value>` constraint from the
1770    /// reference's `-> ...` tail. The reference-merge pass extracts it from the
1771    /// constraint list during type resolution; the evaluator falls back to it
1772    /// when the target value/rule is missing or vetoes for missing data so the
1773    /// downstream sees the declared default instead of a missing-data veto.
1774    ///
1775    /// The reference itself is evaluated by copying the target's value (data path)
1776    /// or the target rule's result in topological order; `with_data_values`
1777    /// entries for a referenced path override the reference with a literal.
1778    Reference {
1779        target: ReferenceTarget,
1780        resolved_type: LemmaType,
1781        local_constraints: Option<Vec<Constraint>>,
1782        local_default: Option<ValueKind>,
1783        source: Source,
1784    },
1785}
1786
1787impl DataDefinition {
1788    /// Returns the schema type for value, type-declaration, and reference data; `None` for spec references.
1789    pub fn schema_type(&self) -> Option<&LemmaType> {
1790        match self {
1791            DataDefinition::Value { value, .. } => Some(&value.lemma_type),
1792            DataDefinition::TypeDeclaration { resolved_type, .. } => Some(resolved_type),
1793            DataDefinition::Reference { resolved_type, .. } => Some(resolved_type),
1794            DataDefinition::SpecRef { .. } => None,
1795        }
1796    }
1797
1798    /// Returns the literal value when the data already holds one. A `Reference`'s
1799    /// value is produced by the evaluator at runtime, so at plan-time it has no
1800    /// value yet.
1801    pub fn value(&self) -> Option<&LiteralValue> {
1802        match self {
1803            DataDefinition::Value { value, .. } => Some(value),
1804            DataDefinition::TypeDeclaration { .. }
1805            | DataDefinition::SpecRef { .. }
1806            | DataDefinition::Reference { .. } => None,
1807        }
1808    }
1809
1810    /// Schema-level default for this data: the value to surface in
1811    /// [`SpecSchema::data`]'s `default` field.
1812    ///
1813    /// Differs from [`Self::value`]: `Value` data already carries the literal
1814    /// (a default that planning promoted to a value), so both return the
1815    /// same thing for that variant. For `Reference` and `TypeDeclaration` the
1816    /// schema-level default lives separately from the runtime value (the
1817    /// reference's copied target value, or the type-only data's user-supplied
1818    /// value); we synthesize the `LiteralValue` from the declared default and
1819    /// the `resolved_type` here so callers don't have to. `SpecRef` has no
1820    /// schema default.
1821    pub fn schema_default(&self) -> Option<LiteralValue> {
1822        match self {
1823            DataDefinition::Value { value, .. } => Some(value.clone()),
1824            DataDefinition::TypeDeclaration {
1825                resolved_type,
1826                declared_default: Some(dv),
1827                ..
1828            } => Some(LiteralValue {
1829                value: dv.clone(),
1830                lemma_type: resolved_type.clone(),
1831            }),
1832            DataDefinition::Reference {
1833                resolved_type,
1834                local_default: Some(dv),
1835                ..
1836            } => Some(LiteralValue {
1837                value: dv.clone(),
1838                lemma_type: resolved_type.clone(),
1839            }),
1840            DataDefinition::TypeDeclaration { .. }
1841            | DataDefinition::Reference { .. }
1842            | DataDefinition::SpecRef { .. } => None,
1843        }
1844    }
1845
1846    /// Returns the source location for this data.
1847    pub fn source(&self) -> &Source {
1848        match self {
1849            DataDefinition::Value { source, .. } => source,
1850            DataDefinition::TypeDeclaration { source, .. } => source,
1851            DataDefinition::SpecRef { source, .. } => source,
1852            DataDefinition::Reference { source, .. } => source,
1853        }
1854    }
1855
1856    /// Returns the referenced spec Arc for spec reference data; `None` otherwise.
1857    pub fn spec_arc(&self) -> Option<&Arc<crate::parsing::ast::LemmaSpec>> {
1858        match self {
1859            DataDefinition::Value { .. }
1860            | DataDefinition::TypeDeclaration { .. }
1861            | DataDefinition::Reference { .. } => None,
1862            DataDefinition::SpecRef { spec: spec_arc, .. } => Some(spec_arc),
1863        }
1864    }
1865
1866    /// Returns the referenced spec name for spec reference data; `None` otherwise.
1867    pub fn spec_ref(&self) -> Option<&str> {
1868        match self {
1869            DataDefinition::Value { .. }
1870            | DataDefinition::TypeDeclaration { .. }
1871            | DataDefinition::Reference { .. } => None,
1872            DataDefinition::SpecRef { spec, .. } => Some(&spec.name),
1873        }
1874    }
1875
1876    /// Returns the reference target when this data copies a value from another
1877    /// data path or rule result; `None` otherwise.
1878    pub fn reference_target(&self) -> Option<&ReferenceTarget> {
1879        match self {
1880            DataDefinition::Reference { target, .. } => Some(target),
1881            _ => None,
1882        }
1883    }
1884}
1885
1886/// Convert parser Value to ValueKind. Fails if Scale/Ratio have no unit (strict).
1887pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
1888    use crate::parsing::ast::Value;
1889    Ok(match value {
1890        Value::Number(n) => ValueKind::Number(*n),
1891        Value::Text(s) => ValueKind::Text(s.clone()),
1892        Value::Boolean(b) => ValueKind::Boolean(bool::from(*b)),
1893        Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
1894        Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
1895        Value::Duration(n, u) => ValueKind::Duration(*n, duration_unit_to_semantic(u)),
1896        Value::Scale(n, unit) => ValueKind::Scale(*n, unit.clone()),
1897        Value::Ratio(n, unit) => ValueKind::Ratio(*n, unit.clone()),
1898    })
1899}
1900
1901/// Convert AST date-time to semantic (for tests and planning).
1902pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
1903    SemanticDateTime {
1904        year: dt.year,
1905        month: dt.month,
1906        day: dt.day,
1907        hour: dt.hour,
1908        minute: dt.minute,
1909        second: dt.second,
1910        microsecond: dt.microsecond,
1911        timezone: dt.timezone.as_ref().map(|tz| SemanticTimezone {
1912            offset_hours: tz.offset_hours,
1913            offset_minutes: tz.offset_minutes,
1914        }),
1915    }
1916}
1917
1918/// Convert AST time to semantic (for tests and planning).
1919pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
1920    SemanticTime {
1921        hour: t.hour.into(),
1922        minute: t.minute.into(),
1923        second: t.second.into(),
1924        timezone: t.timezone.as_ref().map(|tz| SemanticTimezone {
1925            offset_hours: tz.offset_hours,
1926            offset_minutes: tz.offset_minutes,
1927        }),
1928    }
1929}
1930
1931/// Compare two semantic date-time values by year, month, day, hour, minute, second.
1932///
1933/// Microsecond and timezone are intentionally excluded so the ordering matches
1934/// what user-facing date constraints can express (lemma date literals do not
1935/// expose sub-second precision, and timezone normalisation is a separate concern
1936/// handled at evaluation time).
1937pub(crate) fn compare_semantic_dates(
1938    left: &SemanticDateTime,
1939    right: &SemanticDateTime,
1940) -> std::cmp::Ordering {
1941    left.year
1942        .cmp(&right.year)
1943        .then_with(|| left.month.cmp(&right.month))
1944        .then_with(|| left.day.cmp(&right.day))
1945        .then_with(|| left.hour.cmp(&right.hour))
1946        .then_with(|| left.minute.cmp(&right.minute))
1947        .then_with(|| left.second.cmp(&right.second))
1948}
1949
1950/// Compare two semantic time values by hour, minute, second.
1951///
1952/// Timezone is excluded for the same reason as [`compare_semantic_dates`].
1953pub(crate) fn compare_semantic_times(
1954    left: &SemanticTime,
1955    right: &SemanticTime,
1956) -> std::cmp::Ordering {
1957    left.hour
1958        .cmp(&right.hour)
1959        .then_with(|| left.minute.cmp(&right.minute))
1960        .then_with(|| left.second.cmp(&right.second))
1961}
1962
1963/// Convert AST duration unit to semantic (for tests and planning).
1964pub(crate) fn duration_unit_to_semantic(
1965    u: &crate::parsing::ast::DurationUnit,
1966) -> SemanticDurationUnit {
1967    use crate::parsing::ast::DurationUnit as DU;
1968    match u {
1969        DU::Year => SemanticDurationUnit::Year,
1970        DU::Month => SemanticDurationUnit::Month,
1971        DU::Week => SemanticDurationUnit::Week,
1972        DU::Day => SemanticDurationUnit::Day,
1973        DU::Hour => SemanticDurationUnit::Hour,
1974        DU::Minute => SemanticDurationUnit::Minute,
1975        DU::Second => SemanticDurationUnit::Second,
1976        DU::Millisecond => SemanticDurationUnit::Millisecond,
1977        DU::Microsecond => SemanticDurationUnit::Microsecond,
1978    }
1979}
1980
1981/// Convert AST conversion target to semantic (planning boundary; evaluation/computation use only semantic).
1982///
1983/// The AST uses `ConversionTarget::Unit(name)` for non-duration units; this function looks up `name`
1984/// in the spec's unit index and returns `RatioUnit` or `ScaleUnit` based on the type that defines
1985/// the unit. The unit must be defined by a scale or ratio type in the spec (e.g. primitive ratio for
1986/// "percent", "permille").
1987pub fn conversion_target_to_semantic(
1988    ct: &ConversionTarget,
1989    unit_index: Option<&HashMap<String, LemmaType>>,
1990) -> Result<SemanticConversionTarget, String> {
1991    match ct {
1992        ConversionTarget::Duration(u) => Ok(SemanticConversionTarget::Duration(
1993            duration_unit_to_semantic(u),
1994        )),
1995        ConversionTarget::Unit(name) => {
1996            let index = unit_index.ok_or_else(|| {
1997                "Unit conversion requires type resolution; unit index not available.".to_string()
1998            })?;
1999            let lemma_type = index.get(name).ok_or_else(|| {
2000                format!(
2001                    "Unknown unit '{}'. Unit must be defined by a scale or ratio type.",
2002                    name
2003                )
2004            })?;
2005            if lemma_type.is_ratio() {
2006                Ok(SemanticConversionTarget::RatioUnit(name.clone()))
2007            } else if lemma_type.is_scale() {
2008                Ok(SemanticConversionTarget::ScaleUnit(name.clone()))
2009            } else {
2010                Err(format!(
2011                    "Unit '{}' is not a ratio or scale type; cannot use it in conversion.",
2012                    name
2013                ))
2014            }
2015        }
2016    }
2017}
2018
2019// -----------------------------------------------------------------------------
2020// Primitive type constructors (moved from parsing::ast)
2021// -----------------------------------------------------------------------------
2022
2023// Private statics for lazy initialization
2024static PRIMITIVE_BOOLEAN: OnceLock<LemmaType> = OnceLock::new();
2025static PRIMITIVE_SCALE: OnceLock<LemmaType> = OnceLock::new();
2026static PRIMITIVE_NUMBER: OnceLock<LemmaType> = OnceLock::new();
2027static PRIMITIVE_TEXT: OnceLock<LemmaType> = OnceLock::new();
2028static PRIMITIVE_DATE: OnceLock<LemmaType> = OnceLock::new();
2029static PRIMITIVE_TIME: OnceLock<LemmaType> = OnceLock::new();
2030static PRIMITIVE_DURATION: OnceLock<LemmaType> = OnceLock::new();
2031static PRIMITIVE_RATIO: OnceLock<LemmaType> = OnceLock::new();
2032
2033/// Primitive types use the default TypeSpecification from the parser (single source of truth).
2034#[must_use]
2035pub fn primitive_boolean() -> &'static LemmaType {
2036    PRIMITIVE_BOOLEAN.get_or_init(|| LemmaType::primitive(TypeSpecification::boolean()))
2037}
2038
2039#[must_use]
2040pub fn primitive_scale() -> &'static LemmaType {
2041    PRIMITIVE_SCALE.get_or_init(|| LemmaType::primitive(TypeSpecification::scale()))
2042}
2043
2044#[must_use]
2045pub fn primitive_number() -> &'static LemmaType {
2046    PRIMITIVE_NUMBER.get_or_init(|| LemmaType::primitive(TypeSpecification::number()))
2047}
2048
2049#[must_use]
2050pub fn primitive_text() -> &'static LemmaType {
2051    PRIMITIVE_TEXT.get_or_init(|| LemmaType::primitive(TypeSpecification::text()))
2052}
2053
2054#[must_use]
2055pub fn primitive_date() -> &'static LemmaType {
2056    PRIMITIVE_DATE.get_or_init(|| LemmaType::primitive(TypeSpecification::date()))
2057}
2058
2059#[must_use]
2060pub fn primitive_time() -> &'static LemmaType {
2061    PRIMITIVE_TIME.get_or_init(|| LemmaType::primitive(TypeSpecification::time()))
2062}
2063
2064#[must_use]
2065pub fn primitive_duration() -> &'static LemmaType {
2066    PRIMITIVE_DURATION.get_or_init(|| LemmaType::primitive(TypeSpecification::duration()))
2067}
2068
2069#[must_use]
2070pub fn primitive_ratio() -> &'static LemmaType {
2071    PRIMITIVE_RATIO.get_or_init(|| LemmaType::primitive(TypeSpecification::ratio()))
2072}
2073
2074/// Map PrimitiveKind to TypeSpecification. Single source of truth for primitive type resolution.
2075#[must_use]
2076pub fn type_spec_for_primitive(kind: PrimitiveKind) -> TypeSpecification {
2077    match kind {
2078        PrimitiveKind::Boolean => TypeSpecification::boolean(),
2079        PrimitiveKind::Scale => TypeSpecification::scale(),
2080        PrimitiveKind::Number => TypeSpecification::number(),
2081        PrimitiveKind::Percent | PrimitiveKind::Ratio => TypeSpecification::ratio(),
2082        PrimitiveKind::Text => TypeSpecification::text(),
2083        PrimitiveKind::Date => TypeSpecification::date(),
2084        PrimitiveKind::Time => TypeSpecification::time(),
2085        PrimitiveKind::Duration => TypeSpecification::duration(),
2086    }
2087}
2088
2089// -----------------------------------------------------------------------------
2090// Display implementations
2091// -----------------------------------------------------------------------------
2092
2093impl fmt::Display for PathSegment {
2094    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2095        write!(f, "{} → {}", self.data, self.spec)
2096    }
2097}
2098
2099impl fmt::Display for DataPath {
2100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2101        for segment in &self.segments {
2102            write!(f, "{}.", segment)?;
2103        }
2104        write!(f, "{}", self.data)
2105    }
2106}
2107
2108impl fmt::Display for RulePath {
2109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2110        for segment in &self.segments {
2111            write!(f, "{}.", segment)?;
2112        }
2113        write!(f, "{}", self.rule)
2114    }
2115}
2116
2117impl fmt::Display for LemmaType {
2118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2119        write!(f, "{}", self.name())
2120    }
2121}
2122
2123impl fmt::Display for LiteralValue {
2124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2125        match &self.value {
2126            ValueKind::Scale(n, u) => {
2127                if let TypeSpecification::Scale { decimals, .. } = &self.lemma_type.specifications {
2128                    let s = match decimals {
2129                        Some(d) => {
2130                            let dp = u32::from(*d);
2131                            let rounded = n.round_dp(dp);
2132                            format!("{:.prec$}", rounded, prec = *d as usize)
2133                        }
2134                        None => n.normalize().to_string(),
2135                    };
2136                    return write!(f, "{} {}", s, u);
2137                }
2138                write!(f, "{}", self.value)
2139            }
2140            ValueKind::Ratio(r, Some(unit_name)) => {
2141                if let TypeSpecification::Ratio { units, .. } = &self.lemma_type.specifications {
2142                    if let Ok(unit) = units.get(unit_name) {
2143                        let display_value = (*r * unit.value).normalize();
2144                        let s = if display_value.fract().is_zero() {
2145                            display_value.trunc().to_string()
2146                        } else {
2147                            display_value.to_string()
2148                        };
2149                        // Use shorthand symbols for percent (%) and permille (%%)
2150                        return match unit_name.as_str() {
2151                            "percent" => write!(f, "{}%", s),
2152                            "permille" => write!(f, "{}%%", s),
2153                            _ => write!(f, "{} {}", s, unit_name),
2154                        };
2155                    }
2156                }
2157                write!(f, "{}", self.value)
2158            }
2159            _ => write!(f, "{}", self.value),
2160        }
2161    }
2162}
2163
2164// -----------------------------------------------------------------------------
2165// Tests
2166// -----------------------------------------------------------------------------
2167
2168#[cfg(test)]
2169mod tests {
2170    use super::*;
2171    use crate::parsing::ast::{BooleanValue, DateTimeValue, DurationUnit, LemmaSpec, TimeValue};
2172    use rust_decimal::Decimal;
2173    use std::str::FromStr;
2174    use std::sync::Arc;
2175
2176    #[test]
2177    fn test_negated_comparison() {
2178        assert_eq!(
2179            negated_comparison(ComparisonComputation::LessThan),
2180            ComparisonComputation::GreaterThanOrEqual
2181        );
2182        assert_eq!(
2183            negated_comparison(ComparisonComputation::GreaterThanOrEqual),
2184            ComparisonComputation::LessThan
2185        );
2186        assert_eq!(
2187            negated_comparison(ComparisonComputation::Is),
2188            ComparisonComputation::IsNot
2189        );
2190        assert_eq!(
2191            negated_comparison(ComparisonComputation::IsNot),
2192            ComparisonComputation::Is
2193        );
2194    }
2195
2196    #[test]
2197    fn test_literal_value_to_primitive_type() {
2198        let one = Decimal::from_str("1").unwrap();
2199
2200        assert_eq!(LiteralValue::text("".to_string()).lemma_type.name(), "text");
2201        assert_eq!(LiteralValue::number(one).lemma_type.name(), "number");
2202        assert_eq!(
2203            LiteralValue::from_bool(bool::from(BooleanValue::True))
2204                .lemma_type
2205                .name(),
2206            "boolean"
2207        );
2208
2209        let dt = DateTimeValue {
2210            year: 2024,
2211            month: 1,
2212            day: 1,
2213            hour: 0,
2214            minute: 0,
2215            second: 0,
2216            microsecond: 0,
2217            timezone: None,
2218        };
2219        assert_eq!(
2220            LiteralValue::date(date_time_to_semantic(&dt))
2221                .lemma_type
2222                .name(),
2223            "date"
2224        );
2225        assert_eq!(
2226            LiteralValue::ratio(one / Decimal::from(100), Some("percent".to_string()))
2227                .lemma_type
2228                .name(),
2229            "ratio"
2230        );
2231        assert_eq!(
2232            LiteralValue::duration(one, duration_unit_to_semantic(&DurationUnit::Second))
2233                .lemma_type
2234                .name(),
2235            "duration"
2236        );
2237    }
2238
2239    #[test]
2240    fn test_type_display() {
2241        let specs = TypeSpecification::text();
2242        let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
2243        assert_eq!(format!("{}", lemma_type), "name");
2244    }
2245
2246    #[test]
2247    fn test_type_serialization() {
2248        let specs = TypeSpecification::number();
2249        let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
2250        let serialized = serde_json::to_string(&lemma_type).unwrap();
2251        let deserialized: LemmaType = serde_json::from_str(&serialized).unwrap();
2252        assert_eq!(lemma_type, deserialized);
2253    }
2254
2255    #[test]
2256    fn test_literal_value_display_value() {
2257        let ten = Decimal::from_str("10").unwrap();
2258
2259        assert_eq!(
2260            LiteralValue::text("hello".to_string()).display_value(),
2261            "hello"
2262        );
2263        assert_eq!(LiteralValue::number(ten).display_value(), "10");
2264        assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
2265        assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
2266
2267        // 0.10 ratio with "percent" unit displays as 10% (unit conversion applied)
2268        let ten_percent_ratio = LiteralValue::ratio(
2269            Decimal::from_str("0.10").unwrap(),
2270            Some("percent".to_string()),
2271        );
2272        assert_eq!(ten_percent_ratio.display_value(), "10%");
2273
2274        let time = TimeValue {
2275            hour: 14,
2276            minute: 30,
2277            second: 0,
2278            timezone: None,
2279        };
2280        let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
2281        assert!(time_display.contains("14"));
2282        assert!(time_display.contains("30"));
2283    }
2284
2285    #[test]
2286    fn test_scale_display_respects_type_decimals() {
2287        let money_type = LemmaType {
2288            name: Some("money".to_string()),
2289            specifications: TypeSpecification::Scale {
2290                minimum: None,
2291                maximum: None,
2292                decimals: Some(2),
2293                precision: None,
2294                units: ScaleUnits::from(vec![ScaleUnit {
2295                    name: "eur".to_string(),
2296                    value: Decimal::from(1),
2297                }]),
2298                help: String::new(),
2299            },
2300            extends: TypeExtends::Primitive,
2301        };
2302        let val = LiteralValue::scale_with_type(
2303            Decimal::from_str("1.8").unwrap(),
2304            "eur".to_string(),
2305            money_type.clone(),
2306        );
2307        assert_eq!(val.display_value(), "1.80 eur");
2308        let more_precision = LiteralValue::scale_with_type(
2309            Decimal::from_str("1.80000").unwrap(),
2310            "eur".to_string(),
2311            money_type,
2312        );
2313        assert_eq!(more_precision.display_value(), "1.80 eur");
2314        let scale_no_decimals = LemmaType {
2315            name: Some("count".to_string()),
2316            specifications: TypeSpecification::Scale {
2317                minimum: None,
2318                maximum: None,
2319                decimals: None,
2320                precision: None,
2321                units: ScaleUnits::from(vec![ScaleUnit {
2322                    name: "items".to_string(),
2323                    value: Decimal::from(1),
2324                }]),
2325                help: String::new(),
2326            },
2327            extends: TypeExtends::Primitive,
2328        };
2329        let val_any = LiteralValue::scale_with_type(
2330            Decimal::from_str("42.50").unwrap(),
2331            "items".to_string(),
2332            scale_no_decimals,
2333        );
2334        assert_eq!(val_any.display_value(), "42.5 items");
2335    }
2336
2337    #[test]
2338    fn test_literal_value_time_type() {
2339        let time = TimeValue {
2340            hour: 14,
2341            minute: 30,
2342            second: 0,
2343            timezone: None,
2344        };
2345        let lit = LiteralValue::time(time_to_semantic(&time));
2346        assert_eq!(lit.lemma_type.name(), "time");
2347    }
2348
2349    #[test]
2350    fn test_scale_family_name_primitive_root() {
2351        let scale_spec = TypeSpecification::scale();
2352        let money_primitive = LemmaType::new(
2353            "money".to_string(),
2354            scale_spec.clone(),
2355            TypeExtends::Primitive,
2356        );
2357        assert_eq!(money_primitive.scale_family_name(), Some("money"));
2358    }
2359
2360    #[test]
2361    fn test_scale_family_name_custom() {
2362        let scale_spec = TypeSpecification::scale();
2363        let money_custom = LemmaType::new(
2364            "money".to_string(),
2365            scale_spec,
2366            TypeExtends::custom_local("money".to_string(), "money".to_string()),
2367        );
2368        assert_eq!(money_custom.scale_family_name(), Some("money"));
2369    }
2370
2371    #[test]
2372    fn test_same_scale_family_same_name_different_extends() {
2373        let scale_spec = TypeSpecification::scale();
2374        let money_primitive = LemmaType::new(
2375            "money".to_string(),
2376            scale_spec.clone(),
2377            TypeExtends::Primitive,
2378        );
2379        let money_custom = LemmaType::new(
2380            "money".to_string(),
2381            scale_spec,
2382            TypeExtends::custom_local("money".to_string(), "money".to_string()),
2383        );
2384        assert!(money_primitive.same_scale_family(&money_custom));
2385        assert!(money_custom.same_scale_family(&money_primitive));
2386    }
2387
2388    #[test]
2389    fn test_same_scale_family_parent_and_child() {
2390        let scale_spec = TypeSpecification::scale();
2391        let type_x = LemmaType::new("x".to_string(), scale_spec.clone(), TypeExtends::Primitive);
2392        let type_x2 = LemmaType::new(
2393            "x2".to_string(),
2394            scale_spec,
2395            TypeExtends::custom_local("x".to_string(), "x".to_string()),
2396        );
2397        assert_eq!(type_x.scale_family_name(), Some("x"));
2398        assert_eq!(type_x2.scale_family_name(), Some("x"));
2399        assert!(type_x.same_scale_family(&type_x2));
2400        assert!(type_x2.same_scale_family(&type_x));
2401    }
2402
2403    #[test]
2404    fn test_same_scale_family_siblings() {
2405        let scale_spec = TypeSpecification::scale();
2406        let type_x2_a = LemmaType::new(
2407            "x2a".to_string(),
2408            scale_spec.clone(),
2409            TypeExtends::custom_local("x".to_string(), "x".to_string()),
2410        );
2411        let type_x2_b = LemmaType::new(
2412            "x2b".to_string(),
2413            scale_spec,
2414            TypeExtends::custom_local("x".to_string(), "x".to_string()),
2415        );
2416        assert!(type_x2_a.same_scale_family(&type_x2_b));
2417    }
2418
2419    #[test]
2420    fn test_same_scale_family_different_families() {
2421        let scale_spec = TypeSpecification::scale();
2422        let money = LemmaType::new(
2423            "money".to_string(),
2424            scale_spec.clone(),
2425            TypeExtends::Primitive,
2426        );
2427        let temperature = LemmaType::new(
2428            "temperature".to_string(),
2429            scale_spec,
2430            TypeExtends::Primitive,
2431        );
2432        assert!(!money.same_scale_family(&temperature));
2433        assert!(!temperature.same_scale_family(&money));
2434    }
2435
2436    #[test]
2437    fn test_same_scale_family_scale_vs_non_scale() {
2438        let scale_spec = TypeSpecification::scale();
2439        let number_spec = TypeSpecification::number();
2440        let scale_type = LemmaType::new("money".to_string(), scale_spec, TypeExtends::Primitive);
2441        let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
2442        assert!(!scale_type.same_scale_family(&number_type));
2443        assert!(!number_type.same_scale_family(&scale_type));
2444    }
2445
2446    #[test]
2447    fn test_scale_family_name_non_scale_returns_none() {
2448        let number_spec = TypeSpecification::number();
2449        let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
2450        assert_eq!(number_type.scale_family_name(), None);
2451    }
2452
2453    #[test]
2454    fn test_lemma_type_inequality_local_vs_import_same_shape() {
2455        let dep = Arc::new(LemmaSpec::new("dep".to_string()));
2456        let scale_spec = TypeSpecification::scale();
2457        let local = LemmaType::new(
2458            "t".to_string(),
2459            scale_spec.clone(),
2460            TypeExtends::custom_local("money".to_string(), "money".to_string()),
2461        );
2462        let imported = LemmaType::new(
2463            "t".to_string(),
2464            scale_spec,
2465            TypeExtends::Custom {
2466                parent: "money".to_string(),
2467                family: "money".to_string(),
2468                defining_spec: TypeDefiningSpec::Import {
2469                    spec: Arc::clone(&dep),
2470                },
2471            },
2472        );
2473        assert_ne!(local, imported);
2474    }
2475
2476    #[test]
2477    fn test_lemma_type_equality_import_same_resolved_spec_semantics() {
2478        let spec_a = Arc::new(LemmaSpec::new("dep".to_string()));
2479        let spec_b = Arc::new(LemmaSpec::new("dep".to_string()));
2480        assert!(is_same_spec(spec_a.as_ref(), spec_b.as_ref()));
2481        let scale_spec = TypeSpecification::scale();
2482        let left = LemmaType::new(
2483            "t".to_string(),
2484            scale_spec.clone(),
2485            TypeExtends::Custom {
2486                parent: "money".to_string(),
2487                family: "money".to_string(),
2488                defining_spec: TypeDefiningSpec::Import {
2489                    spec: Arc::clone(&spec_a),
2490                },
2491            },
2492        );
2493        let right = LemmaType::new(
2494            "t".to_string(),
2495            scale_spec,
2496            TypeExtends::Custom {
2497                parent: "money".to_string(),
2498                family: "money".to_string(),
2499                defining_spec: TypeDefiningSpec::Import { spec: spec_b },
2500            },
2501        );
2502        assert_eq!(left, right);
2503    }
2504}