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