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 (internal use only - not user-declarable)
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        #[serde(alias = "expected_hash_pin")]
1912        resolved_plan_hash: Option<String>,
1913    },
1914}
1915
1916impl FactData {
1917    /// Returns the schema type for value and type-declaration facts; `None` for spec references.
1918    pub fn schema_type(&self) -> Option<&LemmaType> {
1919        match self {
1920            FactData::Value { value, .. } => Some(&value.lemma_type),
1921            FactData::TypeDeclaration { resolved_type, .. } => Some(resolved_type),
1922            FactData::SpecRef { .. } => None,
1923        }
1924    }
1925
1926    /// Returns the literal value for value facts; `None` for type-declaration and spec references.
1927    pub fn value(&self) -> Option<&LiteralValue> {
1928        match self {
1929            FactData::Value { value, .. } => Some(value),
1930            FactData::TypeDeclaration { .. } | FactData::SpecRef { .. } => None,
1931        }
1932    }
1933
1934    /// Returns the literal value only if it was explicitly defined in the spec
1935    /// (not from a type `-> default` constraint). Used by schema methods to decide
1936    /// which facts need user input.
1937    pub fn explicit_value(&self) -> Option<&LiteralValue> {
1938        match self {
1939            FactData::Value {
1940                value, is_default, ..
1941            } => {
1942                if *is_default {
1943                    None
1944                } else {
1945                    Some(value)
1946                }
1947            }
1948            FactData::TypeDeclaration { .. } | FactData::SpecRef { .. } => None,
1949        }
1950    }
1951
1952    /// Returns the source location for this fact.
1953    pub fn source(&self) -> &Source {
1954        match self {
1955            FactData::Value { source, .. } => source,
1956            FactData::TypeDeclaration { source, .. } => source,
1957            FactData::SpecRef { source, .. } => source,
1958        }
1959    }
1960
1961    /// Returns the resolved dependency plan hash for spec reference facts; `None` for other fact kinds.
1962    pub fn resolved_plan_hash(&self) -> Option<&str> {
1963        match self {
1964            FactData::Value { .. } | FactData::TypeDeclaration { .. } => None,
1965            FactData::SpecRef {
1966                resolved_plan_hash, ..
1967            } => resolved_plan_hash.as_deref(),
1968        }
1969    }
1970
1971    /// Returns the referenced spec Arc for spec reference facts; `None` otherwise.
1972    pub fn spec_arc(&self) -> Option<&Arc<crate::parsing::ast::LemmaSpec>> {
1973        match self {
1974            FactData::Value { .. } | FactData::TypeDeclaration { .. } => None,
1975            FactData::SpecRef { spec: spec_arc, .. } => Some(spec_arc),
1976        }
1977    }
1978
1979    /// Returns the referenced spec name for spec reference facts; `None` otherwise.
1980    pub fn spec_ref(&self) -> Option<&str> {
1981        match self {
1982            FactData::Value { .. } | FactData::TypeDeclaration { .. } => None,
1983            FactData::SpecRef { spec, .. } => Some(&spec.name),
1984        }
1985    }
1986}
1987
1988/// Convert parser Value to ValueKind. Fails if Scale/Ratio have no unit (strict).
1989pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
1990    use crate::parsing::ast::Value;
1991    Ok(match value {
1992        Value::Number(n) => ValueKind::Number(*n),
1993        Value::Text(s) => ValueKind::Text(s.clone()),
1994        Value::Boolean(b) => ValueKind::Boolean(bool::from(*b)),
1995        Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
1996        Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
1997        Value::Duration(n, u) => ValueKind::Duration(*n, duration_unit_to_semantic(u)),
1998        Value::Scale(n, unit) => ValueKind::Scale(*n, unit.clone()),
1999        Value::Ratio(n, unit) => ValueKind::Ratio(*n, unit.clone()),
2000    })
2001}
2002
2003/// Convert AST date-time to semantic (for tests and planning).
2004pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
2005    SemanticDateTime {
2006        year: dt.year,
2007        month: dt.month,
2008        day: dt.day,
2009        hour: dt.hour,
2010        minute: dt.minute,
2011        second: dt.second,
2012        microsecond: dt.microsecond,
2013        timezone: dt.timezone.as_ref().map(|tz| SemanticTimezone {
2014            offset_hours: tz.offset_hours,
2015            offset_minutes: tz.offset_minutes,
2016        }),
2017    }
2018}
2019
2020/// Convert AST time to semantic (for tests and planning).
2021pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
2022    SemanticTime {
2023        hour: t.hour.into(),
2024        minute: t.minute.into(),
2025        second: t.second.into(),
2026        timezone: t.timezone.as_ref().map(|tz| SemanticTimezone {
2027            offset_hours: tz.offset_hours,
2028            offset_minutes: tz.offset_minutes,
2029        }),
2030    }
2031}
2032
2033/// Convert AST duration unit to semantic (for tests and planning).
2034pub(crate) fn duration_unit_to_semantic(
2035    u: &crate::parsing::ast::DurationUnit,
2036) -> SemanticDurationUnit {
2037    use crate::parsing::ast::DurationUnit as DU;
2038    match u {
2039        DU::Year => SemanticDurationUnit::Year,
2040        DU::Month => SemanticDurationUnit::Month,
2041        DU::Week => SemanticDurationUnit::Week,
2042        DU::Day => SemanticDurationUnit::Day,
2043        DU::Hour => SemanticDurationUnit::Hour,
2044        DU::Minute => SemanticDurationUnit::Minute,
2045        DU::Second => SemanticDurationUnit::Second,
2046        DU::Millisecond => SemanticDurationUnit::Millisecond,
2047        DU::Microsecond => SemanticDurationUnit::Microsecond,
2048    }
2049}
2050
2051/// Convert AST conversion target to semantic (planning boundary; evaluation/computation use only semantic).
2052///
2053/// The AST uses `ConversionTarget::Unit(name)` for non-duration units; this function looks up `name`
2054/// in the spec's unit index and returns `RatioUnit` or `ScaleUnit` based on the type that defines
2055/// the unit. The unit must be defined by a scale or ratio type in the spec (e.g. primitive ratio for
2056/// "percent", "permille").
2057pub fn conversion_target_to_semantic(
2058    ct: &ConversionTarget,
2059    unit_index: Option<&HashMap<String, (LemmaType, Option<crate::parsing::ast::TypeDef>)>>,
2060) -> Result<SemanticConversionTarget, String> {
2061    match ct {
2062        ConversionTarget::Duration(u) => Ok(SemanticConversionTarget::Duration(
2063            duration_unit_to_semantic(u),
2064        )),
2065        ConversionTarget::Unit(name) => {
2066            let index = unit_index.ok_or_else(|| {
2067                "Unit conversion requires type resolution; unit index not available.".to_string()
2068            })?;
2069            let (lemma_type, _) = index.get(name).ok_or_else(|| {
2070                format!(
2071                    "Unknown unit '{}'. Unit must be defined by a scale or ratio type.",
2072                    name
2073                )
2074            })?;
2075            if lemma_type.is_ratio() {
2076                Ok(SemanticConversionTarget::RatioUnit(name.clone()))
2077            } else if lemma_type.is_scale() {
2078                Ok(SemanticConversionTarget::ScaleUnit(name.clone()))
2079            } else {
2080                Err(format!(
2081                    "Unit '{}' is not a ratio or scale type; cannot use it in conversion.",
2082                    name
2083                ))
2084            }
2085        }
2086    }
2087}
2088
2089// -----------------------------------------------------------------------------
2090// Primitive type constructors (moved from parsing::ast)
2091// -----------------------------------------------------------------------------
2092
2093// Private statics for lazy initialization
2094static PRIMITIVE_BOOLEAN: OnceLock<LemmaType> = OnceLock::new();
2095static PRIMITIVE_SCALE: OnceLock<LemmaType> = OnceLock::new();
2096static PRIMITIVE_NUMBER: OnceLock<LemmaType> = OnceLock::new();
2097static PRIMITIVE_TEXT: OnceLock<LemmaType> = OnceLock::new();
2098static PRIMITIVE_DATE: OnceLock<LemmaType> = OnceLock::new();
2099static PRIMITIVE_TIME: OnceLock<LemmaType> = OnceLock::new();
2100static PRIMITIVE_DURATION: OnceLock<LemmaType> = OnceLock::new();
2101static PRIMITIVE_RATIO: OnceLock<LemmaType> = OnceLock::new();
2102
2103/// Primitive types use the default TypeSpecification from the parser (single source of truth).
2104#[must_use]
2105pub fn primitive_boolean() -> &'static LemmaType {
2106    PRIMITIVE_BOOLEAN.get_or_init(|| LemmaType::primitive(TypeSpecification::boolean()))
2107}
2108
2109#[must_use]
2110pub fn primitive_scale() -> &'static LemmaType {
2111    PRIMITIVE_SCALE.get_or_init(|| LemmaType::primitive(TypeSpecification::scale()))
2112}
2113
2114#[must_use]
2115pub fn primitive_number() -> &'static LemmaType {
2116    PRIMITIVE_NUMBER.get_or_init(|| LemmaType::primitive(TypeSpecification::number()))
2117}
2118
2119#[must_use]
2120pub fn primitive_text() -> &'static LemmaType {
2121    PRIMITIVE_TEXT.get_or_init(|| LemmaType::primitive(TypeSpecification::text()))
2122}
2123
2124#[must_use]
2125pub fn primitive_date() -> &'static LemmaType {
2126    PRIMITIVE_DATE.get_or_init(|| LemmaType::primitive(TypeSpecification::date()))
2127}
2128
2129#[must_use]
2130pub fn primitive_time() -> &'static LemmaType {
2131    PRIMITIVE_TIME.get_or_init(|| LemmaType::primitive(TypeSpecification::time()))
2132}
2133
2134#[must_use]
2135pub fn primitive_duration() -> &'static LemmaType {
2136    PRIMITIVE_DURATION.get_or_init(|| LemmaType::primitive(TypeSpecification::duration()))
2137}
2138
2139#[must_use]
2140pub fn primitive_ratio() -> &'static LemmaType {
2141    PRIMITIVE_RATIO.get_or_init(|| LemmaType::primitive(TypeSpecification::ratio()))
2142}
2143
2144/// Map PrimitiveKind to TypeSpecification. Single source of truth for primitive type resolution.
2145#[must_use]
2146pub fn type_spec_for_primitive(kind: PrimitiveKind) -> TypeSpecification {
2147    match kind {
2148        PrimitiveKind::Boolean => TypeSpecification::boolean(),
2149        PrimitiveKind::Scale => TypeSpecification::scale(),
2150        PrimitiveKind::Number => TypeSpecification::number(),
2151        PrimitiveKind::Percent | PrimitiveKind::Ratio => TypeSpecification::ratio(),
2152        PrimitiveKind::Text => TypeSpecification::text(),
2153        PrimitiveKind::Date => TypeSpecification::date(),
2154        PrimitiveKind::Time => TypeSpecification::time(),
2155        PrimitiveKind::Duration => TypeSpecification::duration(),
2156    }
2157}
2158
2159// -----------------------------------------------------------------------------
2160// Display implementations
2161// -----------------------------------------------------------------------------
2162
2163impl fmt::Display for PathSegment {
2164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2165        write!(f, "{} → {}", self.fact, self.spec)
2166    }
2167}
2168
2169impl fmt::Display for FactPath {
2170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2171        for segment in &self.segments {
2172            write!(f, "{}.", segment)?;
2173        }
2174        write!(f, "{}", self.fact)
2175    }
2176}
2177
2178impl fmt::Display for RulePath {
2179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2180        for segment in &self.segments {
2181            write!(f, "{}.", segment)?;
2182        }
2183        write!(f, "{}", self.rule)
2184    }
2185}
2186
2187impl fmt::Display for LemmaType {
2188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2189        write!(f, "{}", self.name())
2190    }
2191}
2192
2193impl fmt::Display for LiteralValue {
2194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2195        match &self.value {
2196            ValueKind::Scale(n, u) => {
2197                if let TypeSpecification::Scale { decimals, .. } = &self.lemma_type.specifications {
2198                    let s = match decimals {
2199                        Some(d) => {
2200                            let dp = u32::from(*d);
2201                            let rounded = n.round_dp(dp);
2202                            format!("{:.prec$}", rounded, prec = *d as usize)
2203                        }
2204                        None => n.normalize().to_string(),
2205                    };
2206                    return write!(f, "{} {}", s, u);
2207                }
2208                write!(f, "{}", self.value)
2209            }
2210            ValueKind::Ratio(r, Some(unit_name)) => {
2211                if let TypeSpecification::Ratio { units, .. } = &self.lemma_type.specifications {
2212                    if let Ok(unit) = units.get(unit_name) {
2213                        let display_value = (*r * unit.value).normalize();
2214                        let s = if display_value.fract().is_zero() {
2215                            display_value.trunc().to_string()
2216                        } else {
2217                            display_value.to_string()
2218                        };
2219                        // Use shorthand symbols for percent (%) and permille (%%)
2220                        return match unit_name.as_str() {
2221                            "percent" => write!(f, "{}%", s),
2222                            "permille" => write!(f, "{}%%", s),
2223                            _ => write!(f, "{} {}", s, unit_name),
2224                        };
2225                    }
2226                }
2227                write!(f, "{}", self.value)
2228            }
2229            _ => write!(f, "{}", self.value),
2230        }
2231    }
2232}
2233
2234// -----------------------------------------------------------------------------
2235// Tests
2236// -----------------------------------------------------------------------------
2237
2238#[cfg(test)]
2239mod tests {
2240    use super::*;
2241    use crate::parsing::ast::{BooleanValue, DateTimeValue, DurationUnit, LemmaSpec, TimeValue};
2242    use rust_decimal::Decimal;
2243    use std::str::FromStr;
2244    use std::sync::Arc;
2245
2246    #[test]
2247    fn test_negated_comparison() {
2248        assert_eq!(
2249            negated_comparison(ComparisonComputation::LessThan),
2250            ComparisonComputation::GreaterThanOrEqual
2251        );
2252        assert_eq!(
2253            negated_comparison(ComparisonComputation::GreaterThanOrEqual),
2254            ComparisonComputation::LessThan
2255        );
2256        assert_eq!(
2257            negated_comparison(ComparisonComputation::Equal),
2258            ComparisonComputation::IsNot,
2259            "== negates to 'is not'"
2260        );
2261        assert_eq!(
2262            negated_comparison(ComparisonComputation::NotEqual),
2263            ComparisonComputation::Is,
2264            "!= negates to 'is'"
2265        );
2266        assert_eq!(
2267            negated_comparison(ComparisonComputation::Is),
2268            ComparisonComputation::IsNot
2269        );
2270        assert_eq!(
2271            negated_comparison(ComparisonComputation::IsNot),
2272            ComparisonComputation::Is
2273        );
2274    }
2275
2276    #[test]
2277    fn test_literal_value_to_primitive_type() {
2278        let one = Decimal::from_str("1").unwrap();
2279
2280        assert_eq!(LiteralValue::text("".to_string()).lemma_type.name(), "text");
2281        assert_eq!(LiteralValue::number(one).lemma_type.name(), "number");
2282        assert_eq!(
2283            LiteralValue::from_bool(bool::from(BooleanValue::True))
2284                .lemma_type
2285                .name(),
2286            "boolean"
2287        );
2288
2289        let dt = DateTimeValue {
2290            year: 2024,
2291            month: 1,
2292            day: 1,
2293            hour: 0,
2294            minute: 0,
2295            second: 0,
2296            microsecond: 0,
2297            timezone: None,
2298        };
2299        assert_eq!(
2300            LiteralValue::date(date_time_to_semantic(&dt))
2301                .lemma_type
2302                .name(),
2303            "date"
2304        );
2305        assert_eq!(
2306            LiteralValue::ratio(one / Decimal::from(100), Some("percent".to_string()))
2307                .lemma_type
2308                .name(),
2309            "ratio"
2310        );
2311        assert_eq!(
2312            LiteralValue::duration(one, duration_unit_to_semantic(&DurationUnit::Second))
2313                .lemma_type
2314                .name(),
2315            "duration"
2316        );
2317    }
2318
2319    #[test]
2320    fn test_spec_type_display() {
2321        assert_eq!(format!("{}", primitive_text()), "text");
2322        assert_eq!(format!("{}", primitive_number()), "number");
2323        assert_eq!(format!("{}", primitive_date()), "date");
2324        assert_eq!(format!("{}", primitive_boolean()), "boolean");
2325        assert_eq!(format!("{}", primitive_duration()), "duration");
2326    }
2327
2328    #[test]
2329    fn test_type_constructor() {
2330        let specs = TypeSpecification::number();
2331        let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
2332        assert_eq!(lemma_type.name(), "dice");
2333    }
2334
2335    #[test]
2336    fn test_type_display() {
2337        let specs = TypeSpecification::text();
2338        let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
2339        assert_eq!(format!("{}", lemma_type), "name");
2340    }
2341
2342    #[test]
2343    fn test_type_equality() {
2344        let specs1 = TypeSpecification::number();
2345        let specs2 = TypeSpecification::number();
2346        let lemma_type1 = LemmaType::new("dice".to_string(), specs1, TypeExtends::Primitive);
2347        let lemma_type2 = LemmaType::new("dice".to_string(), specs2, TypeExtends::Primitive);
2348        assert_eq!(lemma_type1, lemma_type2);
2349    }
2350
2351    #[test]
2352    fn test_type_serialization() {
2353        let specs = TypeSpecification::number();
2354        let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
2355        let serialized = serde_json::to_string(&lemma_type).unwrap();
2356        let deserialized: LemmaType = serde_json::from_str(&serialized).unwrap();
2357        assert_eq!(lemma_type, deserialized);
2358    }
2359
2360    #[test]
2361    fn test_literal_value_display_value() {
2362        let ten = Decimal::from_str("10").unwrap();
2363
2364        assert_eq!(
2365            LiteralValue::text("hello".to_string()).display_value(),
2366            "hello"
2367        );
2368        assert_eq!(LiteralValue::number(ten).display_value(), "10");
2369        assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
2370        assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
2371
2372        // 0.10 ratio with "percent" unit displays as 10% (unit conversion applied)
2373        let ten_percent_ratio = LiteralValue::ratio(
2374            Decimal::from_str("0.10").unwrap(),
2375            Some("percent".to_string()),
2376        );
2377        assert_eq!(ten_percent_ratio.display_value(), "10%");
2378
2379        let time = TimeValue {
2380            hour: 14,
2381            minute: 30,
2382            second: 0,
2383            timezone: None,
2384        };
2385        let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
2386        assert!(time_display.contains("14"));
2387        assert!(time_display.contains("30"));
2388    }
2389
2390    #[test]
2391    fn test_scale_display_respects_type_decimals() {
2392        let money_type = LemmaType {
2393            name: Some("money".to_string()),
2394            specifications: TypeSpecification::Scale {
2395                minimum: None,
2396                maximum: None,
2397                decimals: Some(2),
2398                precision: None,
2399                units: ScaleUnits::from(vec![ScaleUnit {
2400                    name: "eur".to_string(),
2401                    value: Decimal::from(1),
2402                }]),
2403                help: String::new(),
2404                default: None,
2405            },
2406            extends: TypeExtends::Primitive,
2407        };
2408        let val = LiteralValue::scale_with_type(
2409            Decimal::from_str("1.8").unwrap(),
2410            "eur".to_string(),
2411            money_type.clone(),
2412        );
2413        assert_eq!(val.display_value(), "1.80 eur");
2414        let more_precision = LiteralValue::scale_with_type(
2415            Decimal::from_str("1.80000").unwrap(),
2416            "eur".to_string(),
2417            money_type,
2418        );
2419        assert_eq!(more_precision.display_value(), "1.80 eur");
2420        let scale_no_decimals = LemmaType {
2421            name: Some("count".to_string()),
2422            specifications: TypeSpecification::Scale {
2423                minimum: None,
2424                maximum: None,
2425                decimals: None,
2426                precision: None,
2427                units: ScaleUnits::from(vec![ScaleUnit {
2428                    name: "items".to_string(),
2429                    value: Decimal::from(1),
2430                }]),
2431                help: String::new(),
2432                default: None,
2433            },
2434            extends: TypeExtends::Primitive,
2435        };
2436        let val_any = LiteralValue::scale_with_type(
2437            Decimal::from_str("42.50").unwrap(),
2438            "items".to_string(),
2439            scale_no_decimals,
2440        );
2441        assert_eq!(val_any.display_value(), "42.5 items");
2442    }
2443
2444    #[test]
2445    fn test_literal_value_time_type() {
2446        let time = TimeValue {
2447            hour: 14,
2448            minute: 30,
2449            second: 0,
2450            timezone: None,
2451        };
2452        let lit = LiteralValue::time(time_to_semantic(&time));
2453        assert_eq!(lit.lemma_type.name(), "time");
2454    }
2455
2456    #[test]
2457    fn test_scale_family_name_primitive_root() {
2458        let scale_spec = TypeSpecification::scale();
2459        let money_primitive = LemmaType::new(
2460            "money".to_string(),
2461            scale_spec.clone(),
2462            TypeExtends::Primitive,
2463        );
2464        assert_eq!(money_primitive.scale_family_name(), Some("money"));
2465    }
2466
2467    #[test]
2468    fn test_scale_family_name_custom() {
2469        let scale_spec = TypeSpecification::scale();
2470        let money_custom = LemmaType::new(
2471            "money".to_string(),
2472            scale_spec,
2473            TypeExtends::custom_local("money".to_string(), "money".to_string()),
2474        );
2475        assert_eq!(money_custom.scale_family_name(), Some("money"));
2476    }
2477
2478    #[test]
2479    fn test_same_scale_family_same_name_different_extends() {
2480        let scale_spec = TypeSpecification::scale();
2481        let money_primitive = LemmaType::new(
2482            "money".to_string(),
2483            scale_spec.clone(),
2484            TypeExtends::Primitive,
2485        );
2486        let money_custom = LemmaType::new(
2487            "money".to_string(),
2488            scale_spec,
2489            TypeExtends::custom_local("money".to_string(), "money".to_string()),
2490        );
2491        assert!(money_primitive.same_scale_family(&money_custom));
2492        assert!(money_custom.same_scale_family(&money_primitive));
2493    }
2494
2495    #[test]
2496    fn test_same_scale_family_parent_and_child() {
2497        let scale_spec = TypeSpecification::scale();
2498        let type_x = LemmaType::new("x".to_string(), scale_spec.clone(), TypeExtends::Primitive);
2499        let type_x2 = LemmaType::new(
2500            "x2".to_string(),
2501            scale_spec,
2502            TypeExtends::custom_local("x".to_string(), "x".to_string()),
2503        );
2504        assert_eq!(type_x.scale_family_name(), Some("x"));
2505        assert_eq!(type_x2.scale_family_name(), Some("x"));
2506        assert!(type_x.same_scale_family(&type_x2));
2507        assert!(type_x2.same_scale_family(&type_x));
2508    }
2509
2510    #[test]
2511    fn test_same_scale_family_siblings() {
2512        let scale_spec = TypeSpecification::scale();
2513        let type_x2_a = LemmaType::new(
2514            "x2a".to_string(),
2515            scale_spec.clone(),
2516            TypeExtends::custom_local("x".to_string(), "x".to_string()),
2517        );
2518        let type_x2_b = LemmaType::new(
2519            "x2b".to_string(),
2520            scale_spec,
2521            TypeExtends::custom_local("x".to_string(), "x".to_string()),
2522        );
2523        assert!(type_x2_a.same_scale_family(&type_x2_b));
2524    }
2525
2526    #[test]
2527    fn test_same_scale_family_different_families() {
2528        let scale_spec = TypeSpecification::scale();
2529        let money = LemmaType::new(
2530            "money".to_string(),
2531            scale_spec.clone(),
2532            TypeExtends::Primitive,
2533        );
2534        let temperature = LemmaType::new(
2535            "temperature".to_string(),
2536            scale_spec,
2537            TypeExtends::Primitive,
2538        );
2539        assert!(!money.same_scale_family(&temperature));
2540        assert!(!temperature.same_scale_family(&money));
2541    }
2542
2543    #[test]
2544    fn test_same_scale_family_scale_vs_non_scale() {
2545        let scale_spec = TypeSpecification::scale();
2546        let number_spec = TypeSpecification::number();
2547        let scale_type = LemmaType::new("money".to_string(), scale_spec, TypeExtends::Primitive);
2548        let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
2549        assert!(!scale_type.same_scale_family(&number_type));
2550        assert!(!number_type.same_scale_family(&scale_type));
2551    }
2552
2553    #[test]
2554    fn test_scale_family_name_non_scale_returns_none() {
2555        let number_spec = TypeSpecification::number();
2556        let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
2557        assert_eq!(number_type.scale_family_name(), None);
2558    }
2559
2560    #[test]
2561    fn test_explicit_value_returns_none_for_default() {
2562        let source = crate::Source::new(
2563            "test.lemma",
2564            crate::parsing::ast::Span {
2565                start: 0,
2566                end: 1,
2567                line: 1,
2568                col: 0,
2569            },
2570        );
2571        let fact = FactData::Value {
2572            value: LiteralValue::number(Decimal::from(25)),
2573            source: source.clone(),
2574            is_default: true,
2575        };
2576        assert!(
2577            fact.explicit_value().is_none(),
2578            "is_default=true should yield None from explicit_value()"
2579        );
2580        assert!(
2581            fact.value().is_some(),
2582            "value() should still return the value regardless of is_default"
2583        );
2584    }
2585
2586    #[test]
2587    fn test_explicit_value_returns_some_for_non_default() {
2588        let source = crate::Source::new(
2589            "test.lemma",
2590            crate::parsing::ast::Span {
2591                start: 0,
2592                end: 1,
2593                line: 1,
2594                col: 0,
2595            },
2596        );
2597        let fact = FactData::Value {
2598            value: LiteralValue::number(Decimal::from(42)),
2599            source,
2600            is_default: false,
2601        };
2602        assert!(
2603            fact.explicit_value().is_some(),
2604            "is_default=false should yield Some from explicit_value()"
2605        );
2606        assert_eq!(
2607            fact.explicit_value().unwrap().value,
2608            ValueKind::Number(Decimal::from(42))
2609        );
2610    }
2611
2612    #[test]
2613    fn test_explicit_value_returns_none_for_type_declaration() {
2614        let source = crate::Source::new(
2615            "test.lemma",
2616            crate::parsing::ast::Span {
2617                start: 0,
2618                end: 1,
2619                line: 1,
2620                col: 0,
2621            },
2622        );
2623        let fact = FactData::TypeDeclaration {
2624            resolved_type: primitive_number().clone(),
2625            source,
2626        };
2627        assert!(
2628            fact.explicit_value().is_none(),
2629            "TypeDeclaration should yield None from explicit_value()"
2630        );
2631    }
2632
2633    #[test]
2634    fn test_lemma_type_inequality_local_vs_import_same_shape() {
2635        let dep = Arc::new(LemmaSpec::new("dep".to_string()));
2636        let scale_spec = TypeSpecification::scale();
2637        let local = LemmaType::new(
2638            "t".to_string(),
2639            scale_spec.clone(),
2640            TypeExtends::custom_local("money".to_string(), "money".to_string()),
2641        );
2642        let imported = LemmaType::new(
2643            "t".to_string(),
2644            scale_spec,
2645            TypeExtends::Custom {
2646                parent: "money".to_string(),
2647                family: "money".to_string(),
2648                defining_spec: TypeDefiningSpec::Import {
2649                    spec: Arc::clone(&dep),
2650                    resolved_plan_hash: "a1b2c3d4".to_string(),
2651                },
2652            },
2653        );
2654        assert_ne!(local, imported);
2655    }
2656
2657    #[test]
2658    fn test_lemma_type_equality_import_same_resolved_spec_semantics() {
2659        let spec_a = Arc::new(LemmaSpec::new("dep".to_string()));
2660        let spec_b = Arc::new(LemmaSpec::new("dep".to_string()));
2661        assert!(is_same_spec(spec_a.as_ref(), spec_b.as_ref()));
2662        let scale_spec = TypeSpecification::scale();
2663        let left = LemmaType::new(
2664            "t".to_string(),
2665            scale_spec.clone(),
2666            TypeExtends::Custom {
2667                parent: "money".to_string(),
2668                family: "money".to_string(),
2669                defining_spec: TypeDefiningSpec::Import {
2670                    spec: Arc::clone(&spec_a),
2671                    resolved_plan_hash: "a1b2c3d4".to_string(),
2672                },
2673            },
2674        );
2675        let right = LemmaType::new(
2676            "t".to_string(),
2677            scale_spec,
2678            TypeExtends::Custom {
2679                parent: "money".to_string(),
2680                family: "money".to_string(),
2681                defining_spec: TypeDefiningSpec::Import {
2682                    spec: spec_b,
2683                    resolved_plan_hash: "a1b2c3d4".to_string(),
2684                },
2685            },
2686        );
2687        assert_eq!(left, right);
2688    }
2689}