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