1use crate::parsing::ast::{
7 ComparisonComputation, DateTimeValue, FactValue, LemmaSpec, TimeValue, TypeDef,
8};
9use crate::planning::semantics::{
10 Expression, ExpressionKind, FactData, FactPath, LemmaType, RulePath, SemanticConversionTarget,
11 TypeSpecification, ValueKind,
12};
13use crate::Error;
14use crate::Source;
15use indexmap::IndexMap;
16use rust_decimal::Decimal;
17use std::cmp::Ordering;
18use std::collections::{HashMap, HashSet};
19use std::sync::Arc;
20
21pub fn validate_type_specifications(
31 specs: &TypeSpecification,
32 type_name: &str,
33 source: &Source,
34 spec_context: Option<Arc<LemmaSpec>>,
35) -> Vec<Error> {
36 let mut errors = Vec::new();
37
38 match specs {
39 TypeSpecification::Scale {
40 minimum,
41 maximum,
42 decimals,
43 precision,
44 default,
45 units,
46 ..
47 } => {
48 if let (Some(min), Some(max)) = (minimum, maximum) {
50 if min > max {
51 errors.push(Error::validation_with_context(
52 format!(
53 "Type '{}' has invalid range: minimum {} is greater than maximum {}",
54 type_name, min, max
55 ),
56 Some(source.clone()),
57 None::<String>,
58 spec_context.clone(),
59 None,
60 ));
61 }
62 }
63
64 if let Some(d) = decimals {
66 if *d > 28 {
67 errors.push(Error::validation_with_context(
68 format!(
69 "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
70 type_name, d
71 ),
72 Some(source.clone()),
73 None::<String>,
74 spec_context.clone(),
75 None,
76 ));
77 }
78 }
79
80 if let Some(prec) = precision {
82 if *prec <= Decimal::ZERO {
83 errors.push(Error::validation_with_context(
84 format!(
85 "Type '{}' has invalid precision: {}. Must be positive",
86 type_name, prec
87 ),
88 Some(source.clone()),
89 None::<String>,
90 spec_context.clone(),
91 None,
92 ));
93 }
94 }
95
96 if let Some((def_value, def_unit)) = default {
98 if !units.iter().any(|u| u.name == *def_unit) {
100 errors.push(Error::validation_with_context(
101 format!(
102 "Type '{}' default unit '{}' is not a valid unit. Valid units: {}",
103 type_name,
104 def_unit,
105 units
106 .iter()
107 .map(|u| u.name.clone())
108 .collect::<Vec<_>>()
109 .join(", ")
110 ),
111 Some(source.clone()),
112 None::<String>,
113 spec_context.clone(),
114 None,
115 ));
116 }
117 if let Some(min) = minimum {
118 if *def_value < *min {
119 errors.push(Error::validation_with_context(
120 format!(
121 "Type '{}' default value {} {} is less than minimum {}",
122 type_name, def_value, def_unit, min
123 ),
124 Some(source.clone()),
125 None::<String>,
126 spec_context.clone(),
127 None,
128 ));
129 }
130 }
131 if let Some(max) = maximum {
132 if *def_value > *max {
133 errors.push(Error::validation_with_context(
134 format!(
135 "Type '{}' default value {} {} is greater than maximum {}",
136 type_name, def_value, def_unit, max
137 ),
138 Some(source.clone()),
139 None::<String>,
140 spec_context.clone(),
141 None,
142 ));
143 }
144 }
145 }
146
147 if units.is_empty() {
149 errors.push(Error::validation_with_context(
150 format!(
151 "Type '{}' is a scale type but has no units. Scale types must define at least one unit (e.g. -> unit eur 1).",
152 type_name
153 ),
154 Some(source.clone()),
155 None::<String>,
156 spec_context.clone(),
157 None,
158 ));
159 }
160
161 if !units.is_empty() {
163 let mut seen_names: Vec<String> = Vec::new();
164 for unit in units.iter() {
165 if unit.name.trim().is_empty() {
167 errors.push(Error::validation_with_context(
168 format!(
169 "Type '{}' has a unit with empty name. Unit names cannot be empty.",
170 type_name
171 ),
172 Some(source.clone()),
173 None::<String>,
174 spec_context.clone(),
175 None,
176 ));
177 }
178
179 let lower_name = unit.name.to_lowercase();
181 if seen_names
182 .iter()
183 .any(|seen| seen.to_lowercase() == lower_name)
184 {
185 errors.push(Error::validation_with_context(
186 format!("Type '{}' has duplicate unit name '{}' (case-insensitive). Unit names must be unique within a type.", type_name, unit.name),
187 Some(source.clone()),
188 None::<String>,
189 spec_context.clone(),
190 None,
191 ));
192 } else {
193 seen_names.push(unit.name.clone());
194 }
195
196 if unit.value <= Decimal::ZERO {
198 errors.push(Error::validation_with_context(
199 format!("Type '{}' has unit '{}' with invalid value {}. Unit values must be positive (conversion factor relative to type base).", type_name, unit.name, unit.value),
200 Some(source.clone()),
201 None::<String>,
202 spec_context.clone(),
203 None,
204 ));
205 }
206 }
207 }
208 }
209 TypeSpecification::Number {
210 minimum,
211 maximum,
212 decimals,
213 precision,
214 default,
215 ..
216 } => {
217 if let (Some(min), Some(max)) = (minimum, maximum) {
219 if min > max {
220 errors.push(Error::validation_with_context(
221 format!(
222 "Type '{}' has invalid range: minimum {} is greater than maximum {}",
223 type_name, min, max
224 ),
225 Some(source.clone()),
226 None::<String>,
227 spec_context.clone(),
228 None,
229 ));
230 }
231 }
232
233 if let Some(d) = decimals {
235 if *d > 28 {
236 errors.push(Error::validation_with_context(
237 format!(
238 "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
239 type_name, d
240 ),
241 Some(source.clone()),
242 None::<String>,
243 spec_context.clone(),
244 None,
245 ));
246 }
247 }
248
249 if let Some(prec) = precision {
251 if *prec <= Decimal::ZERO {
252 errors.push(Error::validation_with_context(
253 format!(
254 "Type '{}' has invalid precision: {}. Must be positive",
255 type_name, prec
256 ),
257 Some(source.clone()),
258 None::<String>,
259 spec_context.clone(),
260 None,
261 ));
262 }
263 }
264
265 if let Some(def) = default {
267 if let Some(min) = minimum {
268 if *def < *min {
269 errors.push(Error::validation_with_context(
270 format!(
271 "Type '{}' default value {} is less than minimum {}",
272 type_name, def, min
273 ),
274 Some(source.clone()),
275 None::<String>,
276 spec_context.clone(),
277 None,
278 ));
279 }
280 }
281 if let Some(max) = maximum {
282 if *def > *max {
283 errors.push(Error::validation_with_context(
284 format!(
285 "Type '{}' default value {} is greater than maximum {}",
286 type_name, def, max
287 ),
288 Some(source.clone()),
289 None::<String>,
290 spec_context.clone(),
291 None,
292 ));
293 }
294 }
295 }
296 }
298
299 TypeSpecification::Ratio {
300 minimum,
301 maximum,
302 decimals,
303 default,
304 units,
305 ..
306 } => {
307 if let Some(d) = decimals {
309 if *d > 28 {
310 errors.push(Error::validation_with_context(
311 format!(
312 "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
313 type_name, d
314 ),
315 Some(source.clone()),
316 None::<String>,
317 spec_context.clone(),
318 None,
319 ));
320 }
321 }
322
323 if let (Some(min), Some(max)) = (minimum, maximum) {
325 if min > max {
326 errors.push(Error::validation_with_context(
327 format!(
328 "Type '{}' has invalid range: minimum {} is greater than maximum {}",
329 type_name, min, max
330 ),
331 Some(source.clone()),
332 None::<String>,
333 spec_context.clone(),
334 None,
335 ));
336 }
337 }
338
339 if let Some(def) = default {
341 if let Some(min) = minimum {
342 if *def < *min {
343 errors.push(Error::validation_with_context(
344 format!(
345 "Type '{}' default value {} is less than minimum {}",
346 type_name, def, min
347 ),
348 Some(source.clone()),
349 None::<String>,
350 spec_context.clone(),
351 None,
352 ));
353 }
354 }
355 if let Some(max) = maximum {
356 if *def > *max {
357 errors.push(Error::validation_with_context(
358 format!(
359 "Type '{}' default value {} is greater than maximum {}",
360 type_name, def, max
361 ),
362 Some(source.clone()),
363 None::<String>,
364 spec_context.clone(),
365 None,
366 ));
367 }
368 }
369 }
370
371 if !units.is_empty() {
375 let mut seen_names: Vec<String> = Vec::new();
376 for unit in units.iter() {
377 if unit.name.trim().is_empty() {
379 errors.push(Error::validation_with_context(
380 format!(
381 "Type '{}' has a unit with empty name. Unit names cannot be empty.",
382 type_name
383 ),
384 Some(source.clone()),
385 None::<String>,
386 spec_context.clone(),
387 None,
388 ));
389 }
390
391 let lower_name = unit.name.to_lowercase();
393 if seen_names
394 .iter()
395 .any(|seen| seen.to_lowercase() == lower_name)
396 {
397 errors.push(Error::validation_with_context(
398 format!("Type '{}' has duplicate unit name '{}' (case-insensitive). Unit names must be unique within a type.", type_name, unit.name),
399 Some(source.clone()),
400 None::<String>,
401 spec_context.clone(),
402 None,
403 ));
404 } else {
405 seen_names.push(unit.name.clone());
406 }
407
408 if unit.value <= Decimal::ZERO {
410 errors.push(Error::validation_with_context(
411 format!("Type '{}' has unit '{}' with invalid value {}. Unit values must be positive (conversion factor relative to type base).", type_name, unit.name, unit.value),
412 Some(source.clone()),
413 None::<String>,
414 spec_context.clone(),
415 None,
416 ));
417 }
418 }
419 }
420 }
421
422 TypeSpecification::Text {
423 minimum,
424 maximum,
425 length,
426 options,
427 default,
428 ..
429 } => {
430 if let (Some(min), Some(max)) = (minimum, maximum) {
432 if min > max {
433 errors.push(Error::validation_with_context(
434 format!("Type '{}' has invalid range: minimum length {} is greater than maximum length {}", type_name, min, max),
435 Some(source.clone()),
436 None::<String>,
437 spec_context.clone(),
438 None,
439 ));
440 }
441 }
442
443 if let Some(len) = length {
445 if let Some(min) = minimum {
446 if *len < *min {
447 errors.push(Error::validation_with_context(
448 format!("Type '{}' has inconsistent length constraint: length {} is less than minimum {}", type_name, len, min),
449 Some(source.clone()),
450 None::<String>,
451 spec_context.clone(),
452 None,
453 ));
454 }
455 }
456 if let Some(max) = maximum {
457 if *len > *max {
458 errors.push(Error::validation_with_context(
459 format!("Type '{}' has inconsistent length constraint: length {} is greater than maximum {}", type_name, len, max),
460 Some(source.clone()),
461 None::<String>,
462 spec_context.clone(),
463 None,
464 ));
465 }
466 }
467 }
468
469 if let Some(def) = default {
471 let def_len = def.len();
472
473 if let Some(min) = minimum {
474 if def_len < *min {
475 errors.push(Error::validation_with_context(
476 format!(
477 "Type '{}' default value length {} is less than minimum {}",
478 type_name, def_len, min
479 ),
480 Some(source.clone()),
481 None::<String>,
482 spec_context.clone(),
483 None,
484 ));
485 }
486 }
487 if let Some(max) = maximum {
488 if def_len > *max {
489 errors.push(Error::validation_with_context(
490 format!(
491 "Type '{}' default value length {} is greater than maximum {}",
492 type_name, def_len, max
493 ),
494 Some(source.clone()),
495 None::<String>,
496 spec_context.clone(),
497 None,
498 ));
499 }
500 }
501 if let Some(len) = length {
502 if def_len != *len {
503 errors.push(Error::validation_with_context(
504 format!("Type '{}' default value length {} does not match required length {}", type_name, def_len, len),
505 Some(source.clone()),
506 None::<String>,
507 spec_context.clone(),
508 None,
509 ));
510 }
511 }
512 if !options.is_empty() && !options.contains(def) {
513 errors.push(Error::validation_with_context(
514 format!(
515 "Type '{}' default value '{}' is not in allowed options: {:?}",
516 type_name, def, options
517 ),
518 Some(source.clone()),
519 None::<String>,
520 spec_context.clone(),
521 None,
522 ));
523 }
524 }
525 }
526
527 TypeSpecification::Date {
528 minimum,
529 maximum,
530 default,
531 ..
532 } => {
533 if let (Some(min), Some(max)) = (minimum, maximum) {
535 if compare_date_values(min, max) == Ordering::Greater {
536 errors.push(Error::validation_with_context(
537 format!(
538 "Type '{}' has invalid date range: minimum {} is after maximum {}",
539 type_name, min, max
540 ),
541 Some(source.clone()),
542 None::<String>,
543 spec_context.clone(),
544 None,
545 ));
546 }
547 }
548
549 if let Some(def) = default {
551 if let Some(min) = minimum {
552 if compare_date_values(def, min) == Ordering::Less {
553 errors.push(Error::validation_with_context(
554 format!(
555 "Type '{}' default date {} is before minimum {}",
556 type_name, def, min
557 ),
558 Some(source.clone()),
559 None::<String>,
560 spec_context.clone(),
561 None,
562 ));
563 }
564 }
565 if let Some(max) = maximum {
566 if compare_date_values(def, max) == Ordering::Greater {
567 errors.push(Error::validation_with_context(
568 format!(
569 "Type '{}' default date {} is after maximum {}",
570 type_name, def, max
571 ),
572 Some(source.clone()),
573 None::<String>,
574 spec_context.clone(),
575 None,
576 ));
577 }
578 }
579 }
580 }
581
582 TypeSpecification::Time {
583 minimum,
584 maximum,
585 default,
586 ..
587 } => {
588 if let (Some(min), Some(max)) = (minimum, maximum) {
590 if compare_time_values(min, max) == Ordering::Greater {
591 errors.push(Error::validation_with_context(
592 format!(
593 "Type '{}' has invalid time range: minimum {} is after maximum {}",
594 type_name, min, max
595 ),
596 Some(source.clone()),
597 None::<String>,
598 spec_context.clone(),
599 None,
600 ));
601 }
602 }
603
604 if let Some(def) = default {
606 if let Some(min) = minimum {
607 if compare_time_values(def, min) == Ordering::Less {
608 errors.push(Error::validation_with_context(
609 format!(
610 "Type '{}' default time {} is before minimum {}",
611 type_name, def, min
612 ),
613 Some(source.clone()),
614 None::<String>,
615 spec_context.clone(),
616 None,
617 ));
618 }
619 }
620 if let Some(max) = maximum {
621 if compare_time_values(def, max) == Ordering::Greater {
622 errors.push(Error::validation_with_context(
623 format!(
624 "Type '{}' default time {} is after maximum {}",
625 type_name, def, max
626 ),
627 Some(source.clone()),
628 None::<String>,
629 spec_context.clone(),
630 None,
631 ));
632 }
633 }
634 }
635 }
636
637 TypeSpecification::Boolean { .. } | TypeSpecification::Duration { .. } => {
638 }
640 TypeSpecification::Veto { .. } => {
641 }
644 TypeSpecification::Undetermined => unreachable!(
645 "BUG: validate_type_specification_constraints called with Undetermined sentinel type; this type exists only during type inference"
646 ),
647 }
648
649 errors
650}
651
652fn compare_date_values(left: &DateTimeValue, right: &DateTimeValue) -> Ordering {
654 left.year
656 .cmp(&right.year)
657 .then_with(|| left.month.cmp(&right.month))
658 .then_with(|| left.day.cmp(&right.day))
659 .then_with(|| left.hour.cmp(&right.hour))
660 .then_with(|| left.minute.cmp(&right.minute))
661 .then_with(|| left.second.cmp(&right.second))
662}
663
664fn compare_time_values(left: &TimeValue, right: &TimeValue) -> Ordering {
666 left.hour
668 .cmp(&right.hour)
669 .then_with(|| left.minute.cmp(&right.minute))
670 .then_with(|| left.second.cmp(&right.second))
671}
672
673pub struct RuleEntryForBindingCheck {
679 pub rule_type: LemmaType,
680 pub depends_on_rules: std::collections::BTreeSet<RulePath>,
681 pub branches: Vec<(Option<Expression>, Expression)>,
682}
683
684#[derive(Clone, Copy, Debug)]
685enum BaseTypeRequirement {
686 Any,
687 Boolean,
688 Number,
689 Duration,
690 Ratio,
691 Scale,
692 Text,
693 Date,
694 Time,
695 Comparable,
696 Numeric,
697}
698
699#[derive(Clone, Debug)]
700struct NumericLiteralConstraint {
701 op: ComparisonComputation,
702 literal: Decimal,
703 reference_on_left: bool,
704}
705
706#[derive(Clone, Debug)]
707enum RuleRefRequirement {
708 Base(BaseTypeRequirement),
709 ScaleMustContainUnit(String),
710 RatioMustContainUnit(String),
711 SameBaseAs(LemmaType),
712 SameScaleFamilyAs(LemmaType),
713 ArithmeticCompatibleWithNumber,
714 ArithmeticCompatibleWithRatio,
715 ArithmeticCompatibleWithScale(LemmaType),
716 ArithmeticCompatibleWithDuration,
717 NumericLiteral(NumericLiteralConstraint),
718}
719
720impl RuleRefRequirement {
721 fn describe(&self) -> String {
722 match self {
723 RuleRefRequirement::Base(kind) => match kind {
724 BaseTypeRequirement::Any => "any".to_string(),
725 BaseTypeRequirement::Boolean => "boolean".to_string(),
726 BaseTypeRequirement::Number => "number".to_string(),
727 BaseTypeRequirement::Duration => "duration".to_string(),
728 BaseTypeRequirement::Ratio => "ratio".to_string(),
729 BaseTypeRequirement::Scale => "scale".to_string(),
730 BaseTypeRequirement::Text => "text".to_string(),
731 BaseTypeRequirement::Date => "date".to_string(),
732 BaseTypeRequirement::Time => "time".to_string(),
733 BaseTypeRequirement::Comparable => "comparable".to_string(),
734 BaseTypeRequirement::Numeric => "numeric (number, scale, or ratio)".to_string(),
735 },
736 RuleRefRequirement::ScaleMustContainUnit(unit) => {
737 format!("scale type containing unit '{}'", unit)
738 }
739 RuleRefRequirement::RatioMustContainUnit(unit) => {
740 format!("ratio type containing unit '{}'", unit)
741 }
742 RuleRefRequirement::SameBaseAs(other) => {
743 format!("same base type as {}", other.name())
744 }
745 RuleRefRequirement::SameScaleFamilyAs(other) => {
746 format!("same scale family as {}", other.name())
747 }
748 RuleRefRequirement::ArithmeticCompatibleWithNumber => {
749 "arithmetic-compatible with number (number or ratio)".to_string()
750 }
751 RuleRefRequirement::ArithmeticCompatibleWithRatio => {
752 "arithmetic-compatible with ratio".to_string()
753 }
754 RuleRefRequirement::ArithmeticCompatibleWithScale(other) => {
755 format!("arithmetic-compatible with scale family {}", other.name())
756 }
757 RuleRefRequirement::ArithmeticCompatibleWithDuration => {
758 "arithmetic-compatible with duration".to_string()
759 }
760 RuleRefRequirement::NumericLiteral(rule) => {
761 let side = if rule.reference_on_left {
762 "left"
763 } else {
764 "right"
765 };
766 format!(
767 "numeric range compatible with comparison (rule-ref {} side, op {:?}, literal {})",
768 side, rule.op, rule.literal
769 )
770 }
771 }
772 }
773}
774
775fn lemma_type_to_base_requirement(lemma_type: &LemmaType) -> BaseTypeRequirement {
776 if lemma_type.is_boolean() {
777 return BaseTypeRequirement::Boolean;
778 }
779 if lemma_type.is_number() {
780 return BaseTypeRequirement::Number;
781 }
782 if lemma_type.is_scale() {
783 return BaseTypeRequirement::Scale;
784 }
785 if lemma_type.is_duration() {
786 return BaseTypeRequirement::Duration;
787 }
788 if lemma_type.is_ratio() {
789 return BaseTypeRequirement::Ratio;
790 }
791 if lemma_type.is_text() {
792 return BaseTypeRequirement::Text;
793 }
794 if lemma_type.is_date() {
795 return BaseTypeRequirement::Date;
796 }
797 if lemma_type.is_time() {
798 return BaseTypeRequirement::Time;
799 }
800 BaseTypeRequirement::Any
801}
802
803fn base_requirement_satisfied(lemma_type: &LemmaType, constraint: BaseTypeRequirement) -> bool {
804 match constraint {
805 BaseTypeRequirement::Any => true,
806 BaseTypeRequirement::Boolean => lemma_type.is_boolean(),
807 BaseTypeRequirement::Number => lemma_type.is_number(),
808 BaseTypeRequirement::Duration => lemma_type.is_duration(),
809 BaseTypeRequirement::Ratio => lemma_type.is_ratio(),
810 BaseTypeRequirement::Scale => lemma_type.is_scale(),
811 BaseTypeRequirement::Text => lemma_type.is_text(),
812 BaseTypeRequirement::Date => lemma_type.is_date(),
813 BaseTypeRequirement::Time => lemma_type.is_time(),
814 BaseTypeRequirement::Numeric => {
815 lemma_type.is_number() || lemma_type.is_scale() || lemma_type.is_ratio()
816 }
817 BaseTypeRequirement::Comparable => {
818 lemma_type.is_boolean()
819 || lemma_type.is_text()
820 || lemma_type.is_number()
821 || lemma_type.is_ratio()
822 || lemma_type.is_date()
823 || lemma_type.is_time()
824 || lemma_type.is_scale()
825 || lemma_type.is_duration()
826 }
827 }
828}
829
830fn has_scale_unit(lemma_type: &LemmaType, unit: &str) -> bool {
831 match &lemma_type.specifications {
832 TypeSpecification::Scale { units, .. } => {
833 units.iter().any(|u| u.name.eq_ignore_ascii_case(unit))
834 }
835 _ => false,
836 }
837}
838
839fn has_ratio_unit(lemma_type: &LemmaType, unit: &str) -> bool {
840 match &lemma_type.specifications {
841 TypeSpecification::Ratio { units, .. } => {
842 units.iter().any(|u| u.name.eq_ignore_ascii_case(unit))
843 }
844 _ => false,
845 }
846}
847
848fn numeric_bounds(lemma_type: &LemmaType) -> Option<(Option<Decimal>, Option<Decimal>)> {
849 match &lemma_type.specifications {
850 TypeSpecification::Number {
851 minimum, maximum, ..
852 }
853 | TypeSpecification::Scale {
854 minimum, maximum, ..
855 }
856 | TypeSpecification::Ratio {
857 minimum, maximum, ..
858 } => Some((*minimum, *maximum)),
859 _ => None,
860 }
861}
862
863fn normalize_literal_constraint(rule: NumericLiteralConstraint) -> NumericLiteralConstraint {
864 if rule.reference_on_left {
865 return rule;
866 }
867 let op = match rule.op {
868 ComparisonComputation::GreaterThan => ComparisonComputation::LessThan,
869 ComparisonComputation::LessThan => ComparisonComputation::GreaterThan,
870 ComparisonComputation::GreaterThanOrEqual => ComparisonComputation::LessThanOrEqual,
871 ComparisonComputation::LessThanOrEqual => ComparisonComputation::GreaterThanOrEqual,
872 ComparisonComputation::Equal => ComparisonComputation::Equal,
873 ComparisonComputation::NotEqual => ComparisonComputation::NotEqual,
874 ComparisonComputation::Is => ComparisonComputation::Is,
875 ComparisonComputation::IsNot => ComparisonComputation::IsNot,
876 };
877 NumericLiteralConstraint {
878 op,
879 literal: rule.literal,
880 reference_on_left: true,
881 }
882}
883
884fn numeric_literal_constraint_satisfied(
885 lemma_type: &LemmaType,
886 rule: NumericLiteralConstraint,
887) -> bool {
888 let Some((minimum, maximum)) = numeric_bounds(lemma_type) else {
889 return false;
890 };
891 let normalized = normalize_literal_constraint(rule);
892 match normalized.op {
893 ComparisonComputation::GreaterThan => maximum.is_none_or(|max| max > normalized.literal),
894 ComparisonComputation::GreaterThanOrEqual => {
895 maximum.is_none_or(|max| max >= normalized.literal)
896 }
897 ComparisonComputation::LessThan => minimum.is_none_or(|min| min < normalized.literal),
898 ComparisonComputation::LessThanOrEqual => {
899 minimum.is_none_or(|min| min <= normalized.literal)
900 }
901 ComparisonComputation::Equal | ComparisonComputation::Is => {
902 minimum.is_none_or(|min| min <= normalized.literal)
903 && maximum.is_none_or(|max| max >= normalized.literal)
904 }
905 ComparisonComputation::NotEqual | ComparisonComputation::IsNot => {
906 !(minimum == Some(normalized.literal) && maximum == Some(normalized.literal))
907 }
908 }
909}
910
911fn rule_type_satisfies_requirement(
912 lemma_type: &LemmaType,
913 requirement: &RuleRefRequirement,
914) -> bool {
915 if lemma_type.is_undetermined() {
916 unreachable!("BUG: rule_type_satisfies_requirement called with undetermined type; this type exists only during type inference")
917 }
918 if lemma_type.vetoed() {
920 return true;
921 }
922 match requirement {
923 RuleRefRequirement::Base(kind) => base_requirement_satisfied(lemma_type, *kind),
924 RuleRefRequirement::ScaleMustContainUnit(unit) => {
925 lemma_type.is_scale() && has_scale_unit(lemma_type, unit)
926 }
927 RuleRefRequirement::RatioMustContainUnit(unit) => {
928 lemma_type.is_ratio() && has_ratio_unit(lemma_type, unit)
929 }
930 RuleRefRequirement::SameBaseAs(other) => lemma_type.has_same_base_type(other),
931 RuleRefRequirement::SameScaleFamilyAs(other) => {
932 lemma_type.is_scale() && other.is_scale() && lemma_type.same_scale_family(other)
933 }
934 RuleRefRequirement::ArithmeticCompatibleWithNumber => {
935 lemma_type.is_number() || lemma_type.is_ratio()
936 }
937 RuleRefRequirement::ArithmeticCompatibleWithRatio => {
938 lemma_type.is_number()
939 || lemma_type.is_ratio()
940 || lemma_type.is_scale()
941 || lemma_type.is_duration()
942 }
943 RuleRefRequirement::ArithmeticCompatibleWithScale(other) => {
944 lemma_type.is_number()
945 || lemma_type.is_ratio()
946 || (lemma_type.is_scale()
947 && other.is_scale()
948 && lemma_type.same_scale_family(other))
949 }
950 RuleRefRequirement::ArithmeticCompatibleWithDuration => {
951 lemma_type.is_number() || lemma_type.is_ratio() || lemma_type.is_duration()
952 }
953 RuleRefRequirement::NumericLiteral(rule) => {
954 numeric_literal_constraint_satisfied(lemma_type, rule.clone())
955 }
956 }
957}
958
959fn infer_interface_expression_type(
960 expr: &Expression,
961 rule_entries: &IndexMap<RulePath, RuleEntryForBindingCheck>,
962 facts: &IndexMap<FactPath, FactData>,
963) -> Option<LemmaType> {
964 match &expr.kind {
965 ExpressionKind::Literal(lv) => Some(lv.lemma_type.clone()),
966 ExpressionKind::FactPath(fp) => facts.get(fp).and_then(|f| f.schema_type().cloned()),
967 ExpressionKind::RulePath(rp) => rule_entries.get(rp).map(|r| r.rule_type.clone()),
968 _ => None,
969 }
970}
971
972fn numeric_literal_from_expression(expr: &Expression) -> Option<Decimal> {
973 let ExpressionKind::Literal(lv) = &expr.kind else {
974 return None;
975 };
976 match &lv.value {
977 ValueKind::Number(n) => Some(*n),
978 _ => None,
979 }
980}
981
982fn collect_expected_requirements_for_rule_ref(
983 expr: &Expression,
984 rule_path: &RulePath,
985 expected: RuleRefRequirement,
986 rule_entries: &IndexMap<RulePath, RuleEntryForBindingCheck>,
987 facts: &IndexMap<FactPath, FactData>,
988) -> Vec<(Option<Source>, RuleRefRequirement)> {
989 let mut out = Vec::new();
990 match &expr.kind {
991 ExpressionKind::RulePath(rp) => {
992 if rp == rule_path {
993 out.push((expr.source_location.clone(), expected));
994 }
995 }
996 ExpressionKind::LogicalAnd(left, right) => {
997 out.extend(collect_expected_requirements_for_rule_ref(
998 left,
999 rule_path,
1000 RuleRefRequirement::Base(BaseTypeRequirement::Boolean),
1001 rule_entries,
1002 facts,
1003 ));
1004 out.extend(collect_expected_requirements_for_rule_ref(
1005 right,
1006 rule_path,
1007 RuleRefRequirement::Base(BaseTypeRequirement::Boolean),
1008 rule_entries,
1009 facts,
1010 ));
1011 }
1012 ExpressionKind::LogicalNegation(operand, _) => {
1013 out.extend(collect_expected_requirements_for_rule_ref(
1014 operand,
1015 rule_path,
1016 RuleRefRequirement::Base(BaseTypeRequirement::Boolean),
1017 rule_entries,
1018 facts,
1019 ));
1020 }
1021 ExpressionKind::Comparison(left, op, right) => {
1022 out.extend(collect_expected_requirements_for_rule_ref(
1023 left,
1024 rule_path,
1025 RuleRefRequirement::Base(BaseTypeRequirement::Comparable),
1026 rule_entries,
1027 facts,
1028 ));
1029 out.extend(collect_expected_requirements_for_rule_ref(
1030 right,
1031 rule_path,
1032 RuleRefRequirement::Base(BaseTypeRequirement::Comparable),
1033 rule_entries,
1034 facts,
1035 ));
1036
1037 if let ExpressionKind::RulePath(rp) = &left.kind {
1038 if rp == rule_path {
1039 if let Some(other_type) =
1040 infer_interface_expression_type(right, rule_entries, facts)
1041 {
1042 out.push((
1043 expr.source_location.clone(),
1044 RuleRefRequirement::SameBaseAs(other_type.clone()),
1045 ));
1046 if other_type.is_scale() {
1047 out.push((
1048 expr.source_location.clone(),
1049 RuleRefRequirement::SameScaleFamilyAs(other_type),
1050 ));
1051 }
1052 }
1053 if let Some(lit) = numeric_literal_from_expression(right) {
1054 out.push((
1055 expr.source_location.clone(),
1056 RuleRefRequirement::NumericLiteral(NumericLiteralConstraint {
1057 op: op.clone(),
1058 literal: lit,
1059 reference_on_left: true,
1060 }),
1061 ));
1062 }
1063 }
1064 }
1065 if let ExpressionKind::RulePath(rp) = &right.kind {
1066 if rp == rule_path {
1067 if let Some(other_type) =
1068 infer_interface_expression_type(left, rule_entries, facts)
1069 {
1070 out.push((
1071 expr.source_location.clone(),
1072 RuleRefRequirement::SameBaseAs(other_type.clone()),
1073 ));
1074 if other_type.is_scale() {
1075 out.push((
1076 expr.source_location.clone(),
1077 RuleRefRequirement::SameScaleFamilyAs(other_type),
1078 ));
1079 }
1080 }
1081 if let Some(lit) = numeric_literal_from_expression(left) {
1082 out.push((
1083 expr.source_location.clone(),
1084 RuleRefRequirement::NumericLiteral(NumericLiteralConstraint {
1085 op: op.clone(),
1086 literal: lit,
1087 reference_on_left: false,
1088 }),
1089 ));
1090 }
1091 }
1092 }
1093 }
1094 ExpressionKind::Arithmetic(left, _, right) => {
1095 out.extend(collect_expected_requirements_for_rule_ref(
1096 left,
1097 rule_path,
1098 RuleRefRequirement::Base(BaseTypeRequirement::Numeric),
1099 rule_entries,
1100 facts,
1101 ));
1102 out.extend(collect_expected_requirements_for_rule_ref(
1103 right,
1104 rule_path,
1105 RuleRefRequirement::Base(BaseTypeRequirement::Numeric),
1106 rule_entries,
1107 facts,
1108 ));
1109
1110 if let ExpressionKind::RulePath(rp) = &left.kind {
1111 if rp == rule_path {
1112 if let Some(other_type) =
1113 infer_interface_expression_type(right, rule_entries, facts)
1114 {
1115 if other_type.is_scale() {
1116 out.push((
1117 expr.source_location.clone(),
1118 RuleRefRequirement::ArithmeticCompatibleWithScale(other_type),
1119 ));
1120 } else if other_type.is_number() || other_type.is_ratio() {
1121 out.push((
1122 expr.source_location.clone(),
1123 if other_type.is_number() {
1124 RuleRefRequirement::ArithmeticCompatibleWithNumber
1125 } else {
1126 RuleRefRequirement::ArithmeticCompatibleWithRatio
1127 },
1128 ));
1129 } else if other_type.is_duration() {
1130 out.push((
1131 expr.source_location.clone(),
1132 RuleRefRequirement::ArithmeticCompatibleWithDuration,
1133 ));
1134 }
1135 }
1136 }
1137 }
1138 if let ExpressionKind::RulePath(rp) = &right.kind {
1139 if rp == rule_path {
1140 if let Some(other_type) =
1141 infer_interface_expression_type(left, rule_entries, facts)
1142 {
1143 if other_type.is_scale() {
1144 out.push((
1145 expr.source_location.clone(),
1146 RuleRefRequirement::ArithmeticCompatibleWithScale(other_type),
1147 ));
1148 } else if other_type.is_number() || other_type.is_ratio() {
1149 out.push((
1150 expr.source_location.clone(),
1151 if other_type.is_number() {
1152 RuleRefRequirement::ArithmeticCompatibleWithNumber
1153 } else {
1154 RuleRefRequirement::ArithmeticCompatibleWithRatio
1155 },
1156 ));
1157 } else if other_type.is_duration() {
1158 out.push((
1159 expr.source_location.clone(),
1160 RuleRefRequirement::ArithmeticCompatibleWithDuration,
1161 ));
1162 }
1163 }
1164 }
1165 }
1166 }
1167 ExpressionKind::UnitConversion(source, target) => {
1168 let constraint = match target {
1169 SemanticConversionTarget::Duration(_) => {
1170 RuleRefRequirement::Base(BaseTypeRequirement::Duration)
1171 }
1172 SemanticConversionTarget::ScaleUnit(unit) => {
1173 RuleRefRequirement::ScaleMustContainUnit(unit.clone())
1174 }
1175 SemanticConversionTarget::RatioUnit(unit) => {
1176 RuleRefRequirement::RatioMustContainUnit(unit.clone())
1177 }
1178 };
1179 out.extend(collect_expected_requirements_for_rule_ref(
1180 source,
1181 rule_path,
1182 constraint,
1183 rule_entries,
1184 facts,
1185 ));
1186 }
1187 ExpressionKind::MathematicalComputation(_, operand) => {
1188 out.extend(collect_expected_requirements_for_rule_ref(
1189 operand,
1190 rule_path,
1191 RuleRefRequirement::Base(BaseTypeRequirement::Number),
1192 rule_entries,
1193 facts,
1194 ));
1195 }
1196 ExpressionKind::DateRelative(_, date_expr, tolerance) => {
1197 out.extend(collect_expected_requirements_for_rule_ref(
1198 date_expr,
1199 rule_path,
1200 RuleRefRequirement::Base(BaseTypeRequirement::Date),
1201 rule_entries,
1202 facts,
1203 ));
1204 if let Some(tol) = tolerance {
1205 out.extend(collect_expected_requirements_for_rule_ref(
1206 tol,
1207 rule_path,
1208 RuleRefRequirement::Base(BaseTypeRequirement::Duration),
1209 rule_entries,
1210 facts,
1211 ));
1212 }
1213 }
1214 ExpressionKind::DateCalendar(_, _, date_expr) => {
1215 out.extend(collect_expected_requirements_for_rule_ref(
1216 date_expr,
1217 rule_path,
1218 RuleRefRequirement::Base(BaseTypeRequirement::Date),
1219 rule_entries,
1220 facts,
1221 ));
1222 }
1223 ExpressionKind::Literal(_)
1224 | ExpressionKind::FactPath(_)
1225 | ExpressionKind::Veto(_)
1226 | ExpressionKind::Now => {}
1227 }
1228 out
1229}
1230
1231fn spec_interface_error(
1232 source: &Source,
1233 message: impl Into<String>,
1234 spec_context: Option<Arc<LemmaSpec>>,
1235 related_spec: Option<Arc<LemmaSpec>>,
1236) -> Error {
1237 Error::validation_with_context(
1238 message.into(),
1239 Some(source.clone()),
1240 None::<String>,
1241 spec_context,
1242 related_spec,
1243 )
1244}
1245
1246pub fn validate_spec_interfaces(
1256 referenced_rules: &HashMap<Vec<String>, HashSet<String>>,
1257 spec_ref_facts: &[(FactPath, Arc<LemmaSpec>, Source)],
1258 facts: &IndexMap<FactPath, FactData>,
1259 rule_entries: &IndexMap<RulePath, RuleEntryForBindingCheck>,
1260 main_spec: &Arc<LemmaSpec>,
1261) -> Result<(), Vec<Error>> {
1262 let mut errors = Vec::new();
1263
1264 for (fact_path, spec_arc, fact_source) in spec_ref_facts {
1265 let mut full_path: Vec<String> =
1266 fact_path.segments.iter().map(|s| s.fact.clone()).collect();
1267 full_path.push(fact_path.fact.clone());
1268
1269 let Some(required_rules) = referenced_rules.get(&full_path) else {
1270 continue;
1271 };
1272
1273 let spec = spec_arc.as_ref();
1274 let spec_rule_names: HashSet<&str> = spec.rules.iter().map(|r| r.name.as_str()).collect();
1275
1276 for required_rule in required_rules {
1277 if !spec_rule_names.contains(required_rule.as_str()) {
1278 errors.push(spec_interface_error(
1279 fact_source,
1280 format!(
1281 "Spec '{}' referenced by '{}' is missing required rule '{}'",
1282 spec.name, fact_path, required_rule
1283 ),
1284 Some(Arc::clone(main_spec)),
1285 Some(Arc::clone(spec_arc)),
1286 ));
1287 continue;
1288 }
1289
1290 let mut ref_segments = fact_path.segments.clone();
1291 ref_segments.push(crate::planning::semantics::PathSegment {
1292 fact: fact_path.fact.clone(),
1293 spec: spec.name.clone(),
1294 });
1295 let ref_rule_path = RulePath::new(ref_segments, required_rule.clone());
1296 let Some(ref_entry) = rule_entries.get(&ref_rule_path) else {
1297 let binding_path_str = fact_path
1298 .segments
1299 .iter()
1300 .map(|s| s.fact.as_str())
1301 .collect::<Vec<_>>()
1302 .join(".");
1303 let binding_path_str = if binding_path_str.is_empty() {
1304 fact_path.fact.clone()
1305 } else {
1306 format!("{}.{}", binding_path_str, fact_path.fact)
1307 };
1308 errors.push(spec_interface_error(
1309 fact_source,
1310 format!(
1311 "Fact binding '{}' sets spec reference to '{}', but interface validation could not resolve rule path '{}.{}' for contract checking",
1312 binding_path_str, spec.name, fact_path.fact, required_rule
1313 ),
1314 Some(Arc::clone(main_spec)),
1315 Some(Arc::clone(spec_arc)),
1316 ));
1317 continue;
1318 };
1319 let ref_rule_type = &ref_entry.rule_type;
1320
1321 for (_referencing_path, entry) in rule_entries {
1322 if !entry.depends_on_rules.contains(&ref_rule_path) {
1323 continue;
1324 }
1325 let expected =
1326 RuleRefRequirement::Base(lemma_type_to_base_requirement(&entry.rule_type));
1327 for (_condition, result_expr) in &entry.branches {
1328 let requirements = collect_expected_requirements_for_rule_ref(
1329 result_expr,
1330 &ref_rule_path,
1331 expected.clone(),
1332 rule_entries,
1333 facts,
1334 );
1335 for (_source, requirement) in requirements {
1336 if !rule_type_satisfies_requirement(ref_rule_type, &requirement) {
1337 let report_source = fact_source;
1338
1339 let binding_path_str = fact_path
1340 .segments
1341 .iter()
1342 .map(|s| s.fact.as_str())
1343 .collect::<Vec<_>>()
1344 .join(".");
1345 let binding_path_str = if binding_path_str.is_empty() {
1346 fact_path.fact.clone()
1347 } else {
1348 format!("{}.{}", binding_path_str, fact_path.fact)
1349 };
1350
1351 errors.push(spec_interface_error(
1352 report_source,
1353 format!(
1354 "Fact binding '{}' sets spec reference to '{}', but that spec's rule '{}' has result type {}; the referencing expression expects a {} value",
1355 binding_path_str,
1356 spec.name,
1357 required_rule,
1358 ref_rule_type.name(),
1359 requirement.describe(),
1360 ),
1361 Some(Arc::clone(main_spec)),
1362 Some(Arc::clone(spec_arc)),
1363 ));
1364 }
1365 }
1366 }
1367 }
1368 }
1369 }
1370
1371 if errors.is_empty() {
1372 Ok(())
1373 } else {
1374 Err(errors)
1375 }
1376}
1377
1378pub fn collect_bare_registry_refs(spec: &LemmaSpec) -> Vec<String> {
1384 if !spec.from_registry {
1385 return Vec::new();
1386 }
1387 let mut bare: Vec<String> = Vec::new();
1388 for fact in &spec.facts {
1389 match &fact.value {
1390 FactValue::SpecReference(r) if !r.from_registry => {
1391 bare.push(r.name.clone());
1392 }
1393 FactValue::TypeDeclaration { from: Some(r), .. } if !r.from_registry => {
1394 bare.push(r.name.clone());
1395 }
1396 _ => {}
1397 }
1398 }
1399 for type_def in &spec.types {
1400 match type_def {
1401 TypeDef::Import { from, .. } if !from.from_registry => {
1402 bare.push(from.name.clone());
1403 }
1404 TypeDef::Inline { from: Some(r), .. } if !r.from_registry => {
1405 bare.push(r.name.clone());
1406 }
1407 _ => {}
1408 }
1409 }
1410 bare
1411}
1412
1413#[cfg(test)]
1414mod tests {
1415 use super::*;
1416 use crate::parsing::ast::{CommandArg, TypeConstraintCommand};
1417 use crate::planning::semantics::{
1418 LemmaType, RatioUnit, RatioUnits, ScaleUnit, ScaleUnits, TypeSpecification,
1419 };
1420 use rust_decimal::Decimal;
1421
1422 fn test_source() -> Source {
1423 Source::new(
1424 "<test>",
1425 crate::parsing::ast::Span {
1426 start: 0,
1427 end: 0,
1428 line: 1,
1429 col: 0,
1430 },
1431 )
1432 }
1433
1434 #[test]
1435 fn validate_number_minimum_greater_than_maximum() {
1436 let mut specs = TypeSpecification::number();
1437 specs = specs
1438 .apply_constraint(
1439 TypeConstraintCommand::Minimum,
1440 &[CommandArg::Number("100".to_string())],
1441 )
1442 .unwrap();
1443 specs = specs
1444 .apply_constraint(
1445 TypeConstraintCommand::Maximum,
1446 &[CommandArg::Number("50".to_string())],
1447 )
1448 .unwrap();
1449
1450 let src = test_source();
1451 let errors = validate_type_specifications(&specs, "test", &src, None);
1452 assert_eq!(errors.len(), 1);
1453 assert!(errors[0]
1454 .to_string()
1455 .contains("minimum 100 is greater than maximum 50"));
1456 }
1457
1458 #[test]
1459 fn validate_number_valid_range() {
1460 let mut specs = TypeSpecification::number();
1461 specs = specs
1462 .apply_constraint(
1463 TypeConstraintCommand::Minimum,
1464 &[CommandArg::Number("0".to_string())],
1465 )
1466 .unwrap();
1467 specs = specs
1468 .apply_constraint(
1469 TypeConstraintCommand::Maximum,
1470 &[CommandArg::Number("100".to_string())],
1471 )
1472 .unwrap();
1473
1474 let src = test_source();
1475 let errors = validate_type_specifications(&specs, "test", &src, None);
1476 assert!(errors.is_empty());
1477 }
1478
1479 #[test]
1480 fn validate_number_default_below_minimum() {
1481 let specs = TypeSpecification::Number {
1482 minimum: Some(Decimal::from(10)),
1483 maximum: None,
1484 decimals: None,
1485 precision: None,
1486 help: String::new(),
1487 default: Some(Decimal::from(5)),
1488 };
1489
1490 let src = test_source();
1491 let errors = validate_type_specifications(&specs, "test", &src, None);
1492 assert_eq!(errors.len(), 1);
1493 assert!(errors[0]
1494 .to_string()
1495 .contains("default value 5 is less than minimum 10"));
1496 }
1497
1498 #[test]
1499 fn validate_number_default_above_maximum() {
1500 let specs = TypeSpecification::Number {
1501 minimum: None,
1502 maximum: Some(Decimal::from(100)),
1503 decimals: None,
1504 precision: None,
1505 help: String::new(),
1506 default: Some(Decimal::from(150)),
1507 };
1508
1509 let src = test_source();
1510 let errors = validate_type_specifications(&specs, "test", &src, None);
1511 assert_eq!(errors.len(), 1);
1512 assert!(errors[0]
1513 .to_string()
1514 .contains("default value 150 is greater than maximum 100"));
1515 }
1516
1517 #[test]
1518 fn validate_number_default_valid() {
1519 let specs = TypeSpecification::Number {
1520 minimum: Some(Decimal::from(0)),
1521 maximum: Some(Decimal::from(100)),
1522 decimals: None,
1523 precision: None,
1524 help: String::new(),
1525 default: Some(Decimal::from(50)),
1526 };
1527
1528 let src = test_source();
1529 let errors = validate_type_specifications(&specs, "test", &src, None);
1530 assert!(errors.is_empty());
1531 }
1532
1533 #[test]
1534 fn validate_text_minimum_greater_than_maximum() {
1535 let mut specs = TypeSpecification::text();
1536 specs = specs
1537 .apply_constraint(
1538 TypeConstraintCommand::Minimum,
1539 &[CommandArg::Number("100".to_string())],
1540 )
1541 .unwrap();
1542 specs = specs
1543 .apply_constraint(
1544 TypeConstraintCommand::Maximum,
1545 &[CommandArg::Number("50".to_string())],
1546 )
1547 .unwrap();
1548
1549 let src = test_source();
1550 let errors = validate_type_specifications(&specs, "test", &src, None);
1551 assert_eq!(errors.len(), 1);
1552 assert!(errors[0]
1553 .to_string()
1554 .contains("minimum length 100 is greater than maximum length 50"));
1555 }
1556
1557 #[test]
1558 fn validate_text_length_inconsistent_with_minimum() {
1559 let mut specs = TypeSpecification::text();
1560 specs = specs
1561 .apply_constraint(
1562 TypeConstraintCommand::Minimum,
1563 &[CommandArg::Number("10".to_string())],
1564 )
1565 .unwrap();
1566 specs = specs
1567 .apply_constraint(
1568 TypeConstraintCommand::Length,
1569 &[CommandArg::Number("5".to_string())],
1570 )
1571 .unwrap();
1572
1573 let src = test_source();
1574 let errors = validate_type_specifications(&specs, "test", &src, None);
1575 assert_eq!(errors.len(), 1);
1576 assert!(errors[0]
1577 .to_string()
1578 .contains("length 5 is less than minimum 10"));
1579 }
1580
1581 #[test]
1582 fn validate_text_default_not_in_options() {
1583 let specs = TypeSpecification::Text {
1584 minimum: None,
1585 maximum: None,
1586 length: None,
1587 options: vec!["red".to_string(), "blue".to_string()],
1588 help: String::new(),
1589 default: Some("green".to_string()),
1590 };
1591
1592 let src = test_source();
1593 let errors = validate_type_specifications(&specs, "test", &src, None);
1594 assert_eq!(errors.len(), 1);
1595 assert!(errors[0]
1596 .to_string()
1597 .contains("default value 'green' is not in allowed options"));
1598 }
1599
1600 #[test]
1601 fn validate_text_default_valid_in_options() {
1602 let specs = TypeSpecification::Text {
1603 minimum: None,
1604 maximum: None,
1605 length: None,
1606 options: vec!["red".to_string(), "blue".to_string()],
1607 help: String::new(),
1608 default: Some("red".to_string()),
1609 };
1610
1611 let src = test_source();
1612 let errors = validate_type_specifications(&specs, "test", &src, None);
1613 assert!(errors.is_empty());
1614 }
1615
1616 #[test]
1617 fn validate_ratio_minimum_greater_than_maximum() {
1618 let specs = TypeSpecification::Ratio {
1619 minimum: Some(Decimal::from(2)),
1620 maximum: Some(Decimal::from(1)),
1621 decimals: None,
1622 units: crate::planning::semantics::RatioUnits::new(),
1623 help: String::new(),
1624 default: None,
1625 };
1626
1627 let src = test_source();
1628 let errors = validate_type_specifications(&specs, "test", &src, None);
1629 assert_eq!(errors.len(), 1);
1630 assert!(errors[0]
1631 .to_string()
1632 .contains("minimum 2 is greater than maximum 1"));
1633 }
1634
1635 #[test]
1636 fn validate_date_minimum_after_maximum() {
1637 let mut specs = TypeSpecification::date();
1638 specs = specs
1639 .apply_constraint(
1640 TypeConstraintCommand::Minimum,
1641 &[CommandArg::Label("2024-12-31".to_string())],
1642 )
1643 .unwrap();
1644 specs = specs
1645 .apply_constraint(
1646 TypeConstraintCommand::Maximum,
1647 &[CommandArg::Label("2024-01-01".to_string())],
1648 )
1649 .unwrap();
1650
1651 let src = test_source();
1652 let errors = validate_type_specifications(&specs, "test", &src, None);
1653 assert_eq!(errors.len(), 1);
1654 assert!(
1655 errors[0].to_string().contains("minimum")
1656 && errors[0].to_string().contains("is after maximum")
1657 );
1658 }
1659
1660 #[test]
1661 fn validate_date_valid_range() {
1662 let mut specs = TypeSpecification::date();
1663 specs = specs
1664 .apply_constraint(
1665 TypeConstraintCommand::Minimum,
1666 &[CommandArg::Label("2024-01-01".to_string())],
1667 )
1668 .unwrap();
1669 specs = specs
1670 .apply_constraint(
1671 TypeConstraintCommand::Maximum,
1672 &[CommandArg::Label("2024-12-31".to_string())],
1673 )
1674 .unwrap();
1675
1676 let src = test_source();
1677 let errors = validate_type_specifications(&specs, "test", &src, None);
1678 assert!(errors.is_empty());
1679 }
1680
1681 #[test]
1682 fn validate_time_minimum_after_maximum() {
1683 let mut specs = TypeSpecification::time();
1684 specs = specs
1685 .apply_constraint(
1686 TypeConstraintCommand::Minimum,
1687 &[CommandArg::Label("23:00:00".to_string())],
1688 )
1689 .unwrap();
1690 specs = specs
1691 .apply_constraint(
1692 TypeConstraintCommand::Maximum,
1693 &[CommandArg::Label("10:00:00".to_string())],
1694 )
1695 .unwrap();
1696
1697 let src = test_source();
1698 let errors = validate_type_specifications(&specs, "test", &src, None);
1699 assert_eq!(errors.len(), 1);
1700 assert!(
1701 errors[0].to_string().contains("minimum")
1702 && errors[0].to_string().contains("is after maximum")
1703 );
1704 }
1705
1706 #[test]
1707 fn validate_type_definition_with_invalid_constraints() {
1708 use crate::engine::Context;
1712 use crate::parsing::ast::{LemmaSpec, ParentType, PrimitiveKind, TypeDef};
1713 use crate::planning::types::PerSliceTypeResolver;
1714 use std::sync::Arc;
1715
1716 let spec = Arc::new(LemmaSpec::new("test".to_string()));
1717 let mut ctx = Context::new();
1718 ctx.insert_spec(Arc::clone(&spec), false)
1719 .expect("insert test spec");
1720 let type_def = TypeDef::Regular {
1721 source_location: crate::Source::new(
1722 "<test>",
1723 crate::parsing::ast::Span {
1724 start: 0,
1725 end: 0,
1726 line: 1,
1727 col: 0,
1728 },
1729 ),
1730 name: "invalid_money".to_string(),
1731 parent: ParentType::Primitive {
1732 primitive: PrimitiveKind::Number,
1733 },
1734 constraints: Some(vec![
1735 (
1736 TypeConstraintCommand::Minimum,
1737 vec![CommandArg::Number("100".to_string())],
1738 ),
1739 (
1740 TypeConstraintCommand::Maximum,
1741 vec![CommandArg::Number("50".to_string())],
1742 ),
1743 ]),
1744 };
1745
1746 let plan_hashes = crate::planning::PlanHashRegistry::default();
1747 let mut type_resolver = PerSliceTypeResolver::new(&ctx, None, &plan_hashes);
1748 type_resolver
1749 .register_type(&spec, type_def)
1750 .expect("Should register type");
1751 let resolved_types = type_resolver
1752 .resolve_named_types(&spec)
1753 .expect("Should resolve types");
1754
1755 let lemma_type = resolved_types
1757 .named_types
1758 .get("invalid_money")
1759 .expect("Should have invalid_money type");
1760 let src = test_source();
1761 let errors =
1762 validate_type_specifications(&lemma_type.specifications, "invalid_money", &src, None);
1763 assert!(!errors.is_empty());
1764 assert!(errors.iter().any(|e| e
1765 .to_string()
1766 .contains("minimum 100 is greater than maximum 50")));
1767 }
1768
1769 fn lt(spec: TypeSpecification) -> LemmaType {
1770 LemmaType::primitive(spec)
1771 }
1772
1773 #[test]
1774 fn interface_requirement_matrix_all_types_base_checks() {
1775 let bool_t = lt(TypeSpecification::boolean());
1776 let num_t = lt(TypeSpecification::number());
1777 let scale_t = lt(TypeSpecification::Scale {
1778 minimum: None,
1779 maximum: None,
1780 decimals: None,
1781 precision: None,
1782 units: ScaleUnits::from(vec![ScaleUnit {
1783 name: "eur".to_string(),
1784 value: Decimal::ONE,
1785 }]),
1786 help: String::new(),
1787 default: None,
1788 });
1789 let ratio_t = lt(TypeSpecification::Ratio {
1790 minimum: None,
1791 maximum: None,
1792 decimals: None,
1793 units: RatioUnits::from(vec![RatioUnit {
1794 name: "percent".to_string(),
1795 value: Decimal::from(100),
1796 }]),
1797 help: String::new(),
1798 default: None,
1799 });
1800 let text_t = lt(TypeSpecification::text());
1801 let date_t = lt(TypeSpecification::date());
1802 let time_t = lt(TypeSpecification::time());
1803 let duration_t = lt(TypeSpecification::duration());
1804 let veto_t = LemmaType::veto_type();
1805 let undetermined_t = LemmaType::undetermined_type();
1806
1807 assert!(rule_type_satisfies_requirement(
1808 &bool_t,
1809 &RuleRefRequirement::Base(BaseTypeRequirement::Boolean)
1810 ));
1811 assert!(rule_type_satisfies_requirement(
1812 &num_t,
1813 &RuleRefRequirement::Base(BaseTypeRequirement::Number)
1814 ));
1815 assert!(rule_type_satisfies_requirement(
1816 &scale_t,
1817 &RuleRefRequirement::Base(BaseTypeRequirement::Scale)
1818 ));
1819 assert!(rule_type_satisfies_requirement(
1820 &ratio_t,
1821 &RuleRefRequirement::Base(BaseTypeRequirement::Ratio)
1822 ));
1823 assert!(rule_type_satisfies_requirement(
1824 &text_t,
1825 &RuleRefRequirement::Base(BaseTypeRequirement::Text)
1826 ));
1827 assert!(rule_type_satisfies_requirement(
1828 &date_t,
1829 &RuleRefRequirement::Base(BaseTypeRequirement::Date)
1830 ));
1831 assert!(rule_type_satisfies_requirement(
1832 &time_t,
1833 &RuleRefRequirement::Base(BaseTypeRequirement::Time)
1834 ));
1835 assert!(rule_type_satisfies_requirement(
1836 &duration_t,
1837 &RuleRefRequirement::Base(BaseTypeRequirement::Duration)
1838 ));
1839
1840 assert!(!rule_type_satisfies_requirement(
1841 &num_t,
1842 &RuleRefRequirement::Base(BaseTypeRequirement::Boolean)
1843 ));
1844 assert!(!rule_type_satisfies_requirement(
1845 &scale_t,
1846 &RuleRefRequirement::Base(BaseTypeRequirement::Number)
1847 ));
1848 assert!(rule_type_satisfies_requirement(
1850 &veto_t,
1851 &RuleRefRequirement::Base(BaseTypeRequirement::Any)
1852 ));
1853 assert!(
1854 std::panic::catch_unwind(|| {
1855 rule_type_satisfies_requirement(
1856 &undetermined_t,
1857 &RuleRefRequirement::Base(BaseTypeRequirement::Any),
1858 )
1859 })
1860 .is_err(),
1861 "should panic when rule_type_satisfies_requirement is called with undetermined type"
1862 );
1863 }
1864
1865 #[test]
1866 fn interface_requirement_matrix_unit_family_and_bounds_checks() {
1867 let money = LemmaType::new(
1868 "money".to_string(),
1869 TypeSpecification::Scale {
1870 minimum: Some(Decimal::ZERO),
1871 maximum: Some(Decimal::from(100)),
1872 decimals: None,
1873 precision: None,
1874 units: ScaleUnits::from(vec![
1875 ScaleUnit {
1876 name: "eur".to_string(),
1877 value: Decimal::ONE,
1878 },
1879 ScaleUnit {
1880 name: "usd".to_string(),
1881 value: Decimal::new(11, 1),
1882 },
1883 ]),
1884 help: String::new(),
1885 default: None,
1886 },
1887 crate::planning::semantics::TypeExtends::Primitive,
1888 );
1889 let weight = LemmaType::new(
1890 "weight".to_string(),
1891 TypeSpecification::Scale {
1892 minimum: None,
1893 maximum: None,
1894 decimals: None,
1895 precision: None,
1896 units: ScaleUnits::from(vec![ScaleUnit {
1897 name: "kg".to_string(),
1898 value: Decimal::ONE,
1899 }]),
1900 help: String::new(),
1901 default: None,
1902 },
1903 crate::planning::semantics::TypeExtends::Primitive,
1904 );
1905 let ratio = lt(TypeSpecification::Ratio {
1906 minimum: Some(Decimal::ZERO),
1907 maximum: Some(Decimal::from(100)),
1908 decimals: None,
1909 units: RatioUnits::from(vec![RatioUnit {
1910 name: "percent".to_string(),
1911 value: Decimal::from(100),
1912 }]),
1913 help: String::new(),
1914 default: None,
1915 });
1916 let bounded_number = lt(TypeSpecification::Number {
1917 minimum: Some(Decimal::ZERO),
1918 maximum: Some(Decimal::from(100)),
1919 decimals: None,
1920 precision: None,
1921 help: String::new(),
1922 default: None,
1923 });
1924
1925 assert!(rule_type_satisfies_requirement(
1926 &money,
1927 &RuleRefRequirement::ScaleMustContainUnit("eur".to_string())
1928 ));
1929 assert!(!rule_type_satisfies_requirement(
1930 &money,
1931 &RuleRefRequirement::ScaleMustContainUnit("gbp".to_string())
1932 ));
1933 assert!(rule_type_satisfies_requirement(
1934 &ratio,
1935 &RuleRefRequirement::RatioMustContainUnit("percent".to_string())
1936 ));
1937 assert!(!rule_type_satisfies_requirement(
1938 &ratio,
1939 &RuleRefRequirement::RatioMustContainUnit("permille".to_string())
1940 ));
1941 assert!(rule_type_satisfies_requirement(
1942 &money,
1943 &RuleRefRequirement::SameScaleFamilyAs(money.clone())
1944 ));
1945 assert!(!rule_type_satisfies_requirement(
1946 &money,
1947 &RuleRefRequirement::SameScaleFamilyAs(weight)
1948 ));
1949
1950 assert!(rule_type_satisfies_requirement(
1951 &bounded_number,
1952 &RuleRefRequirement::NumericLiteral(NumericLiteralConstraint {
1953 op: ComparisonComputation::GreaterThan,
1954 literal: Decimal::from(50),
1955 reference_on_left: true,
1956 })
1957 ));
1958 assert!(!rule_type_satisfies_requirement(
1959 &bounded_number,
1960 &RuleRefRequirement::NumericLiteral(NumericLiteralConstraint {
1961 op: ComparisonComputation::GreaterThan,
1962 literal: Decimal::from(500),
1963 reference_on_left: true,
1964 })
1965 ));
1966 }
1967}