1use crate::parsing::ast::{DateTimeValue, FactValue, LemmaSpec, TimeValue, TypeDef};
7use crate::planning::semantics::{
8 Expression, ExpressionKind, FactPath, LemmaType, RulePath, SemanticConversionTarget,
9 TypeSpecification,
10};
11use crate::Error;
12use crate::Source;
13use indexmap::IndexMap;
14use rust_decimal::Decimal;
15use std::cmp::Ordering;
16use std::collections::{HashMap, HashSet};
17use std::sync::Arc;
18
19pub fn validate_type_specifications(
29 specs: &TypeSpecification,
30 type_name: &str,
31 source: &Source,
32 spec_context: Option<Arc<LemmaSpec>>,
33) -> Vec<Error> {
34 let mut errors = Vec::new();
35
36 match specs {
37 TypeSpecification::Scale {
38 minimum,
39 maximum,
40 decimals,
41 precision,
42 default,
43 units,
44 ..
45 } => {
46 if let (Some(min), Some(max)) = (minimum, maximum) {
48 if min > max {
49 errors.push(Error::validation_with_context(
50 format!(
51 "Type '{}' has invalid range: minimum {} is greater than maximum {}",
52 type_name, min, max
53 ),
54 Some(source.clone()),
55 None::<String>,
56 spec_context.clone(),
57 None,
58 ));
59 }
60 }
61
62 if let Some(d) = decimals {
64 if *d > 28 {
65 errors.push(Error::validation_with_context(
66 format!(
67 "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
68 type_name, d
69 ),
70 Some(source.clone()),
71 None::<String>,
72 spec_context.clone(),
73 None,
74 ));
75 }
76 }
77
78 if let Some(prec) = precision {
80 if *prec <= Decimal::ZERO {
81 errors.push(Error::validation_with_context(
82 format!(
83 "Type '{}' has invalid precision: {}. Must be positive",
84 type_name, prec
85 ),
86 Some(source.clone()),
87 None::<String>,
88 spec_context.clone(),
89 None,
90 ));
91 }
92 }
93
94 if let Some((def_value, def_unit)) = default {
96 if !units.iter().any(|u| u.name == *def_unit) {
98 errors.push(Error::validation_with_context(
99 format!(
100 "Type '{}' default unit '{}' is not a valid unit. Valid units: {}",
101 type_name,
102 def_unit,
103 units
104 .iter()
105 .map(|u| u.name.clone())
106 .collect::<Vec<_>>()
107 .join(", ")
108 ),
109 Some(source.clone()),
110 None::<String>,
111 spec_context.clone(),
112 None,
113 ));
114 }
115 if let Some(min) = minimum {
116 if *def_value < *min {
117 errors.push(Error::validation_with_context(
118 format!(
119 "Type '{}' default value {} {} is less than minimum {}",
120 type_name, def_value, def_unit, min
121 ),
122 Some(source.clone()),
123 None::<String>,
124 spec_context.clone(),
125 None,
126 ));
127 }
128 }
129 if let Some(max) = maximum {
130 if *def_value > *max {
131 errors.push(Error::validation_with_context(
132 format!(
133 "Type '{}' default value {} {} is greater than maximum {}",
134 type_name, def_value, def_unit, max
135 ),
136 Some(source.clone()),
137 None::<String>,
138 spec_context.clone(),
139 None,
140 ));
141 }
142 }
143 }
144
145 if units.is_empty() {
147 errors.push(Error::validation_with_context(
148 format!(
149 "Type '{}' is a scale type but has no units. Scale types must define at least one unit (e.g. -> unit eur 1).",
150 type_name
151 ),
152 Some(source.clone()),
153 None::<String>,
154 spec_context.clone(),
155 None,
156 ));
157 }
158
159 if !units.is_empty() {
161 let mut seen_names: Vec<String> = Vec::new();
162 for unit in units.iter() {
163 if unit.name.trim().is_empty() {
165 errors.push(Error::validation_with_context(
166 format!(
167 "Type '{}' has a unit with empty name. Unit names cannot be empty.",
168 type_name
169 ),
170 Some(source.clone()),
171 None::<String>,
172 spec_context.clone(),
173 None,
174 ));
175 }
176
177 let lower_name = unit.name.to_lowercase();
179 if seen_names
180 .iter()
181 .any(|seen| seen.to_lowercase() == lower_name)
182 {
183 errors.push(Error::validation_with_context(
184 format!("Type '{}' has duplicate unit name '{}' (case-insensitive). Unit names must be unique within a type.", type_name, unit.name),
185 Some(source.clone()),
186 None::<String>,
187 spec_context.clone(),
188 None,
189 ));
190 } else {
191 seen_names.push(unit.name.clone());
192 }
193
194 if unit.value <= Decimal::ZERO {
196 errors.push(Error::validation_with_context(
197 format!("Type '{}' has unit '{}' with invalid value {}. Unit values must be positive (conversion factor relative to type base).", type_name, unit.name, unit.value),
198 Some(source.clone()),
199 None::<String>,
200 spec_context.clone(),
201 None,
202 ));
203 }
204 }
205 }
206 }
207 TypeSpecification::Number {
208 minimum,
209 maximum,
210 decimals,
211 precision,
212 default,
213 ..
214 } => {
215 if let (Some(min), Some(max)) = (minimum, maximum) {
217 if min > max {
218 errors.push(Error::validation_with_context(
219 format!(
220 "Type '{}' has invalid range: minimum {} is greater than maximum {}",
221 type_name, min, max
222 ),
223 Some(source.clone()),
224 None::<String>,
225 spec_context.clone(),
226 None,
227 ));
228 }
229 }
230
231 if let Some(d) = decimals {
233 if *d > 28 {
234 errors.push(Error::validation_with_context(
235 format!(
236 "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
237 type_name, d
238 ),
239 Some(source.clone()),
240 None::<String>,
241 spec_context.clone(),
242 None,
243 ));
244 }
245 }
246
247 if let Some(prec) = precision {
249 if *prec <= Decimal::ZERO {
250 errors.push(Error::validation_with_context(
251 format!(
252 "Type '{}' has invalid precision: {}. Must be positive",
253 type_name, prec
254 ),
255 Some(source.clone()),
256 None::<String>,
257 spec_context.clone(),
258 None,
259 ));
260 }
261 }
262
263 if let Some(def) = default {
265 if let Some(min) = minimum {
266 if *def < *min {
267 errors.push(Error::validation_with_context(
268 format!(
269 "Type '{}' default value {} is less than minimum {}",
270 type_name, def, min
271 ),
272 Some(source.clone()),
273 None::<String>,
274 spec_context.clone(),
275 None,
276 ));
277 }
278 }
279 if let Some(max) = maximum {
280 if *def > *max {
281 errors.push(Error::validation_with_context(
282 format!(
283 "Type '{}' default value {} is greater than maximum {}",
284 type_name, def, max
285 ),
286 Some(source.clone()),
287 None::<String>,
288 spec_context.clone(),
289 None,
290 ));
291 }
292 }
293 }
294 }
296
297 TypeSpecification::Ratio {
298 minimum,
299 maximum,
300 decimals,
301 default,
302 units,
303 ..
304 } => {
305 if let Some(d) = decimals {
307 if *d > 28 {
308 errors.push(Error::validation_with_context(
309 format!(
310 "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
311 type_name, d
312 ),
313 Some(source.clone()),
314 None::<String>,
315 spec_context.clone(),
316 None,
317 ));
318 }
319 }
320
321 if let (Some(min), Some(max)) = (minimum, maximum) {
323 if min > max {
324 errors.push(Error::validation_with_context(
325 format!(
326 "Type '{}' has invalid range: minimum {} is greater than maximum {}",
327 type_name, min, max
328 ),
329 Some(source.clone()),
330 None::<String>,
331 spec_context.clone(),
332 None,
333 ));
334 }
335 }
336
337 if let Some(def) = default {
339 if let Some(min) = minimum {
340 if *def < *min {
341 errors.push(Error::validation_with_context(
342 format!(
343 "Type '{}' default value {} is less than minimum {}",
344 type_name, def, min
345 ),
346 Some(source.clone()),
347 None::<String>,
348 spec_context.clone(),
349 None,
350 ));
351 }
352 }
353 if let Some(max) = maximum {
354 if *def > *max {
355 errors.push(Error::validation_with_context(
356 format!(
357 "Type '{}' default value {} is greater than maximum {}",
358 type_name, def, max
359 ),
360 Some(source.clone()),
361 None::<String>,
362 spec_context.clone(),
363 None,
364 ));
365 }
366 }
367 }
368
369 if !units.is_empty() {
373 let mut seen_names: Vec<String> = Vec::new();
374 for unit in units.iter() {
375 if unit.name.trim().is_empty() {
377 errors.push(Error::validation_with_context(
378 format!(
379 "Type '{}' has a unit with empty name. Unit names cannot be empty.",
380 type_name
381 ),
382 Some(source.clone()),
383 None::<String>,
384 spec_context.clone(),
385 None,
386 ));
387 }
388
389 let lower_name = unit.name.to_lowercase();
391 if seen_names
392 .iter()
393 .any(|seen| seen.to_lowercase() == lower_name)
394 {
395 errors.push(Error::validation_with_context(
396 format!("Type '{}' has duplicate unit name '{}' (case-insensitive). Unit names must be unique within a type.", type_name, unit.name),
397 Some(source.clone()),
398 None::<String>,
399 spec_context.clone(),
400 None,
401 ));
402 } else {
403 seen_names.push(unit.name.clone());
404 }
405
406 if unit.value <= Decimal::ZERO {
408 errors.push(Error::validation_with_context(
409 format!("Type '{}' has unit '{}' with invalid value {}. Unit values must be positive (conversion factor relative to type base).", type_name, unit.name, unit.value),
410 Some(source.clone()),
411 None::<String>,
412 spec_context.clone(),
413 None,
414 ));
415 }
416 }
417 }
418 }
419
420 TypeSpecification::Text {
421 minimum,
422 maximum,
423 length,
424 options,
425 default,
426 ..
427 } => {
428 if let (Some(min), Some(max)) = (minimum, maximum) {
430 if min > max {
431 errors.push(Error::validation_with_context(
432 format!("Type '{}' has invalid range: minimum length {} is greater than maximum length {}", type_name, min, max),
433 Some(source.clone()),
434 None::<String>,
435 spec_context.clone(),
436 None,
437 ));
438 }
439 }
440
441 if let Some(len) = length {
443 if let Some(min) = minimum {
444 if *len < *min {
445 errors.push(Error::validation_with_context(
446 format!("Type '{}' has inconsistent length constraint: length {} is less than minimum {}", type_name, len, min),
447 Some(source.clone()),
448 None::<String>,
449 spec_context.clone(),
450 None,
451 ));
452 }
453 }
454 if let Some(max) = maximum {
455 if *len > *max {
456 errors.push(Error::validation_with_context(
457 format!("Type '{}' has inconsistent length constraint: length {} is greater than maximum {}", type_name, len, max),
458 Some(source.clone()),
459 None::<String>,
460 spec_context.clone(),
461 None,
462 ));
463 }
464 }
465 }
466
467 if let Some(def) = default {
469 let def_len = def.len();
470
471 if let Some(min) = minimum {
472 if def_len < *min {
473 errors.push(Error::validation_with_context(
474 format!(
475 "Type '{}' default value length {} is less than minimum {}",
476 type_name, def_len, min
477 ),
478 Some(source.clone()),
479 None::<String>,
480 spec_context.clone(),
481 None,
482 ));
483 }
484 }
485 if let Some(max) = maximum {
486 if def_len > *max {
487 errors.push(Error::validation_with_context(
488 format!(
489 "Type '{}' default value length {} is greater than maximum {}",
490 type_name, def_len, max
491 ),
492 Some(source.clone()),
493 None::<String>,
494 spec_context.clone(),
495 None,
496 ));
497 }
498 }
499 if let Some(len) = length {
500 if def_len != *len {
501 errors.push(Error::validation_with_context(
502 format!("Type '{}' default value length {} does not match required length {}", type_name, def_len, len),
503 Some(source.clone()),
504 None::<String>,
505 spec_context.clone(),
506 None,
507 ));
508 }
509 }
510 if !options.is_empty() && !options.contains(def) {
511 errors.push(Error::validation_with_context(
512 format!(
513 "Type '{}' default value '{}' is not in allowed options: {:?}",
514 type_name, def, options
515 ),
516 Some(source.clone()),
517 None::<String>,
518 spec_context.clone(),
519 None,
520 ));
521 }
522 }
523 }
524
525 TypeSpecification::Date {
526 minimum,
527 maximum,
528 default,
529 ..
530 } => {
531 if let (Some(min), Some(max)) = (minimum, maximum) {
533 if compare_date_values(min, max) == Ordering::Greater {
534 errors.push(Error::validation_with_context(
535 format!(
536 "Type '{}' has invalid date range: minimum {} is after maximum {}",
537 type_name, min, max
538 ),
539 Some(source.clone()),
540 None::<String>,
541 spec_context.clone(),
542 None,
543 ));
544 }
545 }
546
547 if let Some(def) = default {
549 if let Some(min) = minimum {
550 if compare_date_values(def, min) == Ordering::Less {
551 errors.push(Error::validation_with_context(
552 format!(
553 "Type '{}' default date {} is before minimum {}",
554 type_name, def, min
555 ),
556 Some(source.clone()),
557 None::<String>,
558 spec_context.clone(),
559 None,
560 ));
561 }
562 }
563 if let Some(max) = maximum {
564 if compare_date_values(def, max) == Ordering::Greater {
565 errors.push(Error::validation_with_context(
566 format!(
567 "Type '{}' default date {} is after maximum {}",
568 type_name, def, max
569 ),
570 Some(source.clone()),
571 None::<String>,
572 spec_context.clone(),
573 None,
574 ));
575 }
576 }
577 }
578 }
579
580 TypeSpecification::Time {
581 minimum,
582 maximum,
583 default,
584 ..
585 } => {
586 if let (Some(min), Some(max)) = (minimum, maximum) {
588 if compare_time_values(min, max) == Ordering::Greater {
589 errors.push(Error::validation_with_context(
590 format!(
591 "Type '{}' has invalid time range: minimum {} is after maximum {}",
592 type_name, min, max
593 ),
594 Some(source.clone()),
595 None::<String>,
596 spec_context.clone(),
597 None,
598 ));
599 }
600 }
601
602 if let Some(def) = default {
604 if let Some(min) = minimum {
605 if compare_time_values(def, min) == Ordering::Less {
606 errors.push(Error::validation_with_context(
607 format!(
608 "Type '{}' default time {} is before minimum {}",
609 type_name, def, min
610 ),
611 Some(source.clone()),
612 None::<String>,
613 spec_context.clone(),
614 None,
615 ));
616 }
617 }
618 if let Some(max) = maximum {
619 if compare_time_values(def, max) == Ordering::Greater {
620 errors.push(Error::validation_with_context(
621 format!(
622 "Type '{}' default time {} is after maximum {}",
623 type_name, def, max
624 ),
625 Some(source.clone()),
626 None::<String>,
627 spec_context.clone(),
628 None,
629 ));
630 }
631 }
632 }
633 }
634
635 TypeSpecification::Boolean { .. } | TypeSpecification::Duration { .. } => {
636 }
638 TypeSpecification::Veto { .. } => {
639 }
642 TypeSpecification::Undetermined => unreachable!(
643 "BUG: validate_type_specification_constraints called with Undetermined sentinel type; this type exists only during type inference"
644 ),
645 }
646
647 errors
648}
649
650fn compare_date_values(left: &DateTimeValue, right: &DateTimeValue) -> Ordering {
652 left.year
654 .cmp(&right.year)
655 .then_with(|| left.month.cmp(&right.month))
656 .then_with(|| left.day.cmp(&right.day))
657 .then_with(|| left.hour.cmp(&right.hour))
658 .then_with(|| left.minute.cmp(&right.minute))
659 .then_with(|| left.second.cmp(&right.second))
660}
661
662fn compare_time_values(left: &TimeValue, right: &TimeValue) -> Ordering {
664 left.hour
666 .cmp(&right.hour)
667 .then_with(|| left.minute.cmp(&right.minute))
668 .then_with(|| left.second.cmp(&right.second))
669}
670
671pub struct RuleEntryForBindingCheck {
677 pub rule_type: LemmaType,
678 pub depends_on_rules: std::collections::BTreeSet<RulePath>,
679 pub branches: Vec<(Option<Expression>, Expression)>,
680}
681
682#[derive(Clone, Copy, Debug)]
683enum ExpectedRuleTypeConstraint {
684 Numeric,
685 Boolean,
686 Comparable,
687 Number,
688 Duration,
689 Ratio,
690 Scale,
691 Any,
692}
693
694fn lemma_type_to_expected_constraint(lemma_type: &LemmaType) -> ExpectedRuleTypeConstraint {
695 if lemma_type.is_boolean() {
696 return ExpectedRuleTypeConstraint::Boolean;
697 }
698 if lemma_type.is_number() {
699 return ExpectedRuleTypeConstraint::Number;
700 }
701 if lemma_type.is_scale() {
702 return ExpectedRuleTypeConstraint::Scale;
703 }
704 if lemma_type.is_duration() {
705 return ExpectedRuleTypeConstraint::Duration;
706 }
707 if lemma_type.is_ratio() {
708 return ExpectedRuleTypeConstraint::Ratio;
709 }
710 if lemma_type.is_text() || lemma_type.is_date() || lemma_type.is_time() {
711 return ExpectedRuleTypeConstraint::Comparable;
712 }
713 ExpectedRuleTypeConstraint::Any
714}
715
716fn rule_type_satisfies_constraint(
717 lemma_type: &LemmaType,
718 constraint: ExpectedRuleTypeConstraint,
719) -> bool {
720 match constraint {
721 ExpectedRuleTypeConstraint::Any => true,
722 ExpectedRuleTypeConstraint::Boolean => lemma_type.is_boolean(),
723 ExpectedRuleTypeConstraint::Number => lemma_type.is_number(),
724 ExpectedRuleTypeConstraint::Duration => lemma_type.is_duration(),
725 ExpectedRuleTypeConstraint::Ratio => lemma_type.is_ratio(),
726 ExpectedRuleTypeConstraint::Scale => lemma_type.is_scale(),
727 ExpectedRuleTypeConstraint::Numeric => {
728 lemma_type.is_number() || lemma_type.is_scale() || lemma_type.is_ratio()
729 }
730 ExpectedRuleTypeConstraint::Comparable => {
731 lemma_type.is_boolean()
732 || lemma_type.is_text()
733 || lemma_type.is_number()
734 || lemma_type.is_ratio()
735 || lemma_type.is_date()
736 || lemma_type.is_time()
737 || lemma_type.is_scale()
738 || lemma_type.is_duration()
739 }
740 }
741}
742
743fn collect_expected_constraints_for_rule_ref(
744 expr: &Expression,
745 rule_path: &RulePath,
746 expected: ExpectedRuleTypeConstraint,
747) -> Vec<(Option<Source>, ExpectedRuleTypeConstraint)> {
748 let mut out = Vec::new();
749 match &expr.kind {
750 ExpressionKind::RulePath(rp) => {
751 if rp == rule_path {
752 out.push((expr.source_location.clone(), expected));
753 }
754 }
755 ExpressionKind::LogicalAnd(left, right) => {
756 out.extend(collect_expected_constraints_for_rule_ref(
757 left,
758 rule_path,
759 ExpectedRuleTypeConstraint::Boolean,
760 ));
761 out.extend(collect_expected_constraints_for_rule_ref(
762 right,
763 rule_path,
764 ExpectedRuleTypeConstraint::Boolean,
765 ));
766 }
767 ExpressionKind::LogicalNegation(operand, _) => {
768 out.extend(collect_expected_constraints_for_rule_ref(
769 operand,
770 rule_path,
771 ExpectedRuleTypeConstraint::Boolean,
772 ));
773 }
774 ExpressionKind::Comparison(left, _, right) => {
775 out.extend(collect_expected_constraints_for_rule_ref(
776 left,
777 rule_path,
778 ExpectedRuleTypeConstraint::Comparable,
779 ));
780 out.extend(collect_expected_constraints_for_rule_ref(
781 right,
782 rule_path,
783 ExpectedRuleTypeConstraint::Comparable,
784 ));
785 }
786 ExpressionKind::Arithmetic(left, _, right) => {
787 out.extend(collect_expected_constraints_for_rule_ref(
788 left,
789 rule_path,
790 ExpectedRuleTypeConstraint::Numeric,
791 ));
792 out.extend(collect_expected_constraints_for_rule_ref(
793 right,
794 rule_path,
795 ExpectedRuleTypeConstraint::Numeric,
796 ));
797 }
798 ExpressionKind::UnitConversion(source, target) => {
799 let constraint = match target {
800 SemanticConversionTarget::Duration(_) => ExpectedRuleTypeConstraint::Duration,
801 SemanticConversionTarget::ScaleUnit(_) => ExpectedRuleTypeConstraint::Scale,
802 SemanticConversionTarget::RatioUnit(_) => ExpectedRuleTypeConstraint::Ratio,
803 };
804 out.extend(collect_expected_constraints_for_rule_ref(
805 source, rule_path, constraint,
806 ));
807 }
808 ExpressionKind::MathematicalComputation(_, operand) => {
809 out.extend(collect_expected_constraints_for_rule_ref(
810 operand,
811 rule_path,
812 ExpectedRuleTypeConstraint::Number,
813 ));
814 }
815 ExpressionKind::DateRelative(_, date_expr, tolerance) => {
816 out.extend(collect_expected_constraints_for_rule_ref(
817 date_expr,
818 rule_path,
819 ExpectedRuleTypeConstraint::Comparable,
820 ));
821 if let Some(tol) = tolerance {
822 out.extend(collect_expected_constraints_for_rule_ref(
823 tol,
824 rule_path,
825 ExpectedRuleTypeConstraint::Duration,
826 ));
827 }
828 }
829 ExpressionKind::DateCalendar(_, _, date_expr) => {
830 out.extend(collect_expected_constraints_for_rule_ref(
831 date_expr,
832 rule_path,
833 ExpectedRuleTypeConstraint::Comparable,
834 ));
835 }
836 ExpressionKind::Literal(_)
837 | ExpressionKind::FactPath(_)
838 | ExpressionKind::Veto(_)
839 | ExpressionKind::Now => {}
840 }
841 out
842}
843
844fn expected_constraint_name(c: ExpectedRuleTypeConstraint) -> &'static str {
845 match c {
846 ExpectedRuleTypeConstraint::Numeric => "numeric (number, scale, or ratio)",
847 ExpectedRuleTypeConstraint::Boolean => "boolean",
848 ExpectedRuleTypeConstraint::Comparable => "comparable",
849 ExpectedRuleTypeConstraint::Number => "number",
850 ExpectedRuleTypeConstraint::Duration => "duration",
851 ExpectedRuleTypeConstraint::Ratio => "ratio",
852 ExpectedRuleTypeConstraint::Scale => "scale",
853 ExpectedRuleTypeConstraint::Any => "any",
854 }
855}
856
857fn spec_interface_error(
858 source: &Source,
859 message: impl Into<String>,
860 spec_context: Option<Arc<LemmaSpec>>,
861 related_spec: Option<Arc<LemmaSpec>>,
862) -> Error {
863 Error::validation_with_context(
864 message.into(),
865 Some(source.clone()),
866 None::<String>,
867 spec_context,
868 related_spec,
869 )
870}
871
872pub fn validate_spec_interfaces(
875 referenced_rules: &HashMap<Vec<String>, HashSet<String>>,
876 spec_ref_facts: &[(FactPath, Arc<LemmaSpec>, Source)],
877 rule_entries: &IndexMap<RulePath, RuleEntryForBindingCheck>,
878 main_spec: &Arc<LemmaSpec>,
879) -> Result<(), Vec<Error>> {
880 let mut errors = Vec::new();
881
882 for (fact_path, spec_arc, fact_source) in spec_ref_facts {
883 let mut full_path: Vec<String> =
884 fact_path.segments.iter().map(|s| s.fact.clone()).collect();
885 full_path.push(fact_path.fact.clone());
886
887 let Some(required_rules) = referenced_rules.get(&full_path) else {
888 continue;
889 };
890
891 let spec = spec_arc.as_ref();
892 let spec_rule_names: HashSet<&str> = spec.rules.iter().map(|r| r.name.as_str()).collect();
893
894 for required_rule in required_rules {
895 if !spec_rule_names.contains(required_rule.as_str()) {
896 errors.push(spec_interface_error(
897 fact_source,
898 format!(
899 "Spec '{}' referenced by '{}' is missing required rule '{}'",
900 spec.name, fact_path, required_rule
901 ),
902 Some(Arc::clone(main_spec)),
903 Some(Arc::clone(spec_arc)),
904 ));
905 continue;
906 }
907
908 let ref_rule_path = RulePath::new(fact_path.segments.clone(), required_rule.clone());
909 let Some(ref_entry) = rule_entries.get(&ref_rule_path) else {
910 continue;
911 };
912 let ref_rule_type = &ref_entry.rule_type;
913
914 for (_referencing_path, entry) in rule_entries {
915 if !entry.depends_on_rules.contains(&ref_rule_path) {
916 continue;
917 }
918 let expected = lemma_type_to_expected_constraint(&entry.rule_type);
919 for (_condition, result_expr) in &entry.branches {
920 let constraints = collect_expected_constraints_for_rule_ref(
921 result_expr,
922 &ref_rule_path,
923 expected,
924 );
925 for (_source, constraint) in constraints {
926 if !rule_type_satisfies_constraint(ref_rule_type, constraint) {
927 let report_source = fact_source;
928
929 let binding_path_str = fact_path
930 .segments
931 .iter()
932 .map(|s| s.fact.as_str())
933 .collect::<Vec<_>>()
934 .join(".");
935 let binding_path_str = if binding_path_str.is_empty() {
936 fact_path.fact.clone()
937 } else {
938 format!("{}.{}", binding_path_str, fact_path.fact)
939 };
940
941 errors.push(spec_interface_error(
942 report_source,
943 format!(
944 "Fact binding '{}' sets spec reference to '{}', but that spec's rule '{}' has result type {}; the referencing expression expects a {} value",
945 binding_path_str,
946 spec.name,
947 required_rule,
948 ref_rule_type.name(),
949 expected_constraint_name(constraint),
950 ),
951 Some(Arc::clone(main_spec)),
952 Some(Arc::clone(spec_arc)),
953 ));
954 }
955 }
956 }
957 }
958 }
959 }
960
961 if errors.is_empty() {
962 Ok(())
963 } else {
964 Err(errors)
965 }
966}
967
968pub fn collect_bare_registry_refs(spec: &LemmaSpec) -> Vec<String> {
974 if !spec.from_registry {
975 return Vec::new();
976 }
977 let mut bare: Vec<String> = Vec::new();
978 for fact in &spec.facts {
979 match &fact.value {
980 FactValue::SpecReference(r) if !r.from_registry => {
981 bare.push(r.name.clone());
982 }
983 FactValue::TypeDeclaration { from: Some(r), .. } if !r.from_registry => {
984 bare.push(r.name.clone());
985 }
986 _ => {}
987 }
988 }
989 for type_def in &spec.types {
990 match type_def {
991 TypeDef::Import { from, .. } if !from.from_registry => {
992 bare.push(from.name.clone());
993 }
994 TypeDef::Inline { from: Some(r), .. } if !r.from_registry => {
995 bare.push(r.name.clone());
996 }
997 _ => {}
998 }
999 }
1000 bare
1001}
1002
1003#[cfg(test)]
1004mod tests {
1005 use super::*;
1006 use crate::parsing::ast::{CommandArg, TypeConstraintCommand};
1007 use crate::planning::semantics::TypeSpecification;
1008 use rust_decimal::Decimal;
1009
1010 fn test_source() -> Source {
1011 Source::new(
1012 "<test>",
1013 crate::parsing::ast::Span {
1014 start: 0,
1015 end: 0,
1016 line: 1,
1017 col: 0,
1018 },
1019 )
1020 }
1021
1022 #[test]
1023 fn validate_number_minimum_greater_than_maximum() {
1024 let mut specs = TypeSpecification::number();
1025 specs = specs
1026 .apply_constraint(
1027 TypeConstraintCommand::Minimum,
1028 &[CommandArg::Number("100".to_string())],
1029 )
1030 .unwrap();
1031 specs = specs
1032 .apply_constraint(
1033 TypeConstraintCommand::Maximum,
1034 &[CommandArg::Number("50".to_string())],
1035 )
1036 .unwrap();
1037
1038 let src = test_source();
1039 let errors = validate_type_specifications(&specs, "test", &src, None);
1040 assert_eq!(errors.len(), 1);
1041 assert!(errors[0]
1042 .to_string()
1043 .contains("minimum 100 is greater than maximum 50"));
1044 }
1045
1046 #[test]
1047 fn validate_number_valid_range() {
1048 let mut specs = TypeSpecification::number();
1049 specs = specs
1050 .apply_constraint(
1051 TypeConstraintCommand::Minimum,
1052 &[CommandArg::Number("0".to_string())],
1053 )
1054 .unwrap();
1055 specs = specs
1056 .apply_constraint(
1057 TypeConstraintCommand::Maximum,
1058 &[CommandArg::Number("100".to_string())],
1059 )
1060 .unwrap();
1061
1062 let src = test_source();
1063 let errors = validate_type_specifications(&specs, "test", &src, None);
1064 assert!(errors.is_empty());
1065 }
1066
1067 #[test]
1068 fn validate_number_default_below_minimum() {
1069 let specs = TypeSpecification::Number {
1070 minimum: Some(Decimal::from(10)),
1071 maximum: None,
1072 decimals: None,
1073 precision: None,
1074 help: String::new(),
1075 default: Some(Decimal::from(5)),
1076 };
1077
1078 let src = test_source();
1079 let errors = validate_type_specifications(&specs, "test", &src, None);
1080 assert_eq!(errors.len(), 1);
1081 assert!(errors[0]
1082 .to_string()
1083 .contains("default value 5 is less than minimum 10"));
1084 }
1085
1086 #[test]
1087 fn validate_number_default_above_maximum() {
1088 let specs = TypeSpecification::Number {
1089 minimum: None,
1090 maximum: Some(Decimal::from(100)),
1091 decimals: None,
1092 precision: None,
1093 help: String::new(),
1094 default: Some(Decimal::from(150)),
1095 };
1096
1097 let src = test_source();
1098 let errors = validate_type_specifications(&specs, "test", &src, None);
1099 assert_eq!(errors.len(), 1);
1100 assert!(errors[0]
1101 .to_string()
1102 .contains("default value 150 is greater than maximum 100"));
1103 }
1104
1105 #[test]
1106 fn validate_number_default_valid() {
1107 let specs = TypeSpecification::Number {
1108 minimum: Some(Decimal::from(0)),
1109 maximum: Some(Decimal::from(100)),
1110 decimals: None,
1111 precision: None,
1112 help: String::new(),
1113 default: Some(Decimal::from(50)),
1114 };
1115
1116 let src = test_source();
1117 let errors = validate_type_specifications(&specs, "test", &src, None);
1118 assert!(errors.is_empty());
1119 }
1120
1121 #[test]
1122 fn validate_text_minimum_greater_than_maximum() {
1123 let mut specs = TypeSpecification::text();
1124 specs = specs
1125 .apply_constraint(
1126 TypeConstraintCommand::Minimum,
1127 &[CommandArg::Number("100".to_string())],
1128 )
1129 .unwrap();
1130 specs = specs
1131 .apply_constraint(
1132 TypeConstraintCommand::Maximum,
1133 &[CommandArg::Number("50".to_string())],
1134 )
1135 .unwrap();
1136
1137 let src = test_source();
1138 let errors = validate_type_specifications(&specs, "test", &src, None);
1139 assert_eq!(errors.len(), 1);
1140 assert!(errors[0]
1141 .to_string()
1142 .contains("minimum length 100 is greater than maximum length 50"));
1143 }
1144
1145 #[test]
1146 fn validate_text_length_inconsistent_with_minimum() {
1147 let mut specs = TypeSpecification::text();
1148 specs = specs
1149 .apply_constraint(
1150 TypeConstraintCommand::Minimum,
1151 &[CommandArg::Number("10".to_string())],
1152 )
1153 .unwrap();
1154 specs = specs
1155 .apply_constraint(
1156 TypeConstraintCommand::Length,
1157 &[CommandArg::Number("5".to_string())],
1158 )
1159 .unwrap();
1160
1161 let src = test_source();
1162 let errors = validate_type_specifications(&specs, "test", &src, None);
1163 assert_eq!(errors.len(), 1);
1164 assert!(errors[0]
1165 .to_string()
1166 .contains("length 5 is less than minimum 10"));
1167 }
1168
1169 #[test]
1170 fn validate_text_default_not_in_options() {
1171 let specs = TypeSpecification::Text {
1172 minimum: None,
1173 maximum: None,
1174 length: None,
1175 options: vec!["red".to_string(), "blue".to_string()],
1176 help: String::new(),
1177 default: Some("green".to_string()),
1178 };
1179
1180 let src = test_source();
1181 let errors = validate_type_specifications(&specs, "test", &src, None);
1182 assert_eq!(errors.len(), 1);
1183 assert!(errors[0]
1184 .to_string()
1185 .contains("default value 'green' is not in allowed options"));
1186 }
1187
1188 #[test]
1189 fn validate_text_default_valid_in_options() {
1190 let specs = TypeSpecification::Text {
1191 minimum: None,
1192 maximum: None,
1193 length: None,
1194 options: vec!["red".to_string(), "blue".to_string()],
1195 help: String::new(),
1196 default: Some("red".to_string()),
1197 };
1198
1199 let src = test_source();
1200 let errors = validate_type_specifications(&specs, "test", &src, None);
1201 assert!(errors.is_empty());
1202 }
1203
1204 #[test]
1205 fn validate_ratio_minimum_greater_than_maximum() {
1206 let specs = TypeSpecification::Ratio {
1207 minimum: Some(Decimal::from(2)),
1208 maximum: Some(Decimal::from(1)),
1209 decimals: None,
1210 units: crate::planning::semantics::RatioUnits::new(),
1211 help: String::new(),
1212 default: None,
1213 };
1214
1215 let src = test_source();
1216 let errors = validate_type_specifications(&specs, "test", &src, None);
1217 assert_eq!(errors.len(), 1);
1218 assert!(errors[0]
1219 .to_string()
1220 .contains("minimum 2 is greater than maximum 1"));
1221 }
1222
1223 #[test]
1224 fn validate_date_minimum_after_maximum() {
1225 let mut specs = TypeSpecification::date();
1226 specs = specs
1227 .apply_constraint(
1228 TypeConstraintCommand::Minimum,
1229 &[CommandArg::Label("2024-12-31".to_string())],
1230 )
1231 .unwrap();
1232 specs = specs
1233 .apply_constraint(
1234 TypeConstraintCommand::Maximum,
1235 &[CommandArg::Label("2024-01-01".to_string())],
1236 )
1237 .unwrap();
1238
1239 let src = test_source();
1240 let errors = validate_type_specifications(&specs, "test", &src, None);
1241 assert_eq!(errors.len(), 1);
1242 assert!(
1243 errors[0].to_string().contains("minimum")
1244 && errors[0].to_string().contains("is after maximum")
1245 );
1246 }
1247
1248 #[test]
1249 fn validate_date_valid_range() {
1250 let mut specs = TypeSpecification::date();
1251 specs = specs
1252 .apply_constraint(
1253 TypeConstraintCommand::Minimum,
1254 &[CommandArg::Label("2024-01-01".to_string())],
1255 )
1256 .unwrap();
1257 specs = specs
1258 .apply_constraint(
1259 TypeConstraintCommand::Maximum,
1260 &[CommandArg::Label("2024-12-31".to_string())],
1261 )
1262 .unwrap();
1263
1264 let src = test_source();
1265 let errors = validate_type_specifications(&specs, "test", &src, None);
1266 assert!(errors.is_empty());
1267 }
1268
1269 #[test]
1270 fn validate_time_minimum_after_maximum() {
1271 let mut specs = TypeSpecification::time();
1272 specs = specs
1273 .apply_constraint(
1274 TypeConstraintCommand::Minimum,
1275 &[CommandArg::Label("23:00:00".to_string())],
1276 )
1277 .unwrap();
1278 specs = specs
1279 .apply_constraint(
1280 TypeConstraintCommand::Maximum,
1281 &[CommandArg::Label("10:00:00".to_string())],
1282 )
1283 .unwrap();
1284
1285 let src = test_source();
1286 let errors = validate_type_specifications(&specs, "test", &src, None);
1287 assert_eq!(errors.len(), 1);
1288 assert!(
1289 errors[0].to_string().contains("minimum")
1290 && errors[0].to_string().contains("is after maximum")
1291 );
1292 }
1293
1294 #[test]
1295 fn validate_type_definition_with_invalid_constraints() {
1296 use crate::engine::Context;
1300 use crate::parsing::ast::{LemmaSpec, ParentType, PrimitiveKind, TypeDef};
1301 use crate::planning::types::PerSliceTypeResolver;
1302 use std::sync::Arc;
1303
1304 let spec = Arc::new(LemmaSpec::new("test".to_string()));
1305 let mut ctx = Context::new();
1306 ctx.insert_spec(Arc::clone(&spec), false)
1307 .expect("insert test spec");
1308 let type_def = TypeDef::Regular {
1309 source_location: crate::Source::new(
1310 "<test>",
1311 crate::parsing::ast::Span {
1312 start: 0,
1313 end: 0,
1314 line: 1,
1315 col: 0,
1316 },
1317 ),
1318 name: "invalid_money".to_string(),
1319 parent: ParentType::Primitive {
1320 primitive: PrimitiveKind::Number,
1321 },
1322 constraints: Some(vec![
1323 (
1324 TypeConstraintCommand::Minimum,
1325 vec![CommandArg::Number("100".to_string())],
1326 ),
1327 (
1328 TypeConstraintCommand::Maximum,
1329 vec![CommandArg::Number("50".to_string())],
1330 ),
1331 ]),
1332 };
1333
1334 let plan_hashes = crate::planning::PlanHashRegistry::default();
1335 let mut type_resolver = PerSliceTypeResolver::new(&ctx, None, &plan_hashes);
1336 type_resolver
1337 .register_type(&spec, type_def)
1338 .expect("Should register type");
1339 let resolved_types = type_resolver
1340 .resolve_named_types(&spec)
1341 .expect("Should resolve types");
1342
1343 let lemma_type = resolved_types
1345 .named_types
1346 .get("invalid_money")
1347 .expect("Should have invalid_money type");
1348 let src = test_source();
1349 let errors =
1350 validate_type_specifications(&lemma_type.specifications, "invalid_money", &src, None);
1351 assert!(!errors.is_empty());
1352 assert!(errors.iter().any(|e| e
1353 .to_string()
1354 .contains("minimum 100 is greater than maximum 50")));
1355 }
1356}