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::Is => ComparisonComputation::Is,
873 ComparisonComputation::IsNot => ComparisonComputation::IsNot,
874 };
875 NumericLiteralConstraint {
876 op,
877 literal: rule.literal,
878 reference_on_left: true,
879 }
880}
881
882fn numeric_literal_constraint_satisfied(
883 lemma_type: &LemmaType,
884 rule: NumericLiteralConstraint,
885) -> bool {
886 let Some((minimum, maximum)) = numeric_bounds(lemma_type) else {
887 return false;
888 };
889 let normalized = normalize_literal_constraint(rule);
890 match normalized.op {
891 ComparisonComputation::GreaterThan => maximum.is_none_or(|max| max > normalized.literal),
892 ComparisonComputation::GreaterThanOrEqual => {
893 maximum.is_none_or(|max| max >= normalized.literal)
894 }
895 ComparisonComputation::LessThan => minimum.is_none_or(|min| min < normalized.literal),
896 ComparisonComputation::LessThanOrEqual => {
897 minimum.is_none_or(|min| min <= normalized.literal)
898 }
899 ComparisonComputation::Is => {
900 minimum.is_none_or(|min| min <= normalized.literal)
901 && maximum.is_none_or(|max| max >= normalized.literal)
902 }
903 ComparisonComputation::IsNot => {
904 !(minimum == Some(normalized.literal) && maximum == Some(normalized.literal))
905 }
906 }
907}
908
909fn rule_type_satisfies_requirement(
910 lemma_type: &LemmaType,
911 requirement: &RuleRefRequirement,
912) -> bool {
913 if lemma_type.is_undetermined() {
914 unreachable!("BUG: rule_type_satisfies_requirement called with undetermined type; this type exists only during type inference")
915 }
916 if lemma_type.vetoed() {
918 return true;
919 }
920 match requirement {
921 RuleRefRequirement::Base(kind) => base_requirement_satisfied(lemma_type, *kind),
922 RuleRefRequirement::ScaleMustContainUnit(unit) => {
923 lemma_type.is_scale() && has_scale_unit(lemma_type, unit)
924 }
925 RuleRefRequirement::RatioMustContainUnit(unit) => {
926 lemma_type.is_ratio() && has_ratio_unit(lemma_type, unit)
927 }
928 RuleRefRequirement::SameBaseAs(other) => lemma_type.has_same_base_type(other),
929 RuleRefRequirement::SameScaleFamilyAs(other) => {
930 lemma_type.is_scale() && other.is_scale() && lemma_type.same_scale_family(other)
931 }
932 RuleRefRequirement::ArithmeticCompatibleWithNumber => {
933 lemma_type.is_number() || lemma_type.is_ratio()
934 }
935 RuleRefRequirement::ArithmeticCompatibleWithRatio => {
936 lemma_type.is_number()
937 || lemma_type.is_ratio()
938 || lemma_type.is_scale()
939 || lemma_type.is_duration()
940 }
941 RuleRefRequirement::ArithmeticCompatibleWithScale(other) => {
942 lemma_type.is_number()
943 || lemma_type.is_ratio()
944 || (lemma_type.is_scale()
945 && other.is_scale()
946 && lemma_type.same_scale_family(other))
947 }
948 RuleRefRequirement::ArithmeticCompatibleWithDuration => {
949 lemma_type.is_number() || lemma_type.is_ratio() || lemma_type.is_duration()
950 }
951 RuleRefRequirement::NumericLiteral(rule) => {
952 numeric_literal_constraint_satisfied(lemma_type, rule.clone())
953 }
954 }
955}
956
957fn infer_interface_expression_type(
958 expr: &Expression,
959 rule_entries: &IndexMap<RulePath, RuleEntryForBindingCheck>,
960 facts: &IndexMap<FactPath, FactData>,
961) -> Option<LemmaType> {
962 match &expr.kind {
963 ExpressionKind::Literal(lv) => Some(lv.lemma_type.clone()),
964 ExpressionKind::FactPath(fp) => facts.get(fp).and_then(|f| f.schema_type().cloned()),
965 ExpressionKind::RulePath(rp) => rule_entries.get(rp).map(|r| r.rule_type.clone()),
966 _ => None,
967 }
968}
969
970fn numeric_literal_from_expression(expr: &Expression) -> Option<Decimal> {
971 let ExpressionKind::Literal(lv) = &expr.kind else {
972 return None;
973 };
974 match &lv.value {
975 ValueKind::Number(n) => Some(*n),
976 _ => None,
977 }
978}
979
980fn collect_expected_requirements_for_rule_ref(
981 expr: &Expression,
982 rule_path: &RulePath,
983 expected: RuleRefRequirement,
984 rule_entries: &IndexMap<RulePath, RuleEntryForBindingCheck>,
985 facts: &IndexMap<FactPath, FactData>,
986) -> Vec<(Option<Source>, RuleRefRequirement)> {
987 let mut out = Vec::new();
988 match &expr.kind {
989 ExpressionKind::RulePath(rp) => {
990 if rp == rule_path {
991 out.push((expr.source_location.clone(), expected));
992 }
993 }
994 ExpressionKind::LogicalAnd(left, right) => {
995 out.extend(collect_expected_requirements_for_rule_ref(
996 left,
997 rule_path,
998 RuleRefRequirement::Base(BaseTypeRequirement::Boolean),
999 rule_entries,
1000 facts,
1001 ));
1002 out.extend(collect_expected_requirements_for_rule_ref(
1003 right,
1004 rule_path,
1005 RuleRefRequirement::Base(BaseTypeRequirement::Boolean),
1006 rule_entries,
1007 facts,
1008 ));
1009 }
1010 ExpressionKind::LogicalNegation(operand, _) => {
1011 out.extend(collect_expected_requirements_for_rule_ref(
1012 operand,
1013 rule_path,
1014 RuleRefRequirement::Base(BaseTypeRequirement::Boolean),
1015 rule_entries,
1016 facts,
1017 ));
1018 }
1019 ExpressionKind::Comparison(left, op, right) => {
1020 out.extend(collect_expected_requirements_for_rule_ref(
1021 left,
1022 rule_path,
1023 RuleRefRequirement::Base(BaseTypeRequirement::Comparable),
1024 rule_entries,
1025 facts,
1026 ));
1027 out.extend(collect_expected_requirements_for_rule_ref(
1028 right,
1029 rule_path,
1030 RuleRefRequirement::Base(BaseTypeRequirement::Comparable),
1031 rule_entries,
1032 facts,
1033 ));
1034
1035 if let ExpressionKind::RulePath(rp) = &left.kind {
1036 if rp == rule_path {
1037 if let Some(other_type) =
1038 infer_interface_expression_type(right, rule_entries, facts)
1039 {
1040 out.push((
1041 expr.source_location.clone(),
1042 RuleRefRequirement::SameBaseAs(other_type.clone()),
1043 ));
1044 if other_type.is_scale() {
1045 out.push((
1046 expr.source_location.clone(),
1047 RuleRefRequirement::SameScaleFamilyAs(other_type),
1048 ));
1049 }
1050 }
1051 if let Some(lit) = numeric_literal_from_expression(right) {
1052 out.push((
1053 expr.source_location.clone(),
1054 RuleRefRequirement::NumericLiteral(NumericLiteralConstraint {
1055 op: op.clone(),
1056 literal: lit,
1057 reference_on_left: true,
1058 }),
1059 ));
1060 }
1061 }
1062 }
1063 if let ExpressionKind::RulePath(rp) = &right.kind {
1064 if rp == rule_path {
1065 if let Some(other_type) =
1066 infer_interface_expression_type(left, rule_entries, facts)
1067 {
1068 out.push((
1069 expr.source_location.clone(),
1070 RuleRefRequirement::SameBaseAs(other_type.clone()),
1071 ));
1072 if other_type.is_scale() {
1073 out.push((
1074 expr.source_location.clone(),
1075 RuleRefRequirement::SameScaleFamilyAs(other_type),
1076 ));
1077 }
1078 }
1079 if let Some(lit) = numeric_literal_from_expression(left) {
1080 out.push((
1081 expr.source_location.clone(),
1082 RuleRefRequirement::NumericLiteral(NumericLiteralConstraint {
1083 op: op.clone(),
1084 literal: lit,
1085 reference_on_left: false,
1086 }),
1087 ));
1088 }
1089 }
1090 }
1091 }
1092 ExpressionKind::Arithmetic(left, _, right) => {
1093 out.extend(collect_expected_requirements_for_rule_ref(
1094 left,
1095 rule_path,
1096 RuleRefRequirement::Base(BaseTypeRequirement::Numeric),
1097 rule_entries,
1098 facts,
1099 ));
1100 out.extend(collect_expected_requirements_for_rule_ref(
1101 right,
1102 rule_path,
1103 RuleRefRequirement::Base(BaseTypeRequirement::Numeric),
1104 rule_entries,
1105 facts,
1106 ));
1107
1108 if let ExpressionKind::RulePath(rp) = &left.kind {
1109 if rp == rule_path {
1110 if let Some(other_type) =
1111 infer_interface_expression_type(right, rule_entries, facts)
1112 {
1113 if other_type.is_scale() {
1114 out.push((
1115 expr.source_location.clone(),
1116 RuleRefRequirement::ArithmeticCompatibleWithScale(other_type),
1117 ));
1118 } else if other_type.is_number() || other_type.is_ratio() {
1119 out.push((
1120 expr.source_location.clone(),
1121 if other_type.is_number() {
1122 RuleRefRequirement::ArithmeticCompatibleWithNumber
1123 } else {
1124 RuleRefRequirement::ArithmeticCompatibleWithRatio
1125 },
1126 ));
1127 } else if other_type.is_duration() {
1128 out.push((
1129 expr.source_location.clone(),
1130 RuleRefRequirement::ArithmeticCompatibleWithDuration,
1131 ));
1132 }
1133 }
1134 }
1135 }
1136 if let ExpressionKind::RulePath(rp) = &right.kind {
1137 if rp == rule_path {
1138 if let Some(other_type) =
1139 infer_interface_expression_type(left, rule_entries, facts)
1140 {
1141 if other_type.is_scale() {
1142 out.push((
1143 expr.source_location.clone(),
1144 RuleRefRequirement::ArithmeticCompatibleWithScale(other_type),
1145 ));
1146 } else if other_type.is_number() || other_type.is_ratio() {
1147 out.push((
1148 expr.source_location.clone(),
1149 if other_type.is_number() {
1150 RuleRefRequirement::ArithmeticCompatibleWithNumber
1151 } else {
1152 RuleRefRequirement::ArithmeticCompatibleWithRatio
1153 },
1154 ));
1155 } else if other_type.is_duration() {
1156 out.push((
1157 expr.source_location.clone(),
1158 RuleRefRequirement::ArithmeticCompatibleWithDuration,
1159 ));
1160 }
1161 }
1162 }
1163 }
1164 }
1165 ExpressionKind::UnitConversion(source, target) => {
1166 let constraint = match target {
1167 SemanticConversionTarget::Duration(_) => {
1168 RuleRefRequirement::Base(BaseTypeRequirement::Duration)
1169 }
1170 SemanticConversionTarget::ScaleUnit(unit) => {
1171 RuleRefRequirement::ScaleMustContainUnit(unit.clone())
1172 }
1173 SemanticConversionTarget::RatioUnit(unit) => {
1174 RuleRefRequirement::RatioMustContainUnit(unit.clone())
1175 }
1176 };
1177 out.extend(collect_expected_requirements_for_rule_ref(
1178 source,
1179 rule_path,
1180 constraint,
1181 rule_entries,
1182 facts,
1183 ));
1184 }
1185 ExpressionKind::MathematicalComputation(_, operand) => {
1186 out.extend(collect_expected_requirements_for_rule_ref(
1187 operand,
1188 rule_path,
1189 RuleRefRequirement::Base(BaseTypeRequirement::Number),
1190 rule_entries,
1191 facts,
1192 ));
1193 }
1194 ExpressionKind::DateRelative(_, date_expr, tolerance) => {
1195 out.extend(collect_expected_requirements_for_rule_ref(
1196 date_expr,
1197 rule_path,
1198 RuleRefRequirement::Base(BaseTypeRequirement::Date),
1199 rule_entries,
1200 facts,
1201 ));
1202 if let Some(tol) = tolerance {
1203 out.extend(collect_expected_requirements_for_rule_ref(
1204 tol,
1205 rule_path,
1206 RuleRefRequirement::Base(BaseTypeRequirement::Duration),
1207 rule_entries,
1208 facts,
1209 ));
1210 }
1211 }
1212 ExpressionKind::DateCalendar(_, _, date_expr) => {
1213 out.extend(collect_expected_requirements_for_rule_ref(
1214 date_expr,
1215 rule_path,
1216 RuleRefRequirement::Base(BaseTypeRequirement::Date),
1217 rule_entries,
1218 facts,
1219 ));
1220 }
1221 ExpressionKind::Literal(_)
1222 | ExpressionKind::FactPath(_)
1223 | ExpressionKind::Veto(_)
1224 | ExpressionKind::Now => {}
1225 }
1226 out
1227}
1228
1229fn spec_interface_error(
1230 source: &Source,
1231 message: impl Into<String>,
1232 spec_context: Option<Arc<LemmaSpec>>,
1233 related_spec: Option<Arc<LemmaSpec>>,
1234) -> Error {
1235 Error::validation_with_context(
1236 message.into(),
1237 Some(source.clone()),
1238 None::<String>,
1239 spec_context,
1240 related_spec,
1241 )
1242}
1243
1244pub fn validate_spec_interfaces(
1254 referenced_rules: &HashMap<Vec<String>, HashSet<String>>,
1255 spec_ref_facts: &[(FactPath, Arc<LemmaSpec>, Source)],
1256 facts: &IndexMap<FactPath, FactData>,
1257 rule_entries: &IndexMap<RulePath, RuleEntryForBindingCheck>,
1258 main_spec: &Arc<LemmaSpec>,
1259) -> Result<(), Vec<Error>> {
1260 let mut errors = Vec::new();
1261
1262 for (fact_path, spec_arc, fact_source) in spec_ref_facts {
1263 let mut full_path: Vec<String> =
1264 fact_path.segments.iter().map(|s| s.fact.clone()).collect();
1265 full_path.push(fact_path.fact.clone());
1266
1267 let Some(required_rules) = referenced_rules.get(&full_path) else {
1268 continue;
1269 };
1270
1271 let spec = spec_arc.as_ref();
1272 let spec_rule_names: HashSet<&str> = spec.rules.iter().map(|r| r.name.as_str()).collect();
1273
1274 for required_rule in required_rules {
1275 if !spec_rule_names.contains(required_rule.as_str()) {
1276 errors.push(spec_interface_error(
1277 fact_source,
1278 format!(
1279 "Spec '{}' referenced by '{}' is missing required rule '{}'",
1280 spec.name, fact_path, required_rule
1281 ),
1282 Some(Arc::clone(main_spec)),
1283 Some(Arc::clone(spec_arc)),
1284 ));
1285 continue;
1286 }
1287
1288 let mut ref_segments = fact_path.segments.clone();
1289 ref_segments.push(crate::planning::semantics::PathSegment {
1290 fact: fact_path.fact.clone(),
1291 spec: spec.name.clone(),
1292 });
1293 let ref_rule_path = RulePath::new(ref_segments, required_rule.clone());
1294 let Some(ref_entry) = rule_entries.get(&ref_rule_path) else {
1295 let binding_path_str = fact_path
1296 .segments
1297 .iter()
1298 .map(|s| s.fact.as_str())
1299 .collect::<Vec<_>>()
1300 .join(".");
1301 let binding_path_str = if binding_path_str.is_empty() {
1302 fact_path.fact.clone()
1303 } else {
1304 format!("{}.{}", binding_path_str, fact_path.fact)
1305 };
1306 errors.push(spec_interface_error(
1307 fact_source,
1308 format!(
1309 "Fact binding '{}' sets spec reference to '{}', but interface validation could not resolve rule path '{}.{}' for contract checking",
1310 binding_path_str, spec.name, fact_path.fact, required_rule
1311 ),
1312 Some(Arc::clone(main_spec)),
1313 Some(Arc::clone(spec_arc)),
1314 ));
1315 continue;
1316 };
1317 let ref_rule_type = &ref_entry.rule_type;
1318
1319 for (_referencing_path, entry) in rule_entries {
1320 if !entry.depends_on_rules.contains(&ref_rule_path) {
1321 continue;
1322 }
1323 let expected =
1324 RuleRefRequirement::Base(lemma_type_to_base_requirement(&entry.rule_type));
1325 for (_condition, result_expr) in &entry.branches {
1326 let requirements = collect_expected_requirements_for_rule_ref(
1327 result_expr,
1328 &ref_rule_path,
1329 expected.clone(),
1330 rule_entries,
1331 facts,
1332 );
1333 for (_source, requirement) in requirements {
1334 if !rule_type_satisfies_requirement(ref_rule_type, &requirement) {
1335 let report_source = fact_source;
1336
1337 let binding_path_str = fact_path
1338 .segments
1339 .iter()
1340 .map(|s| s.fact.as_str())
1341 .collect::<Vec<_>>()
1342 .join(".");
1343 let binding_path_str = if binding_path_str.is_empty() {
1344 fact_path.fact.clone()
1345 } else {
1346 format!("{}.{}", binding_path_str, fact_path.fact)
1347 };
1348
1349 errors.push(spec_interface_error(
1350 report_source,
1351 format!(
1352 "Fact binding '{}' sets spec reference to '{}', but that spec's rule '{}' has result type {}; the referencing expression expects a {} value",
1353 binding_path_str,
1354 spec.name,
1355 required_rule,
1356 ref_rule_type.name(),
1357 requirement.describe(),
1358 ),
1359 Some(Arc::clone(main_spec)),
1360 Some(Arc::clone(spec_arc)),
1361 ));
1362 }
1363 }
1364 }
1365 }
1366 }
1367 }
1368
1369 if errors.is_empty() {
1370 Ok(())
1371 } else {
1372 Err(errors)
1373 }
1374}
1375
1376pub fn collect_bare_registry_refs(spec: &LemmaSpec) -> Vec<String> {
1382 if !spec.from_registry {
1383 return Vec::new();
1384 }
1385 let mut bare: Vec<String> = Vec::new();
1386 for fact in &spec.facts {
1387 match &fact.value {
1388 FactValue::SpecReference(r) if !r.from_registry => {
1389 bare.push(r.name.clone());
1390 }
1391 FactValue::TypeDeclaration { from: Some(r), .. } if !r.from_registry => {
1392 bare.push(r.name.clone());
1393 }
1394 _ => {}
1395 }
1396 }
1397 for type_def in &spec.types {
1398 match type_def {
1399 TypeDef::Import { from, .. } if !from.from_registry => {
1400 bare.push(from.name.clone());
1401 }
1402 TypeDef::Inline { from: Some(r), .. } if !r.from_registry => {
1403 bare.push(r.name.clone());
1404 }
1405 _ => {}
1406 }
1407 }
1408 bare
1409}
1410
1411#[cfg(test)]
1412mod tests {
1413 use super::*;
1414 use crate::parsing::ast::{CommandArg, TypeConstraintCommand};
1415 use crate::planning::semantics::{
1416 LemmaType, RatioUnit, RatioUnits, ScaleUnit, ScaleUnits, TypeSpecification,
1417 };
1418 use rust_decimal::Decimal;
1419
1420 fn test_source() -> Source {
1421 Source::new(
1422 "<test>",
1423 crate::parsing::ast::Span {
1424 start: 0,
1425 end: 0,
1426 line: 1,
1427 col: 0,
1428 },
1429 )
1430 }
1431
1432 #[test]
1433 fn validate_number_minimum_greater_than_maximum() {
1434 let mut specs = TypeSpecification::number();
1435 specs = specs
1436 .apply_constraint(
1437 TypeConstraintCommand::Minimum,
1438 &[CommandArg::Number("100".to_string())],
1439 )
1440 .unwrap();
1441 specs = specs
1442 .apply_constraint(
1443 TypeConstraintCommand::Maximum,
1444 &[CommandArg::Number("50".to_string())],
1445 )
1446 .unwrap();
1447
1448 let src = test_source();
1449 let errors = validate_type_specifications(&specs, "test", &src, None);
1450 assert_eq!(errors.len(), 1);
1451 assert!(errors[0]
1452 .to_string()
1453 .contains("minimum 100 is greater than maximum 50"));
1454 }
1455
1456 #[test]
1457 fn validate_number_valid_range() {
1458 let mut specs = TypeSpecification::number();
1459 specs = specs
1460 .apply_constraint(
1461 TypeConstraintCommand::Minimum,
1462 &[CommandArg::Number("0".to_string())],
1463 )
1464 .unwrap();
1465 specs = specs
1466 .apply_constraint(
1467 TypeConstraintCommand::Maximum,
1468 &[CommandArg::Number("100".to_string())],
1469 )
1470 .unwrap();
1471
1472 let src = test_source();
1473 let errors = validate_type_specifications(&specs, "test", &src, None);
1474 assert!(errors.is_empty());
1475 }
1476
1477 #[test]
1478 fn validate_number_default_below_minimum() {
1479 let specs = TypeSpecification::Number {
1480 minimum: Some(Decimal::from(10)),
1481 maximum: None,
1482 decimals: None,
1483 precision: None,
1484 help: String::new(),
1485 default: Some(Decimal::from(5)),
1486 };
1487
1488 let src = test_source();
1489 let errors = validate_type_specifications(&specs, "test", &src, None);
1490 assert_eq!(errors.len(), 1);
1491 assert!(errors[0]
1492 .to_string()
1493 .contains("default value 5 is less than minimum 10"));
1494 }
1495
1496 #[test]
1497 fn validate_number_default_above_maximum() {
1498 let specs = TypeSpecification::Number {
1499 minimum: None,
1500 maximum: Some(Decimal::from(100)),
1501 decimals: None,
1502 precision: None,
1503 help: String::new(),
1504 default: Some(Decimal::from(150)),
1505 };
1506
1507 let src = test_source();
1508 let errors = validate_type_specifications(&specs, "test", &src, None);
1509 assert_eq!(errors.len(), 1);
1510 assert!(errors[0]
1511 .to_string()
1512 .contains("default value 150 is greater than maximum 100"));
1513 }
1514
1515 #[test]
1516 fn validate_number_default_valid() {
1517 let specs = TypeSpecification::Number {
1518 minimum: Some(Decimal::from(0)),
1519 maximum: Some(Decimal::from(100)),
1520 decimals: None,
1521 precision: None,
1522 help: String::new(),
1523 default: Some(Decimal::from(50)),
1524 };
1525
1526 let src = test_source();
1527 let errors = validate_type_specifications(&specs, "test", &src, None);
1528 assert!(errors.is_empty());
1529 }
1530
1531 #[test]
1532 fn validate_text_minimum_greater_than_maximum() {
1533 let mut specs = TypeSpecification::text();
1534 specs = specs
1535 .apply_constraint(
1536 TypeConstraintCommand::Minimum,
1537 &[CommandArg::Number("100".to_string())],
1538 )
1539 .unwrap();
1540 specs = specs
1541 .apply_constraint(
1542 TypeConstraintCommand::Maximum,
1543 &[CommandArg::Number("50".to_string())],
1544 )
1545 .unwrap();
1546
1547 let src = test_source();
1548 let errors = validate_type_specifications(&specs, "test", &src, None);
1549 assert_eq!(errors.len(), 1);
1550 assert!(errors[0]
1551 .to_string()
1552 .contains("minimum length 100 is greater than maximum length 50"));
1553 }
1554
1555 #[test]
1556 fn validate_text_length_inconsistent_with_minimum() {
1557 let mut specs = TypeSpecification::text();
1558 specs = specs
1559 .apply_constraint(
1560 TypeConstraintCommand::Minimum,
1561 &[CommandArg::Number("10".to_string())],
1562 )
1563 .unwrap();
1564 specs = specs
1565 .apply_constraint(
1566 TypeConstraintCommand::Length,
1567 &[CommandArg::Number("5".to_string())],
1568 )
1569 .unwrap();
1570
1571 let src = test_source();
1572 let errors = validate_type_specifications(&specs, "test", &src, None);
1573 assert_eq!(errors.len(), 1);
1574 assert!(errors[0]
1575 .to_string()
1576 .contains("length 5 is less than minimum 10"));
1577 }
1578
1579 #[test]
1580 fn validate_text_default_not_in_options() {
1581 let specs = TypeSpecification::Text {
1582 minimum: None,
1583 maximum: None,
1584 length: None,
1585 options: vec!["red".to_string(), "blue".to_string()],
1586 help: String::new(),
1587 default: Some("green".to_string()),
1588 };
1589
1590 let src = test_source();
1591 let errors = validate_type_specifications(&specs, "test", &src, None);
1592 assert_eq!(errors.len(), 1);
1593 assert!(errors[0]
1594 .to_string()
1595 .contains("default value 'green' is not in allowed options"));
1596 }
1597
1598 #[test]
1599 fn validate_text_default_valid_in_options() {
1600 let specs = TypeSpecification::Text {
1601 minimum: None,
1602 maximum: None,
1603 length: None,
1604 options: vec!["red".to_string(), "blue".to_string()],
1605 help: String::new(),
1606 default: Some("red".to_string()),
1607 };
1608
1609 let src = test_source();
1610 let errors = validate_type_specifications(&specs, "test", &src, None);
1611 assert!(errors.is_empty());
1612 }
1613
1614 #[test]
1615 fn validate_ratio_minimum_greater_than_maximum() {
1616 let specs = TypeSpecification::Ratio {
1617 minimum: Some(Decimal::from(2)),
1618 maximum: Some(Decimal::from(1)),
1619 decimals: None,
1620 units: crate::planning::semantics::RatioUnits::new(),
1621 help: String::new(),
1622 default: None,
1623 };
1624
1625 let src = test_source();
1626 let errors = validate_type_specifications(&specs, "test", &src, None);
1627 assert_eq!(errors.len(), 1);
1628 assert!(errors[0]
1629 .to_string()
1630 .contains("minimum 2 is greater than maximum 1"));
1631 }
1632
1633 #[test]
1634 fn validate_date_minimum_after_maximum() {
1635 let mut specs = TypeSpecification::date();
1636 specs = specs
1637 .apply_constraint(
1638 TypeConstraintCommand::Minimum,
1639 &[CommandArg::Label("2024-12-31".to_string())],
1640 )
1641 .unwrap();
1642 specs = specs
1643 .apply_constraint(
1644 TypeConstraintCommand::Maximum,
1645 &[CommandArg::Label("2024-01-01".to_string())],
1646 )
1647 .unwrap();
1648
1649 let src = test_source();
1650 let errors = validate_type_specifications(&specs, "test", &src, None);
1651 assert_eq!(errors.len(), 1);
1652 assert!(
1653 errors[0].to_string().contains("minimum")
1654 && errors[0].to_string().contains("is after maximum")
1655 );
1656 }
1657
1658 #[test]
1659 fn validate_date_valid_range() {
1660 let mut specs = TypeSpecification::date();
1661 specs = specs
1662 .apply_constraint(
1663 TypeConstraintCommand::Minimum,
1664 &[CommandArg::Label("2024-01-01".to_string())],
1665 )
1666 .unwrap();
1667 specs = specs
1668 .apply_constraint(
1669 TypeConstraintCommand::Maximum,
1670 &[CommandArg::Label("2024-12-31".to_string())],
1671 )
1672 .unwrap();
1673
1674 let src = test_source();
1675 let errors = validate_type_specifications(&specs, "test", &src, None);
1676 assert!(errors.is_empty());
1677 }
1678
1679 #[test]
1680 fn validate_time_minimum_after_maximum() {
1681 let mut specs = TypeSpecification::time();
1682 specs = specs
1683 .apply_constraint(
1684 TypeConstraintCommand::Minimum,
1685 &[CommandArg::Label("23:00:00".to_string())],
1686 )
1687 .unwrap();
1688 specs = specs
1689 .apply_constraint(
1690 TypeConstraintCommand::Maximum,
1691 &[CommandArg::Label("10:00:00".to_string())],
1692 )
1693 .unwrap();
1694
1695 let src = test_source();
1696 let errors = validate_type_specifications(&specs, "test", &src, None);
1697 assert_eq!(errors.len(), 1);
1698 assert!(
1699 errors[0].to_string().contains("minimum")
1700 && errors[0].to_string().contains("is after maximum")
1701 );
1702 }
1703
1704 #[test]
1705 fn validate_type_definition_with_invalid_constraints() {
1706 use crate::engine::Context;
1710 use crate::parsing::ast::{LemmaSpec, ParentType, PrimitiveKind, TypeDef};
1711 use crate::planning::types::PerSliceTypeResolver;
1712 use std::sync::Arc;
1713
1714 let spec = Arc::new(LemmaSpec::new("test".to_string()));
1715 let mut ctx = Context::new();
1716 ctx.insert_spec(Arc::clone(&spec), false)
1717 .expect("insert test spec");
1718 let type_def = TypeDef::Regular {
1719 source_location: crate::Source::new(
1720 "<test>",
1721 crate::parsing::ast::Span {
1722 start: 0,
1723 end: 0,
1724 line: 1,
1725 col: 0,
1726 },
1727 ),
1728 name: "invalid_money".to_string(),
1729 parent: ParentType::Primitive {
1730 primitive: PrimitiveKind::Number,
1731 },
1732 constraints: Some(vec![
1733 (
1734 TypeConstraintCommand::Minimum,
1735 vec![CommandArg::Number("100".to_string())],
1736 ),
1737 (
1738 TypeConstraintCommand::Maximum,
1739 vec![CommandArg::Number("50".to_string())],
1740 ),
1741 ]),
1742 };
1743
1744 let plan_hashes = crate::planning::PlanHashRegistry::default();
1745 let mut type_resolver = PerSliceTypeResolver::new(&ctx, None, &plan_hashes);
1746 type_resolver
1747 .register_type(&spec, type_def)
1748 .expect("Should register type");
1749 let resolved_types = type_resolver
1750 .resolve_named_types(&spec)
1751 .expect("Should resolve types");
1752
1753 let lemma_type = resolved_types
1755 .named_types
1756 .get("invalid_money")
1757 .expect("Should have invalid_money type");
1758 let src = test_source();
1759 let errors =
1760 validate_type_specifications(&lemma_type.specifications, "invalid_money", &src, None);
1761 assert!(!errors.is_empty());
1762 assert!(errors.iter().any(|e| e
1763 .to_string()
1764 .contains("minimum 100 is greater than maximum 50")));
1765 }
1766
1767 fn lt(spec: TypeSpecification) -> LemmaType {
1768 LemmaType::primitive(spec)
1769 }
1770
1771 #[test]
1772 fn interface_requirement_matrix_all_types_base_checks() {
1773 let bool_t = lt(TypeSpecification::boolean());
1774 let num_t = lt(TypeSpecification::number());
1775 let scale_t = lt(TypeSpecification::Scale {
1776 minimum: None,
1777 maximum: None,
1778 decimals: None,
1779 precision: None,
1780 units: ScaleUnits::from(vec![ScaleUnit {
1781 name: "eur".to_string(),
1782 value: Decimal::ONE,
1783 }]),
1784 help: String::new(),
1785 default: None,
1786 });
1787 let ratio_t = lt(TypeSpecification::Ratio {
1788 minimum: None,
1789 maximum: None,
1790 decimals: None,
1791 units: RatioUnits::from(vec![RatioUnit {
1792 name: "percent".to_string(),
1793 value: Decimal::from(100),
1794 }]),
1795 help: String::new(),
1796 default: None,
1797 });
1798 let text_t = lt(TypeSpecification::text());
1799 let date_t = lt(TypeSpecification::date());
1800 let time_t = lt(TypeSpecification::time());
1801 let duration_t = lt(TypeSpecification::duration());
1802 let veto_t = LemmaType::veto_type();
1803 let undetermined_t = LemmaType::undetermined_type();
1804
1805 assert!(rule_type_satisfies_requirement(
1806 &bool_t,
1807 &RuleRefRequirement::Base(BaseTypeRequirement::Boolean)
1808 ));
1809 assert!(rule_type_satisfies_requirement(
1810 &num_t,
1811 &RuleRefRequirement::Base(BaseTypeRequirement::Number)
1812 ));
1813 assert!(rule_type_satisfies_requirement(
1814 &scale_t,
1815 &RuleRefRequirement::Base(BaseTypeRequirement::Scale)
1816 ));
1817 assert!(rule_type_satisfies_requirement(
1818 &ratio_t,
1819 &RuleRefRequirement::Base(BaseTypeRequirement::Ratio)
1820 ));
1821 assert!(rule_type_satisfies_requirement(
1822 &text_t,
1823 &RuleRefRequirement::Base(BaseTypeRequirement::Text)
1824 ));
1825 assert!(rule_type_satisfies_requirement(
1826 &date_t,
1827 &RuleRefRequirement::Base(BaseTypeRequirement::Date)
1828 ));
1829 assert!(rule_type_satisfies_requirement(
1830 &time_t,
1831 &RuleRefRequirement::Base(BaseTypeRequirement::Time)
1832 ));
1833 assert!(rule_type_satisfies_requirement(
1834 &duration_t,
1835 &RuleRefRequirement::Base(BaseTypeRequirement::Duration)
1836 ));
1837
1838 assert!(!rule_type_satisfies_requirement(
1839 &num_t,
1840 &RuleRefRequirement::Base(BaseTypeRequirement::Boolean)
1841 ));
1842 assert!(!rule_type_satisfies_requirement(
1843 &scale_t,
1844 &RuleRefRequirement::Base(BaseTypeRequirement::Number)
1845 ));
1846 assert!(rule_type_satisfies_requirement(
1848 &veto_t,
1849 &RuleRefRequirement::Base(BaseTypeRequirement::Any)
1850 ));
1851 assert!(
1852 std::panic::catch_unwind(|| {
1853 rule_type_satisfies_requirement(
1854 &undetermined_t,
1855 &RuleRefRequirement::Base(BaseTypeRequirement::Any),
1856 )
1857 })
1858 .is_err(),
1859 "should panic when rule_type_satisfies_requirement is called with undetermined type"
1860 );
1861 }
1862
1863 #[test]
1864 fn interface_requirement_matrix_unit_family_and_bounds_checks() {
1865 let money = LemmaType::new(
1866 "money".to_string(),
1867 TypeSpecification::Scale {
1868 minimum: Some(Decimal::ZERO),
1869 maximum: Some(Decimal::from(100)),
1870 decimals: None,
1871 precision: None,
1872 units: ScaleUnits::from(vec![
1873 ScaleUnit {
1874 name: "eur".to_string(),
1875 value: Decimal::ONE,
1876 },
1877 ScaleUnit {
1878 name: "usd".to_string(),
1879 value: Decimal::new(11, 1),
1880 },
1881 ]),
1882 help: String::new(),
1883 default: None,
1884 },
1885 crate::planning::semantics::TypeExtends::Primitive,
1886 );
1887 let weight = LemmaType::new(
1888 "weight".to_string(),
1889 TypeSpecification::Scale {
1890 minimum: None,
1891 maximum: None,
1892 decimals: None,
1893 precision: None,
1894 units: ScaleUnits::from(vec![ScaleUnit {
1895 name: "kg".to_string(),
1896 value: Decimal::ONE,
1897 }]),
1898 help: String::new(),
1899 default: None,
1900 },
1901 crate::planning::semantics::TypeExtends::Primitive,
1902 );
1903 let ratio = lt(TypeSpecification::Ratio {
1904 minimum: Some(Decimal::ZERO),
1905 maximum: Some(Decimal::from(100)),
1906 decimals: None,
1907 units: RatioUnits::from(vec![RatioUnit {
1908 name: "percent".to_string(),
1909 value: Decimal::from(100),
1910 }]),
1911 help: String::new(),
1912 default: None,
1913 });
1914 let bounded_number = lt(TypeSpecification::Number {
1915 minimum: Some(Decimal::ZERO),
1916 maximum: Some(Decimal::from(100)),
1917 decimals: None,
1918 precision: None,
1919 help: String::new(),
1920 default: None,
1921 });
1922
1923 assert!(rule_type_satisfies_requirement(
1924 &money,
1925 &RuleRefRequirement::ScaleMustContainUnit("eur".to_string())
1926 ));
1927 assert!(!rule_type_satisfies_requirement(
1928 &money,
1929 &RuleRefRequirement::ScaleMustContainUnit("gbp".to_string())
1930 ));
1931 assert!(rule_type_satisfies_requirement(
1932 &ratio,
1933 &RuleRefRequirement::RatioMustContainUnit("percent".to_string())
1934 ));
1935 assert!(!rule_type_satisfies_requirement(
1936 &ratio,
1937 &RuleRefRequirement::RatioMustContainUnit("permille".to_string())
1938 ));
1939 assert!(rule_type_satisfies_requirement(
1940 &money,
1941 &RuleRefRequirement::SameScaleFamilyAs(money.clone())
1942 ));
1943 assert!(!rule_type_satisfies_requirement(
1944 &money,
1945 &RuleRefRequirement::SameScaleFamilyAs(weight)
1946 ));
1947
1948 assert!(rule_type_satisfies_requirement(
1949 &bounded_number,
1950 &RuleRefRequirement::NumericLiteral(NumericLiteralConstraint {
1951 op: ComparisonComputation::GreaterThan,
1952 literal: Decimal::from(50),
1953 reference_on_left: true,
1954 })
1955 ));
1956 assert!(!rule_type_satisfies_requirement(
1957 &bounded_number,
1958 &RuleRefRequirement::NumericLiteral(NumericLiteralConstraint {
1959 op: ComparisonComputation::GreaterThan,
1960 literal: Decimal::from(500),
1961 reference_on_left: true,
1962 })
1963 ));
1964 }
1965}