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