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