Skip to main content

lemma/planning/
semantics.rs

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