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::Equal | ComparisonComputation::Is => ComparisonComputation::IsNot,
32        ComparisonComputation::NotEqual | 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    /// Get source text for this expression if available
1271    pub fn get_source_text(&self, sources: &HashMap<String, String>) -> Option<String> {
1272        let source = self.source_location.as_ref()?;
1273        let file_source = sources.get(&source.attribute)?;
1274        let span = &source.span;
1275        Some(file_source.get(span.start..span.end)?.to_string())
1276    }
1277
1278    /// Collect all FactPath references from this resolved expression tree
1279    pub fn collect_fact_paths(&self, facts: &mut std::collections::HashSet<FactPath>) {
1280        self.kind.collect_fact_paths(facts);
1281    }
1282}
1283
1284/// Resolved expression kind (only resolved variants, no unresolved references)
1285#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1286#[serde(rename_all = "snake_case")]
1287pub enum ExpressionKind {
1288    /// Resolved literal with type (boxed to keep enum small)
1289    Literal(Box<LiteralValue>),
1290    /// Resolved fact path
1291    FactPath(FactPath),
1292    /// Resolved rule path
1293    RulePath(RulePath),
1294    LogicalAnd(Arc<Expression>, Arc<Expression>),
1295    Arithmetic(Arc<Expression>, ArithmeticComputation, Arc<Expression>),
1296    Comparison(Arc<Expression>, ComparisonComputation, Arc<Expression>),
1297    UnitConversion(Arc<Expression>, SemanticConversionTarget),
1298    LogicalNegation(Arc<Expression>, NegationType),
1299    MathematicalComputation(MathematicalComputation, Arc<Expression>),
1300    Veto(VetoExpression),
1301    /// The `now` keyword — resolved at evaluation to the effective datetime.
1302    Now,
1303    /// Date-relative sugar: `<date_expr> in past [<duration_expr>]` / `in future [...]`
1304    DateRelative(DateRelativeKind, Arc<Expression>, Option<Arc<Expression>>),
1305    /// Calendar-period sugar: `<date_expr> in [past|future] calendar year|month|week`
1306    DateCalendar(DateCalendarKind, CalendarUnit, Arc<Expression>),
1307}
1308
1309impl ExpressionKind {
1310    /// Collect all FactPath references from this expression kind
1311    fn collect_fact_paths(&self, facts: &mut std::collections::HashSet<FactPath>) {
1312        match self {
1313            ExpressionKind::FactPath(fp) => {
1314                facts.insert(fp.clone());
1315            }
1316            ExpressionKind::LogicalAnd(left, right)
1317            | ExpressionKind::Arithmetic(left, _, right)
1318            | ExpressionKind::Comparison(left, _, right) => {
1319                left.collect_fact_paths(facts);
1320                right.collect_fact_paths(facts);
1321            }
1322            ExpressionKind::UnitConversion(inner, _)
1323            | ExpressionKind::LogicalNegation(inner, _)
1324            | ExpressionKind::MathematicalComputation(_, inner) => {
1325                inner.collect_fact_paths(facts);
1326            }
1327            ExpressionKind::DateRelative(_, date_expr, tolerance) => {
1328                date_expr.collect_fact_paths(facts);
1329                if let Some(tol) = tolerance {
1330                    tol.collect_fact_paths(facts);
1331                }
1332            }
1333            ExpressionKind::DateCalendar(_, _, date_expr) => {
1334                date_expr.collect_fact_paths(facts);
1335            }
1336            ExpressionKind::Literal(_)
1337            | ExpressionKind::RulePath(_)
1338            | ExpressionKind::Veto(_)
1339            | ExpressionKind::Now => {}
1340        }
1341    }
1342}
1343
1344// -----------------------------------------------------------------------------
1345// Resolved types and values
1346// -----------------------------------------------------------------------------
1347
1348/// Whether two resolved specs are the same temporal slice (same `name` and `effective_from` as [`LemmaSpec`]'s `PartialEq`).
1349/// Not `Arc` pointer identity: [`Arc`] equality uses the inner value.
1350#[inline]
1351#[must_use]
1352pub fn is_same_spec(left: &LemmaSpec, right: &LemmaSpec) -> bool {
1353    left == right
1354}
1355
1356/// Where the custom extension chain is rooted: same spec as this type, or imported from another resolved spec.
1357#[derive(Clone, Debug, Serialize, Deserialize)]
1358#[serde(tag = "kind", rename_all = "snake_case")]
1359pub enum TypeDefiningSpec {
1360    /// Parent type is defined in the same spec as this type.
1361    Local,
1362    /// Parent type was resolved from types loaded from this dependency (same concrete version as spec-reference facts).
1363    Import {
1364        spec: Arc<LemmaSpec>,
1365        resolved_plan_hash: String,
1366    },
1367}
1368
1369/// What this type extends (primitive built-in or custom type by name).
1370#[derive(Clone, Debug, Serialize, Deserialize)]
1371#[serde(rename_all = "snake_case")]
1372pub enum TypeExtends {
1373    /// Extends a primitive built-in type (number, boolean, text, etc.)
1374    Primitive,
1375    /// Extends a custom type: parent is the immediate parent type name; family is the root of the extension chain (topmost custom type name).
1376    /// `defining_spec` records whether the parent chain is local or imported from another spec; see [`TypeDefiningSpec`].
1377    Custom {
1378        parent: String,
1379        family: String,
1380        defining_spec: TypeDefiningSpec,
1381    },
1382}
1383
1384impl PartialEq for TypeExtends {
1385    fn eq(&self, other: &Self) -> bool {
1386        match (self, other) {
1387            (TypeExtends::Primitive, TypeExtends::Primitive) => true,
1388            (
1389                TypeExtends::Custom {
1390                    parent: lp,
1391                    family: lf,
1392                    defining_spec: ld,
1393                },
1394                TypeExtends::Custom {
1395                    parent: rp,
1396                    family: rf,
1397                    defining_spec: rd,
1398                },
1399            ) => {
1400                lp == rp
1401                    && lf == rf
1402                    && match (ld, rd) {
1403                        (TypeDefiningSpec::Local, TypeDefiningSpec::Local) => true,
1404                        (
1405                            TypeDefiningSpec::Import {
1406                                spec: left,
1407                                resolved_plan_hash: lh,
1408                            },
1409                            TypeDefiningSpec::Import {
1410                                spec: right,
1411                                resolved_plan_hash: rh,
1412                            },
1413                        ) => is_same_spec(left, right) && lh == rh,
1414                        _ => false,
1415                    }
1416            }
1417            _ => false,
1418        }
1419    }
1420}
1421
1422impl Eq for TypeExtends {}
1423
1424impl TypeExtends {
1425    /// Custom extension in the same spec as the defining type (no cross-spec import for the parent chain).
1426    #[must_use]
1427    pub fn custom_local(parent: String, family: String) -> Self {
1428        TypeExtends::Custom {
1429            parent,
1430            family,
1431            defining_spec: TypeDefiningSpec::Local,
1432        }
1433    }
1434
1435    /// Returns the parent type name if this type extends a custom type.
1436    #[must_use]
1437    pub fn parent_name(&self) -> Option<&str> {
1438        match self {
1439            TypeExtends::Primitive => None,
1440            TypeExtends::Custom { parent, .. } => Some(parent.as_str()),
1441        }
1442    }
1443}
1444
1445/// Resolved type after planning
1446///
1447/// Contains a type specification and optional name. Created during planning
1448/// from TypeSpecification and TypeDef in the AST.
1449#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1450pub struct LemmaType {
1451    /// Optional type name (e.g., "age", "temperature")
1452    pub name: Option<String>,
1453    /// The type specification (Boolean, Number, Scale, etc.)
1454    pub specifications: TypeSpecification,
1455    /// What this type extends (primitive or custom from a spec)
1456    pub extends: TypeExtends,
1457}
1458
1459impl LemmaType {
1460    /// Create a new type with a name
1461    pub fn new(name: String, specifications: TypeSpecification, extends: TypeExtends) -> Self {
1462        Self {
1463            name: Some(name),
1464            specifications,
1465            extends,
1466        }
1467    }
1468
1469    /// Create a type without a name (anonymous/inline type)
1470    pub fn without_name(specifications: TypeSpecification, extends: TypeExtends) -> Self {
1471        Self {
1472            name: None,
1473            specifications,
1474            extends,
1475        }
1476    }
1477
1478    /// Create a primitive type (no name, extends Primitive)
1479    pub fn primitive(specifications: TypeSpecification) -> Self {
1480        Self {
1481            name: None,
1482            specifications,
1483            extends: TypeExtends::Primitive,
1484        }
1485    }
1486
1487    /// Get the type name, or a default based on the type specification
1488    pub fn name(&self) -> String {
1489        self.name.clone().unwrap_or_else(|| {
1490            match &self.specifications {
1491                TypeSpecification::Boolean { .. } => "boolean",
1492                TypeSpecification::Scale { .. } => "scale",
1493                TypeSpecification::Number { .. } => "number",
1494                TypeSpecification::Text { .. } => "text",
1495                TypeSpecification::Date { .. } => "date",
1496                TypeSpecification::Time { .. } => "time",
1497                TypeSpecification::Duration { .. } => "duration",
1498                TypeSpecification::Ratio { .. } => "ratio",
1499                TypeSpecification::Veto { .. } => "veto",
1500                TypeSpecification::Undetermined => "undetermined",
1501            }
1502            .to_string()
1503        })
1504    }
1505
1506    /// Check if this type is boolean
1507    pub fn is_boolean(&self) -> bool {
1508        matches!(&self.specifications, TypeSpecification::Boolean { .. })
1509    }
1510
1511    /// Check if this type is scale
1512    pub fn is_scale(&self) -> bool {
1513        matches!(&self.specifications, TypeSpecification::Scale { .. })
1514    }
1515
1516    /// Check if this type is number (dimensionless)
1517    pub fn is_number(&self) -> bool {
1518        matches!(&self.specifications, TypeSpecification::Number { .. })
1519    }
1520
1521    /// Check if this type is numeric (either scale or number)
1522    pub fn is_numeric(&self) -> bool {
1523        matches!(
1524            &self.specifications,
1525            TypeSpecification::Scale { .. } | TypeSpecification::Number { .. }
1526        )
1527    }
1528
1529    /// Check if this type is text
1530    pub fn is_text(&self) -> bool {
1531        matches!(&self.specifications, TypeSpecification::Text { .. })
1532    }
1533
1534    /// Check if this type is date
1535    pub fn is_date(&self) -> bool {
1536        matches!(&self.specifications, TypeSpecification::Date { .. })
1537    }
1538
1539    /// Check if this type is time
1540    pub fn is_time(&self) -> bool {
1541        matches!(&self.specifications, TypeSpecification::Time { .. })
1542    }
1543
1544    /// Check if this type is duration
1545    pub fn is_duration(&self) -> bool {
1546        matches!(&self.specifications, TypeSpecification::Duration { .. })
1547    }
1548
1549    /// Check if this type is ratio
1550    pub fn is_ratio(&self) -> bool {
1551        matches!(&self.specifications, TypeSpecification::Ratio { .. })
1552    }
1553
1554    /// Check if this type is veto
1555    pub fn vetoed(&self) -> bool {
1556        matches!(&self.specifications, TypeSpecification::Veto { .. })
1557    }
1558
1559    /// True if this type is the undetermined sentinel (type could not be inferred).
1560    pub fn is_undetermined(&self) -> bool {
1561        matches!(&self.specifications, TypeSpecification::Undetermined)
1562    }
1563
1564    /// Check if two types have the same base type specification (ignoring constraints)
1565    pub fn has_same_base_type(&self, other: &LemmaType) -> bool {
1566        use TypeSpecification::*;
1567        matches!(
1568            (&self.specifications, &other.specifications),
1569            (Boolean { .. }, Boolean { .. })
1570                | (Number { .. }, Number { .. })
1571                | (Scale { .. }, Scale { .. })
1572                | (Text { .. }, Text { .. })
1573                | (Date { .. }, Date { .. })
1574                | (Time { .. }, Time { .. })
1575                | (Duration { .. }, Duration { .. })
1576                | (Ratio { .. }, Ratio { .. })
1577                | (Veto { .. }, Veto { .. })
1578                | (Undetermined, Undetermined)
1579        )
1580    }
1581
1582    /// 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.
1583    #[must_use]
1584    pub fn scale_family_name(&self) -> Option<&str> {
1585        if !self.is_scale() {
1586            return None;
1587        }
1588        match &self.extends {
1589            TypeExtends::Custom { family, .. } => Some(family.as_str()),
1590            TypeExtends::Primitive => self.name.as_deref(),
1591        }
1592    }
1593
1594    /// Returns true if both types are scale and belong to the same scale family (same family name).
1595    /// Two anonymous primitive scales (no name, no family) are considered compatible.
1596    #[must_use]
1597    pub fn same_scale_family(&self, other: &LemmaType) -> bool {
1598        if !self.is_scale() || !other.is_scale() {
1599            return false;
1600        }
1601        match (self.scale_family_name(), other.scale_family_name()) {
1602            (Some(self_family), Some(other_family)) => self_family == other_family,
1603            // Two anonymous primitive scales are compatible with each other.
1604            (None, None) => true,
1605            _ => false,
1606        }
1607    }
1608
1609    /// Create a default value from this type's default constraint (if any)
1610    pub fn create_default_value(&self) -> Option<LiteralValue> {
1611        let value = match &self.specifications {
1612            TypeSpecification::Text { default, .. } => default.clone().map(ValueKind::Text),
1613            TypeSpecification::Number { default, .. } => (*default).map(ValueKind::Number),
1614            TypeSpecification::Scale { default, .. } => {
1615                default.clone().map(|(d, u)| ValueKind::Scale(d, u))
1616            }
1617            TypeSpecification::Boolean { default, .. } => (*default).map(ValueKind::Boolean),
1618            TypeSpecification::Date { default, .. } => default
1619                .clone()
1620                .map(|dt| ValueKind::Date(date_time_to_semantic(&dt))),
1621            TypeSpecification::Time { default, .. } => default
1622                .clone()
1623                .map(|t| ValueKind::Time(time_to_semantic(&t))),
1624            TypeSpecification::Duration { default, .. } => default
1625                .clone()
1626                .map(|(v, u)| ValueKind::Duration(v, duration_unit_to_semantic(&u))),
1627            TypeSpecification::Ratio { .. } => None, // Ratio default requires (value, unit); type spec has only Decimal
1628            TypeSpecification::Veto { .. } => None,
1629            TypeSpecification::Undetermined => None,
1630        };
1631
1632        value.map(|v| LiteralValue {
1633            value: v,
1634            lemma_type: self.clone(),
1635        })
1636    }
1637
1638    /// Create a Veto LemmaType
1639    pub fn veto_type() -> Self {
1640        Self::primitive(TypeSpecification::veto())
1641    }
1642
1643    /// LemmaType sentinel for undetermined type (used during inference when a type cannot be determined).
1644    /// Propagates through expressions and is never present in a validated graph.
1645    pub fn undetermined_type() -> Self {
1646        Self::primitive(TypeSpecification::Undetermined)
1647    }
1648
1649    /// Decimal places for display (Number, Scale, and Ratio). Used by formatters.
1650    /// Ratio: optional, no default; when None display is normalized (no trailing zeros).
1651    pub fn decimal_places(&self) -> Option<u8> {
1652        match &self.specifications {
1653            TypeSpecification::Number { decimals, .. } => *decimals,
1654            TypeSpecification::Scale { decimals, .. } => *decimals,
1655            TypeSpecification::Ratio { decimals, .. } => *decimals,
1656            _ => None,
1657        }
1658    }
1659
1660    /// Get an example value string for this type, suitable for UI help text
1661    pub fn example_value(&self) -> &'static str {
1662        match &self.specifications {
1663            TypeSpecification::Text { .. } => "\"hello world\"",
1664            TypeSpecification::Scale { .. } => "12.50 eur",
1665            TypeSpecification::Number { .. } => "3.14",
1666            TypeSpecification::Boolean { .. } => "true",
1667            TypeSpecification::Date { .. } => "2023-12-25T14:30:00Z",
1668            TypeSpecification::Veto { .. } => "veto",
1669            TypeSpecification::Time { .. } => "14:30:00",
1670            TypeSpecification::Duration { .. } => "90 minutes",
1671            TypeSpecification::Ratio { .. } => "50%",
1672            TypeSpecification::Undetermined => unreachable!(
1673                "BUG: example_value called on Undetermined sentinel type; this type must never reach user-facing code"
1674            ),
1675        }
1676    }
1677
1678    /// Factor for a unit of this scale type (for unit conversion during evaluation only).
1679    /// Planning must validate conversions first and return Error for invalid units.
1680    /// If called with a non-scale type or unknown unit name, panics (invariant violation).
1681    #[must_use]
1682    pub fn scale_unit_factor(&self, unit_name: &str) -> Decimal {
1683        let units = match &self.specifications {
1684            TypeSpecification::Scale { units, .. } => units,
1685            _ => unreachable!(
1686                "BUG: scale_unit_factor called with non-scale type {}; only call during evaluation after planning validated scale conversion",
1687                self.name()
1688            ),
1689        };
1690        match units
1691            .iter()
1692            .find(|u| u.name.eq_ignore_ascii_case(unit_name))
1693        {
1694            Some(ScaleUnit { value, .. }) => *value,
1695            None => {
1696                let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
1697                unreachable!(
1698                    "BUG: unknown unit '{}' for scale type {} (valid: {}); planning must reject invalid conversions with Error",
1699                    unit_name,
1700                    self.name(),
1701                    valid.join(", ")
1702                );
1703            }
1704        }
1705    }
1706}
1707
1708/// Literal value with type. The single value type in semantics.
1709#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
1710pub struct LiteralValue {
1711    pub value: ValueKind,
1712    pub lemma_type: LemmaType,
1713}
1714
1715impl Serialize for LiteralValue {
1716    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1717    where
1718        S: serde::Serializer,
1719    {
1720        use serde::ser::SerializeStruct;
1721        let mut state = serializer.serialize_struct("LiteralValue", 3)?;
1722        state.serialize_field("value", &self.value)?;
1723        state.serialize_field("lemma_type", &self.lemma_type)?;
1724        state.serialize_field("display_value", &self.display_value())?;
1725        state.end()
1726    }
1727}
1728
1729impl LiteralValue {
1730    pub fn text(s: String) -> Self {
1731        Self {
1732            value: ValueKind::Text(s),
1733            lemma_type: primitive_text().clone(),
1734        }
1735    }
1736
1737    pub fn text_with_type(s: String, lemma_type: LemmaType) -> Self {
1738        Self {
1739            value: ValueKind::Text(s),
1740            lemma_type,
1741        }
1742    }
1743
1744    pub fn number(n: Decimal) -> Self {
1745        Self {
1746            value: ValueKind::Number(n),
1747            lemma_type: primitive_number().clone(),
1748        }
1749    }
1750
1751    pub fn number_with_type(n: Decimal, lemma_type: LemmaType) -> Self {
1752        Self {
1753            value: ValueKind::Number(n),
1754            lemma_type,
1755        }
1756    }
1757
1758    pub fn scale_with_type(n: Decimal, unit: String, lemma_type: LemmaType) -> Self {
1759        Self {
1760            value: ValueKind::Scale(n, unit),
1761            lemma_type,
1762        }
1763    }
1764
1765    /// Number interpreted as a scale value in the given unit (e.g. "3 in usd" where 3 is a number).
1766    /// Creates an anonymous one-unit scale type so computation does not depend on parsing types.
1767    pub fn number_interpreted_as_scale(value: Decimal, unit_name: String) -> Self {
1768        let lemma_type = LemmaType {
1769            name: None,
1770            specifications: TypeSpecification::Scale {
1771                minimum: None,
1772                maximum: None,
1773                decimals: None,
1774                precision: None,
1775                units: ScaleUnits::from(vec![ScaleUnit {
1776                    name: unit_name.clone(),
1777                    value: Decimal::from(1),
1778                }]),
1779                help: "Format: {value} {unit} (e.g. 100 kilograms)".to_string(),
1780                default: None,
1781            },
1782            extends: TypeExtends::Primitive,
1783        };
1784        Self {
1785            value: ValueKind::Scale(value, unit_name),
1786            lemma_type,
1787        }
1788    }
1789
1790    pub fn from_bool(b: bool) -> Self {
1791        Self {
1792            value: ValueKind::Boolean(b),
1793            lemma_type: primitive_boolean().clone(),
1794        }
1795    }
1796
1797    pub fn date(dt: SemanticDateTime) -> Self {
1798        Self {
1799            value: ValueKind::Date(dt),
1800            lemma_type: primitive_date().clone(),
1801        }
1802    }
1803
1804    pub fn date_with_type(dt: SemanticDateTime, lemma_type: LemmaType) -> Self {
1805        Self {
1806            value: ValueKind::Date(dt),
1807            lemma_type,
1808        }
1809    }
1810
1811    pub fn time(t: SemanticTime) -> Self {
1812        Self {
1813            value: ValueKind::Time(t),
1814            lemma_type: primitive_time().clone(),
1815        }
1816    }
1817
1818    pub fn time_with_type(t: SemanticTime, lemma_type: LemmaType) -> Self {
1819        Self {
1820            value: ValueKind::Time(t),
1821            lemma_type,
1822        }
1823    }
1824
1825    pub fn duration(value: Decimal, unit: SemanticDurationUnit) -> Self {
1826        Self {
1827            value: ValueKind::Duration(value, unit),
1828            lemma_type: primitive_duration().clone(),
1829        }
1830    }
1831
1832    pub fn duration_with_type(
1833        value: Decimal,
1834        unit: SemanticDurationUnit,
1835        lemma_type: LemmaType,
1836    ) -> Self {
1837        Self {
1838            value: ValueKind::Duration(value, unit),
1839            lemma_type,
1840        }
1841    }
1842
1843    pub fn ratio(r: Decimal, unit: Option<String>) -> Self {
1844        Self {
1845            value: ValueKind::Ratio(r, unit),
1846            lemma_type: primitive_ratio().clone(),
1847        }
1848    }
1849
1850    pub fn ratio_with_type(r: Decimal, unit: Option<String>, lemma_type: LemmaType) -> Self {
1851        Self {
1852            value: ValueKind::Ratio(r, unit),
1853            lemma_type,
1854        }
1855    }
1856
1857    /// Get a display string for this value (for UI/output)
1858    pub fn display_value(&self) -> String {
1859        format!("{}", self)
1860    }
1861
1862    /// Approximate byte size for resource limit checks (string representation length)
1863    pub fn byte_size(&self) -> usize {
1864        format!("{}", self).len()
1865    }
1866
1867    /// Get the resolved type of this literal
1868    pub fn get_type(&self) -> &LemmaType {
1869        &self.lemma_type
1870    }
1871}
1872
1873/// Fact value: literal, type declaration (resolved type only), or spec reference.
1874#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1875#[serde(rename_all = "snake_case")]
1876pub enum FactValue {
1877    Literal(LiteralValue),
1878    TypeDeclaration { resolved_type: LemmaType },
1879    SpecReference(String),
1880}
1881
1882/// Fact: path, value, and source location.
1883#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1884pub struct Fact {
1885    pub path: FactPath,
1886    pub value: FactValue,
1887    pub source: Option<Source>,
1888}
1889
1890/// Resolved fact value for the execution plan: aligned with [`FactValue`] but with source per variant.
1891#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1892#[serde(rename_all = "snake_case")]
1893pub enum FactData {
1894    /// Value-holding fact: current value (literal or default); type is on the value.
1895    /// When `is_default` is true, the value came from a type `-> default` constraint
1896    /// rather than an explicit literal in the spec.
1897    Value {
1898        value: LiteralValue,
1899        source: Source,
1900        is_default: bool,
1901    },
1902    /// Type-only fact: schema known, value to be supplied (e.g. via with_values).
1903    TypeDeclaration {
1904        resolved_type: LemmaType,
1905        source: Source,
1906    },
1907    /// Spec reference fact: holds the resolved spec and the dependency's plan hash for this slice.
1908    SpecRef {
1909        spec: Arc<crate::parsing::ast::LemmaSpec>,
1910        source: Source,
1911        resolved_plan_hash: Option<String>,
1912    },
1913}
1914
1915impl FactData {
1916    /// Returns the schema type for value and type-declaration facts; `None` for spec references.
1917    pub fn schema_type(&self) -> Option<&LemmaType> {
1918        match self {
1919            FactData::Value { value, .. } => Some(&value.lemma_type),
1920            FactData::TypeDeclaration { resolved_type, .. } => Some(resolved_type),
1921            FactData::SpecRef { .. } => None,
1922        }
1923    }
1924
1925    /// Returns the literal value for value facts; `None` for type-declaration and spec references.
1926    pub fn value(&self) -> Option<&LiteralValue> {
1927        match self {
1928            FactData::Value { value, .. } => Some(value),
1929            FactData::TypeDeclaration { .. } | FactData::SpecRef { .. } => None,
1930        }
1931    }
1932
1933    /// Returns the literal value only if it was explicitly defined in the spec
1934    /// (not from a type `-> default` constraint). Used by schema methods to decide
1935    /// which facts need user input.
1936    pub fn explicit_value(&self) -> Option<&LiteralValue> {
1937        match self {
1938            FactData::Value {
1939                value, is_default, ..
1940            } => {
1941                if *is_default {
1942                    None
1943                } else {
1944                    Some(value)
1945                }
1946            }
1947            FactData::TypeDeclaration { .. } | FactData::SpecRef { .. } => None,
1948        }
1949    }
1950
1951    /// Returns the source location for this fact.
1952    pub fn source(&self) -> &Source {
1953        match self {
1954            FactData::Value { source, .. } => source,
1955            FactData::TypeDeclaration { source, .. } => source,
1956            FactData::SpecRef { source, .. } => source,
1957        }
1958    }
1959
1960    /// Returns the resolved dependency plan hash for spec reference facts; `None` for other fact kinds.
1961    pub fn resolved_plan_hash(&self) -> Option<&str> {
1962        match self {
1963            FactData::Value { .. } | FactData::TypeDeclaration { .. } => None,
1964            FactData::SpecRef {
1965                resolved_plan_hash, ..
1966            } => resolved_plan_hash.as_deref(),
1967        }
1968    }
1969
1970    /// Returns the referenced spec Arc for spec reference facts; `None` otherwise.
1971    pub fn spec_arc(&self) -> Option<&Arc<crate::parsing::ast::LemmaSpec>> {
1972        match self {
1973            FactData::Value { .. } | FactData::TypeDeclaration { .. } => None,
1974            FactData::SpecRef { spec: spec_arc, .. } => Some(spec_arc),
1975        }
1976    }
1977
1978    /// Returns the referenced spec name for spec reference facts; `None` otherwise.
1979    pub fn spec_ref(&self) -> Option<&str> {
1980        match self {
1981            FactData::Value { .. } | FactData::TypeDeclaration { .. } => None,
1982            FactData::SpecRef { spec, .. } => Some(&spec.name),
1983        }
1984    }
1985}
1986
1987/// Convert parser Value to ValueKind. Fails if Scale/Ratio have no unit (strict).
1988pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
1989    use crate::parsing::ast::Value;
1990    Ok(match value {
1991        Value::Number(n) => ValueKind::Number(*n),
1992        Value::Text(s) => ValueKind::Text(s.clone()),
1993        Value::Boolean(b) => ValueKind::Boolean(bool::from(*b)),
1994        Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
1995        Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
1996        Value::Duration(n, u) => ValueKind::Duration(*n, duration_unit_to_semantic(u)),
1997        Value::Scale(n, unit) => ValueKind::Scale(*n, unit.clone()),
1998        Value::Ratio(n, unit) => ValueKind::Ratio(*n, unit.clone()),
1999    })
2000}
2001
2002/// Convert AST date-time to semantic (for tests and planning).
2003pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
2004    SemanticDateTime {
2005        year: dt.year,
2006        month: dt.month,
2007        day: dt.day,
2008        hour: dt.hour,
2009        minute: dt.minute,
2010        second: dt.second,
2011        microsecond: dt.microsecond,
2012        timezone: dt.timezone.as_ref().map(|tz| SemanticTimezone {
2013            offset_hours: tz.offset_hours,
2014            offset_minutes: tz.offset_minutes,
2015        }),
2016    }
2017}
2018
2019/// Convert AST time to semantic (for tests and planning).
2020pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
2021    SemanticTime {
2022        hour: t.hour.into(),
2023        minute: t.minute.into(),
2024        second: t.second.into(),
2025        timezone: t.timezone.as_ref().map(|tz| SemanticTimezone {
2026            offset_hours: tz.offset_hours,
2027            offset_minutes: tz.offset_minutes,
2028        }),
2029    }
2030}
2031
2032/// Convert AST duration unit to semantic (for tests and planning).
2033pub(crate) fn duration_unit_to_semantic(
2034    u: &crate::parsing::ast::DurationUnit,
2035) -> SemanticDurationUnit {
2036    use crate::parsing::ast::DurationUnit as DU;
2037    match u {
2038        DU::Year => SemanticDurationUnit::Year,
2039        DU::Month => SemanticDurationUnit::Month,
2040        DU::Week => SemanticDurationUnit::Week,
2041        DU::Day => SemanticDurationUnit::Day,
2042        DU::Hour => SemanticDurationUnit::Hour,
2043        DU::Minute => SemanticDurationUnit::Minute,
2044        DU::Second => SemanticDurationUnit::Second,
2045        DU::Millisecond => SemanticDurationUnit::Millisecond,
2046        DU::Microsecond => SemanticDurationUnit::Microsecond,
2047    }
2048}
2049
2050/// Convert AST conversion target to semantic (planning boundary; evaluation/computation use only semantic).
2051///
2052/// The AST uses `ConversionTarget::Unit(name)` for non-duration units; this function looks up `name`
2053/// in the spec's unit index and returns `RatioUnit` or `ScaleUnit` based on the type that defines
2054/// the unit. The unit must be defined by a scale or ratio type in the spec (e.g. primitive ratio for
2055/// "percent", "permille").
2056pub fn conversion_target_to_semantic(
2057    ct: &ConversionTarget,
2058    unit_index: Option<&HashMap<String, (LemmaType, Option<crate::parsing::ast::TypeDef>)>>,
2059) -> Result<SemanticConversionTarget, String> {
2060    match ct {
2061        ConversionTarget::Duration(u) => Ok(SemanticConversionTarget::Duration(
2062            duration_unit_to_semantic(u),
2063        )),
2064        ConversionTarget::Unit(name) => {
2065            let index = unit_index.ok_or_else(|| {
2066                "Unit conversion requires type resolution; unit index not available.".to_string()
2067            })?;
2068            let (lemma_type, _) = index.get(name).ok_or_else(|| {
2069                format!(
2070                    "Unknown unit '{}'. Unit must be defined by a scale or ratio type.",
2071                    name
2072                )
2073            })?;
2074            if lemma_type.is_ratio() {
2075                Ok(SemanticConversionTarget::RatioUnit(name.clone()))
2076            } else if lemma_type.is_scale() {
2077                Ok(SemanticConversionTarget::ScaleUnit(name.clone()))
2078            } else {
2079                Err(format!(
2080                    "Unit '{}' is not a ratio or scale type; cannot use it in conversion.",
2081                    name
2082                ))
2083            }
2084        }
2085    }
2086}
2087
2088// -----------------------------------------------------------------------------
2089// Primitive type constructors (moved from parsing::ast)
2090// -----------------------------------------------------------------------------
2091
2092// Private statics for lazy initialization
2093static PRIMITIVE_BOOLEAN: OnceLock<LemmaType> = OnceLock::new();
2094static PRIMITIVE_SCALE: OnceLock<LemmaType> = OnceLock::new();
2095static PRIMITIVE_NUMBER: OnceLock<LemmaType> = OnceLock::new();
2096static PRIMITIVE_TEXT: OnceLock<LemmaType> = OnceLock::new();
2097static PRIMITIVE_DATE: OnceLock<LemmaType> = OnceLock::new();
2098static PRIMITIVE_TIME: OnceLock<LemmaType> = OnceLock::new();
2099static PRIMITIVE_DURATION: OnceLock<LemmaType> = OnceLock::new();
2100static PRIMITIVE_RATIO: OnceLock<LemmaType> = OnceLock::new();
2101
2102/// Primitive types use the default TypeSpecification from the parser (single source of truth).
2103#[must_use]
2104pub fn primitive_boolean() -> &'static LemmaType {
2105    PRIMITIVE_BOOLEAN.get_or_init(|| LemmaType::primitive(TypeSpecification::boolean()))
2106}
2107
2108#[must_use]
2109pub fn primitive_scale() -> &'static LemmaType {
2110    PRIMITIVE_SCALE.get_or_init(|| LemmaType::primitive(TypeSpecification::scale()))
2111}
2112
2113#[must_use]
2114pub fn primitive_number() -> &'static LemmaType {
2115    PRIMITIVE_NUMBER.get_or_init(|| LemmaType::primitive(TypeSpecification::number()))
2116}
2117
2118#[must_use]
2119pub fn primitive_text() -> &'static LemmaType {
2120    PRIMITIVE_TEXT.get_or_init(|| LemmaType::primitive(TypeSpecification::text()))
2121}
2122
2123#[must_use]
2124pub fn primitive_date() -> &'static LemmaType {
2125    PRIMITIVE_DATE.get_or_init(|| LemmaType::primitive(TypeSpecification::date()))
2126}
2127
2128#[must_use]
2129pub fn primitive_time() -> &'static LemmaType {
2130    PRIMITIVE_TIME.get_or_init(|| LemmaType::primitive(TypeSpecification::time()))
2131}
2132
2133#[must_use]
2134pub fn primitive_duration() -> &'static LemmaType {
2135    PRIMITIVE_DURATION.get_or_init(|| LemmaType::primitive(TypeSpecification::duration()))
2136}
2137
2138#[must_use]
2139pub fn primitive_ratio() -> &'static LemmaType {
2140    PRIMITIVE_RATIO.get_or_init(|| LemmaType::primitive(TypeSpecification::ratio()))
2141}
2142
2143/// Map PrimitiveKind to TypeSpecification. Single source of truth for primitive type resolution.
2144#[must_use]
2145pub fn type_spec_for_primitive(kind: PrimitiveKind) -> TypeSpecification {
2146    match kind {
2147        PrimitiveKind::Boolean => TypeSpecification::boolean(),
2148        PrimitiveKind::Scale => TypeSpecification::scale(),
2149        PrimitiveKind::Number => TypeSpecification::number(),
2150        PrimitiveKind::Percent | PrimitiveKind::Ratio => TypeSpecification::ratio(),
2151        PrimitiveKind::Text => TypeSpecification::text(),
2152        PrimitiveKind::Date => TypeSpecification::date(),
2153        PrimitiveKind::Time => TypeSpecification::time(),
2154        PrimitiveKind::Duration => TypeSpecification::duration(),
2155    }
2156}
2157
2158// -----------------------------------------------------------------------------
2159// Display implementations
2160// -----------------------------------------------------------------------------
2161
2162impl fmt::Display for PathSegment {
2163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2164        write!(f, "{} → {}", self.fact, self.spec)
2165    }
2166}
2167
2168impl fmt::Display for FactPath {
2169    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2170        for segment in &self.segments {
2171            write!(f, "{}.", segment)?;
2172        }
2173        write!(f, "{}", self.fact)
2174    }
2175}
2176
2177impl fmt::Display for RulePath {
2178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2179        for segment in &self.segments {
2180            write!(f, "{}.", segment)?;
2181        }
2182        write!(f, "{}", self.rule)
2183    }
2184}
2185
2186impl fmt::Display for LemmaType {
2187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2188        write!(f, "{}", self.name())
2189    }
2190}
2191
2192impl fmt::Display for LiteralValue {
2193    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2194        match &self.value {
2195            ValueKind::Scale(n, u) => {
2196                if let TypeSpecification::Scale { decimals, .. } = &self.lemma_type.specifications {
2197                    let s = match decimals {
2198                        Some(d) => {
2199                            let dp = u32::from(*d);
2200                            let rounded = n.round_dp(dp);
2201                            format!("{:.prec$}", rounded, prec = *d as usize)
2202                        }
2203                        None => n.normalize().to_string(),
2204                    };
2205                    return write!(f, "{} {}", s, u);
2206                }
2207                write!(f, "{}", self.value)
2208            }
2209            ValueKind::Ratio(r, Some(unit_name)) => {
2210                if let TypeSpecification::Ratio { units, .. } = &self.lemma_type.specifications {
2211                    if let Ok(unit) = units.get(unit_name) {
2212                        let display_value = (*r * unit.value).normalize();
2213                        let s = if display_value.fract().is_zero() {
2214                            display_value.trunc().to_string()
2215                        } else {
2216                            display_value.to_string()
2217                        };
2218                        // Use shorthand symbols for percent (%) and permille (%%)
2219                        return match unit_name.as_str() {
2220                            "percent" => write!(f, "{}%", s),
2221                            "permille" => write!(f, "{}%%", s),
2222                            _ => write!(f, "{} {}", s, unit_name),
2223                        };
2224                    }
2225                }
2226                write!(f, "{}", self.value)
2227            }
2228            _ => write!(f, "{}", self.value),
2229        }
2230    }
2231}
2232
2233// -----------------------------------------------------------------------------
2234// Tests
2235// -----------------------------------------------------------------------------
2236
2237#[cfg(test)]
2238mod tests {
2239    use super::*;
2240    use crate::parsing::ast::{BooleanValue, DateTimeValue, DurationUnit, LemmaSpec, TimeValue};
2241    use rust_decimal::Decimal;
2242    use std::str::FromStr;
2243    use std::sync::Arc;
2244
2245    #[test]
2246    fn test_negated_comparison() {
2247        assert_eq!(
2248            negated_comparison(ComparisonComputation::LessThan),
2249            ComparisonComputation::GreaterThanOrEqual
2250        );
2251        assert_eq!(
2252            negated_comparison(ComparisonComputation::GreaterThanOrEqual),
2253            ComparisonComputation::LessThan
2254        );
2255        assert_eq!(
2256            negated_comparison(ComparisonComputation::Equal),
2257            ComparisonComputation::IsNot,
2258            "== negates to 'is not'"
2259        );
2260        assert_eq!(
2261            negated_comparison(ComparisonComputation::NotEqual),
2262            ComparisonComputation::Is,
2263            "!= negates to 'is'"
2264        );
2265        assert_eq!(
2266            negated_comparison(ComparisonComputation::Is),
2267            ComparisonComputation::IsNot
2268        );
2269        assert_eq!(
2270            negated_comparison(ComparisonComputation::IsNot),
2271            ComparisonComputation::Is
2272        );
2273    }
2274
2275    #[test]
2276    fn test_literal_value_to_primitive_type() {
2277        let one = Decimal::from_str("1").unwrap();
2278
2279        assert_eq!(LiteralValue::text("".to_string()).lemma_type.name(), "text");
2280        assert_eq!(LiteralValue::number(one).lemma_type.name(), "number");
2281        assert_eq!(
2282            LiteralValue::from_bool(bool::from(BooleanValue::True))
2283                .lemma_type
2284                .name(),
2285            "boolean"
2286        );
2287
2288        let dt = DateTimeValue {
2289            year: 2024,
2290            month: 1,
2291            day: 1,
2292            hour: 0,
2293            minute: 0,
2294            second: 0,
2295            microsecond: 0,
2296            timezone: None,
2297        };
2298        assert_eq!(
2299            LiteralValue::date(date_time_to_semantic(&dt))
2300                .lemma_type
2301                .name(),
2302            "date"
2303        );
2304        assert_eq!(
2305            LiteralValue::ratio(one / Decimal::from(100), Some("percent".to_string()))
2306                .lemma_type
2307                .name(),
2308            "ratio"
2309        );
2310        assert_eq!(
2311            LiteralValue::duration(one, duration_unit_to_semantic(&DurationUnit::Second))
2312                .lemma_type
2313                .name(),
2314            "duration"
2315        );
2316    }
2317
2318    #[test]
2319    fn test_spec_type_display() {
2320        assert_eq!(format!("{}", primitive_text()), "text");
2321        assert_eq!(format!("{}", primitive_number()), "number");
2322        assert_eq!(format!("{}", primitive_date()), "date");
2323        assert_eq!(format!("{}", primitive_boolean()), "boolean");
2324        assert_eq!(format!("{}", primitive_duration()), "duration");
2325    }
2326
2327    #[test]
2328    fn test_type_constructor() {
2329        let specs = TypeSpecification::number();
2330        let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
2331        assert_eq!(lemma_type.name(), "dice");
2332    }
2333
2334    #[test]
2335    fn test_type_display() {
2336        let specs = TypeSpecification::text();
2337        let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
2338        assert_eq!(format!("{}", lemma_type), "name");
2339    }
2340
2341    #[test]
2342    fn test_type_equality() {
2343        let specs1 = TypeSpecification::number();
2344        let specs2 = TypeSpecification::number();
2345        let lemma_type1 = LemmaType::new("dice".to_string(), specs1, TypeExtends::Primitive);
2346        let lemma_type2 = LemmaType::new("dice".to_string(), specs2, TypeExtends::Primitive);
2347        assert_eq!(lemma_type1, lemma_type2);
2348    }
2349
2350    #[test]
2351    fn test_type_serialization() {
2352        let specs = TypeSpecification::number();
2353        let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
2354        let serialized = serde_json::to_string(&lemma_type).unwrap();
2355        let deserialized: LemmaType = serde_json::from_str(&serialized).unwrap();
2356        assert_eq!(lemma_type, deserialized);
2357    }
2358
2359    #[test]
2360    fn test_literal_value_display_value() {
2361        let ten = Decimal::from_str("10").unwrap();
2362
2363        assert_eq!(
2364            LiteralValue::text("hello".to_string()).display_value(),
2365            "hello"
2366        );
2367        assert_eq!(LiteralValue::number(ten).display_value(), "10");
2368        assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
2369        assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
2370
2371        // 0.10 ratio with "percent" unit displays as 10% (unit conversion applied)
2372        let ten_percent_ratio = LiteralValue::ratio(
2373            Decimal::from_str("0.10").unwrap(),
2374            Some("percent".to_string()),
2375        );
2376        assert_eq!(ten_percent_ratio.display_value(), "10%");
2377
2378        let time = TimeValue {
2379            hour: 14,
2380            minute: 30,
2381            second: 0,
2382            timezone: None,
2383        };
2384        let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
2385        assert!(time_display.contains("14"));
2386        assert!(time_display.contains("30"));
2387    }
2388
2389    #[test]
2390    fn test_scale_display_respects_type_decimals() {
2391        let money_type = LemmaType {
2392            name: Some("money".to_string()),
2393            specifications: TypeSpecification::Scale {
2394                minimum: None,
2395                maximum: None,
2396                decimals: Some(2),
2397                precision: None,
2398                units: ScaleUnits::from(vec![ScaleUnit {
2399                    name: "eur".to_string(),
2400                    value: Decimal::from(1),
2401                }]),
2402                help: String::new(),
2403                default: None,
2404            },
2405            extends: TypeExtends::Primitive,
2406        };
2407        let val = LiteralValue::scale_with_type(
2408            Decimal::from_str("1.8").unwrap(),
2409            "eur".to_string(),
2410            money_type.clone(),
2411        );
2412        assert_eq!(val.display_value(), "1.80 eur");
2413        let more_precision = LiteralValue::scale_with_type(
2414            Decimal::from_str("1.80000").unwrap(),
2415            "eur".to_string(),
2416            money_type,
2417        );
2418        assert_eq!(more_precision.display_value(), "1.80 eur");
2419        let scale_no_decimals = LemmaType {
2420            name: Some("count".to_string()),
2421            specifications: TypeSpecification::Scale {
2422                minimum: None,
2423                maximum: None,
2424                decimals: None,
2425                precision: None,
2426                units: ScaleUnits::from(vec![ScaleUnit {
2427                    name: "items".to_string(),
2428                    value: Decimal::from(1),
2429                }]),
2430                help: String::new(),
2431                default: None,
2432            },
2433            extends: TypeExtends::Primitive,
2434        };
2435        let val_any = LiteralValue::scale_with_type(
2436            Decimal::from_str("42.50").unwrap(),
2437            "items".to_string(),
2438            scale_no_decimals,
2439        );
2440        assert_eq!(val_any.display_value(), "42.5 items");
2441    }
2442
2443    #[test]
2444    fn test_literal_value_time_type() {
2445        let time = TimeValue {
2446            hour: 14,
2447            minute: 30,
2448            second: 0,
2449            timezone: None,
2450        };
2451        let lit = LiteralValue::time(time_to_semantic(&time));
2452        assert_eq!(lit.lemma_type.name(), "time");
2453    }
2454
2455    #[test]
2456    fn test_scale_family_name_primitive_root() {
2457        let scale_spec = TypeSpecification::scale();
2458        let money_primitive = LemmaType::new(
2459            "money".to_string(),
2460            scale_spec.clone(),
2461            TypeExtends::Primitive,
2462        );
2463        assert_eq!(money_primitive.scale_family_name(), Some("money"));
2464    }
2465
2466    #[test]
2467    fn test_scale_family_name_custom() {
2468        let scale_spec = TypeSpecification::scale();
2469        let money_custom = LemmaType::new(
2470            "money".to_string(),
2471            scale_spec,
2472            TypeExtends::custom_local("money".to_string(), "money".to_string()),
2473        );
2474        assert_eq!(money_custom.scale_family_name(), Some("money"));
2475    }
2476
2477    #[test]
2478    fn test_same_scale_family_same_name_different_extends() {
2479        let scale_spec = TypeSpecification::scale();
2480        let money_primitive = LemmaType::new(
2481            "money".to_string(),
2482            scale_spec.clone(),
2483            TypeExtends::Primitive,
2484        );
2485        let money_custom = LemmaType::new(
2486            "money".to_string(),
2487            scale_spec,
2488            TypeExtends::custom_local("money".to_string(), "money".to_string()),
2489        );
2490        assert!(money_primitive.same_scale_family(&money_custom));
2491        assert!(money_custom.same_scale_family(&money_primitive));
2492    }
2493
2494    #[test]
2495    fn test_same_scale_family_parent_and_child() {
2496        let scale_spec = TypeSpecification::scale();
2497        let type_x = LemmaType::new("x".to_string(), scale_spec.clone(), TypeExtends::Primitive);
2498        let type_x2 = LemmaType::new(
2499            "x2".to_string(),
2500            scale_spec,
2501            TypeExtends::custom_local("x".to_string(), "x".to_string()),
2502        );
2503        assert_eq!(type_x.scale_family_name(), Some("x"));
2504        assert_eq!(type_x2.scale_family_name(), Some("x"));
2505        assert!(type_x.same_scale_family(&type_x2));
2506        assert!(type_x2.same_scale_family(&type_x));
2507    }
2508
2509    #[test]
2510    fn test_same_scale_family_siblings() {
2511        let scale_spec = TypeSpecification::scale();
2512        let type_x2_a = LemmaType::new(
2513            "x2a".to_string(),
2514            scale_spec.clone(),
2515            TypeExtends::custom_local("x".to_string(), "x".to_string()),
2516        );
2517        let type_x2_b = LemmaType::new(
2518            "x2b".to_string(),
2519            scale_spec,
2520            TypeExtends::custom_local("x".to_string(), "x".to_string()),
2521        );
2522        assert!(type_x2_a.same_scale_family(&type_x2_b));
2523    }
2524
2525    #[test]
2526    fn test_same_scale_family_different_families() {
2527        let scale_spec = TypeSpecification::scale();
2528        let money = LemmaType::new(
2529            "money".to_string(),
2530            scale_spec.clone(),
2531            TypeExtends::Primitive,
2532        );
2533        let temperature = LemmaType::new(
2534            "temperature".to_string(),
2535            scale_spec,
2536            TypeExtends::Primitive,
2537        );
2538        assert!(!money.same_scale_family(&temperature));
2539        assert!(!temperature.same_scale_family(&money));
2540    }
2541
2542    #[test]
2543    fn test_same_scale_family_scale_vs_non_scale() {
2544        let scale_spec = TypeSpecification::scale();
2545        let number_spec = TypeSpecification::number();
2546        let scale_type = LemmaType::new("money".to_string(), scale_spec, TypeExtends::Primitive);
2547        let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
2548        assert!(!scale_type.same_scale_family(&number_type));
2549        assert!(!number_type.same_scale_family(&scale_type));
2550    }
2551
2552    #[test]
2553    fn test_scale_family_name_non_scale_returns_none() {
2554        let number_spec = TypeSpecification::number();
2555        let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
2556        assert_eq!(number_type.scale_family_name(), None);
2557    }
2558
2559    #[test]
2560    fn test_explicit_value_returns_none_for_default() {
2561        let source = crate::Source::new(
2562            "test.lemma",
2563            crate::parsing::ast::Span {
2564                start: 0,
2565                end: 1,
2566                line: 1,
2567                col: 0,
2568            },
2569        );
2570        let fact = FactData::Value {
2571            value: LiteralValue::number(Decimal::from(25)),
2572            source: source.clone(),
2573            is_default: true,
2574        };
2575        assert!(
2576            fact.explicit_value().is_none(),
2577            "is_default=true should yield None from explicit_value()"
2578        );
2579        assert!(
2580            fact.value().is_some(),
2581            "value() should still return the value regardless of is_default"
2582        );
2583    }
2584
2585    #[test]
2586    fn test_explicit_value_returns_some_for_non_default() {
2587        let source = crate::Source::new(
2588            "test.lemma",
2589            crate::parsing::ast::Span {
2590                start: 0,
2591                end: 1,
2592                line: 1,
2593                col: 0,
2594            },
2595        );
2596        let fact = FactData::Value {
2597            value: LiteralValue::number(Decimal::from(42)),
2598            source,
2599            is_default: false,
2600        };
2601        assert!(
2602            fact.explicit_value().is_some(),
2603            "is_default=false should yield Some from explicit_value()"
2604        );
2605        assert_eq!(
2606            fact.explicit_value().unwrap().value,
2607            ValueKind::Number(Decimal::from(42))
2608        );
2609    }
2610
2611    #[test]
2612    fn test_explicit_value_returns_none_for_type_declaration() {
2613        let source = crate::Source::new(
2614            "test.lemma",
2615            crate::parsing::ast::Span {
2616                start: 0,
2617                end: 1,
2618                line: 1,
2619                col: 0,
2620            },
2621        );
2622        let fact = FactData::TypeDeclaration {
2623            resolved_type: primitive_number().clone(),
2624            source,
2625        };
2626        assert!(
2627            fact.explicit_value().is_none(),
2628            "TypeDeclaration should yield None from explicit_value()"
2629        );
2630    }
2631
2632    #[test]
2633    fn test_lemma_type_inequality_local_vs_import_same_shape() {
2634        let dep = Arc::new(LemmaSpec::new("dep".to_string()));
2635        let scale_spec = TypeSpecification::scale();
2636        let local = LemmaType::new(
2637            "t".to_string(),
2638            scale_spec.clone(),
2639            TypeExtends::custom_local("money".to_string(), "money".to_string()),
2640        );
2641        let imported = LemmaType::new(
2642            "t".to_string(),
2643            scale_spec,
2644            TypeExtends::Custom {
2645                parent: "money".to_string(),
2646                family: "money".to_string(),
2647                defining_spec: TypeDefiningSpec::Import {
2648                    spec: Arc::clone(&dep),
2649                    resolved_plan_hash: "a1b2c3d4".to_string(),
2650                },
2651            },
2652        );
2653        assert_ne!(local, imported);
2654    }
2655
2656    #[test]
2657    fn test_lemma_type_equality_import_same_resolved_spec_semantics() {
2658        let spec_a = Arc::new(LemmaSpec::new("dep".to_string()));
2659        let spec_b = Arc::new(LemmaSpec::new("dep".to_string()));
2660        assert!(is_same_spec(spec_a.as_ref(), spec_b.as_ref()));
2661        let scale_spec = TypeSpecification::scale();
2662        let left = LemmaType::new(
2663            "t".to_string(),
2664            scale_spec.clone(),
2665            TypeExtends::Custom {
2666                parent: "money".to_string(),
2667                family: "money".to_string(),
2668                defining_spec: TypeDefiningSpec::Import {
2669                    spec: Arc::clone(&spec_a),
2670                    resolved_plan_hash: "a1b2c3d4".to_string(),
2671                },
2672            },
2673        );
2674        let right = LemmaType::new(
2675            "t".to_string(),
2676            scale_spec,
2677            TypeExtends::Custom {
2678                parent: "money".to_string(),
2679                family: "money".to_string(),
2680                defining_spec: TypeDefiningSpec::Import {
2681                    spec: spec_b,
2682                    resolved_plan_hash: "a1b2c3d4".to_string(),
2683                },
2684            },
2685        );
2686        assert_eq!(left, right);
2687    }
2688}