1use crate::engine::Context;
2use crate::parsing::ast::{
3 self as ast, Constraint, EffectiveDate, LemmaData, LemmaRepository, LemmaRule, LemmaSpec,
4 MetaValue, ParentType, PrimitiveKind, Value,
5};
6use crate::parsing::source::Source;
7use crate::planning::discovery;
8use crate::planning::semantics::{
9 self, conversion_target_to_semantic, primitive_boolean, primitive_date, primitive_duration,
10 primitive_number, primitive_ratio, primitive_text, primitive_time, value_to_semantic,
11 ArithmeticComputation, ComparisonComputation, DataDefinition, DataPath, Expression,
12 ExpressionKind, LemmaType, LiteralValue, PathSegment, ReferenceTarget, RulePath,
13 SemanticConversionTarget, TypeDefiningSpec, TypeExtends, TypeSpecification, ValueKind,
14};
15use crate::Error;
16use ast::DataValue as ParsedDataValue;
17use indexmap::IndexMap;
18use rust_decimal::Decimal;
19use std::cmp::Ordering;
20use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
21use std::fmt;
22use std::sync::Arc;
23
24type DataBindings = HashMap<Vec<String>, (BindingValue, Source)>;
33
34#[derive(Debug, Clone)]
43pub(crate) enum BindingValue {
44 Literal(ast::Value),
46 Reference {
48 target: ReferenceTarget,
49 constraints: Option<Vec<Constraint>>,
50 },
51}
52
53#[derive(Debug)]
54pub(crate) struct Graph {
55 main_spec: Arc<LemmaSpec>,
57 data: IndexMap<DataPath, DataDefinition>,
58 rules: BTreeMap<RulePath, RuleNode>,
59 execution_order: Vec<RulePath>,
60 reference_evaluation_order: Vec<DataPath>,
65}
66
67impl Graph {
68 pub(crate) fn data(&self) -> &IndexMap<DataPath, DataDefinition> {
69 &self.data
70 }
71
72 pub(crate) fn rules(&self) -> &BTreeMap<RulePath, RuleNode> {
73 &self.rules
74 }
75
76 pub(crate) fn rules_mut(&mut self) -> &mut BTreeMap<RulePath, RuleNode> {
77 &mut self.rules
78 }
79
80 pub(crate) fn execution_order(&self) -> &[RulePath] {
81 &self.execution_order
82 }
83
84 pub(crate) fn reference_evaluation_order(&self) -> &[DataPath] {
85 &self.reference_evaluation_order
86 }
87
88 pub(crate) fn main_spec(&self) -> &Arc<LemmaSpec> {
89 &self.main_spec
90 }
91
92 pub(crate) fn build_data(&self) -> IndexMap<DataPath, DataDefinition> {
95 struct PendingReference {
96 target: ReferenceTarget,
97 resolved_type: LemmaType,
98 local_constraints: Option<Vec<Constraint>>,
99 local_default: Option<ValueKind>,
100 }
101
102 let mut schema: HashMap<DataPath, LemmaType> = HashMap::new();
103 let mut declared_defaults: HashMap<DataPath, ValueKind> = HashMap::new();
104 let mut values: HashMap<DataPath, LiteralValue> = HashMap::new();
105 let mut spec_arcs: HashMap<DataPath, Arc<LemmaSpec>> = HashMap::new();
106 let mut references: HashMap<DataPath, PendingReference> = HashMap::new();
107
108 for (path, rfv) in self.data.iter() {
109 match rfv {
110 DataDefinition::Value { value, .. } => {
111 values.insert(path.clone(), value.clone());
112 schema.insert(path.clone(), value.lemma_type.clone());
113 }
114 DataDefinition::TypeDeclaration {
115 resolved_type,
116 declared_default,
117 ..
118 } => {
119 schema.insert(path.clone(), resolved_type.clone());
120 if let Some(dv) = declared_default {
121 declared_defaults.insert(path.clone(), dv.clone());
122 }
123 }
124 DataDefinition::Import { spec: spec_arc, .. } => {
125 spec_arcs.insert(path.clone(), Arc::clone(spec_arc));
126 }
127 DataDefinition::Reference {
128 target,
129 resolved_type,
130 local_constraints,
131 local_default,
132 ..
133 } => {
134 schema.insert(path.clone(), resolved_type.clone());
135 references.insert(
136 path.clone(),
137 PendingReference {
138 target: target.clone(),
139 resolved_type: resolved_type.clone(),
140 local_constraints: local_constraints.clone(),
141 local_default: local_default.clone(),
142 },
143 );
144 }
145 }
146 }
147
148 for (path, value) in values.iter_mut() {
149 let Some(schema_type) = schema.get(path).cloned() else {
150 continue;
151 };
152 match Self::coerce_literal_to_schema_type(value, &schema_type) {
153 Ok(coerced) => *value = coerced,
154 Err(msg) => unreachable!("Data {} incompatible: {}", path, msg),
155 }
156 }
157
158 let mut data = IndexMap::new();
159 for (path, rfv) in &self.data {
160 let source = rfv.source().clone();
161 if let Some(spec_arc) = spec_arcs.remove(path) {
162 data.insert(
163 path.clone(),
164 DataDefinition::Import {
165 spec: spec_arc,
166 source,
167 },
168 );
169 } else if let Some(pending) = references.remove(path) {
170 data.insert(
171 path.clone(),
172 DataDefinition::Reference {
173 target: pending.target,
174 resolved_type: pending.resolved_type,
175 local_constraints: pending.local_constraints,
176 local_default: pending.local_default,
177 source,
178 },
179 );
180 } else if let Some(value) = values.remove(path) {
181 data.insert(path.clone(), DataDefinition::Value { value, source });
182 } else {
183 let resolved_type = schema
184 .get(path)
185 .cloned()
186 .expect("non-spec-ref data has schema (value, reference, or type-only)");
187 let declared_default = declared_defaults.remove(path);
188 data.insert(
189 path.clone(),
190 DataDefinition::TypeDeclaration {
191 resolved_type,
192 declared_default,
193 source,
194 },
195 );
196 }
197 }
198 data
199 }
200
201 pub(crate) fn coerce_literal_to_schema_type(
202 lit: &LiteralValue,
203 schema_type: &LemmaType,
204 ) -> Result<LiteralValue, String> {
205 if lit.lemma_type.specifications == schema_type.specifications {
206 let mut out = lit.clone();
207 out.lemma_type = schema_type.clone();
208 return Ok(out);
209 }
210 match (&schema_type.specifications, &lit.value) {
211 (TypeSpecification::Number { .. }, ValueKind::Number(_))
212 | (TypeSpecification::Text { .. }, ValueKind::Text(_))
213 | (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
214 | (TypeSpecification::Date { .. }, ValueKind::Date(_))
215 | (TypeSpecification::Time { .. }, ValueKind::Time(_))
216 | (TypeSpecification::Duration { .. }, ValueKind::Duration(_, _))
217 | (TypeSpecification::Ratio { .. }, ValueKind::Ratio(_, _))
218 | (TypeSpecification::Scale { .. }, ValueKind::Scale(_, _)) => {
219 let mut out = lit.clone();
220 out.lemma_type = schema_type.clone();
221 Ok(out)
222 }
223 (TypeSpecification::Ratio { .. }, ValueKind::Number(n)) => {
224 Ok(LiteralValue::ratio_with_type(*n, None, schema_type.clone()))
225 }
226 _ => Err(format!(
227 "value {} cannot be used as type {}",
228 lit,
229 schema_type.name()
230 )),
231 }
232 }
233
234 fn resolve_data_reference_types(&mut self) -> Result<(), Vec<Error>> {
246 let mut errors: Vec<Error> = Vec::new();
247 let mut updates: Vec<(DataPath, LemmaType, Option<ValueKind>)> = Vec::new();
248
249 for (reference_path, entry) in &self.data {
250 let DataDefinition::Reference {
251 target,
252 resolved_type: provisional,
253 local_constraints,
254 source,
255 ..
256 } = entry
257 else {
258 continue;
259 };
260
261 let target_data_path = match target {
262 ReferenceTarget::Data(path) => path,
263 ReferenceTarget::Rule(_) => continue,
264 };
265
266 let Some(target_entry) = self.data.get(target_data_path) else {
267 errors.push(reference_error(
268 &self.main_spec,
269 source,
270 format!(
271 "Data reference '{}' target '{}' does not exist",
272 reference_path, target_data_path
273 ),
274 ));
275 continue;
276 };
277
278 let Some(target_type) = target_entry.schema_type().cloned() else {
279 errors.push(reference_error(
280 &self.main_spec,
281 source,
282 format!(
283 "Data reference '{}' target '{}' is a spec reference and cannot carry a value",
284 reference_path, target_data_path
285 ),
286 ));
287 continue;
288 };
289
290 let lhs_declared_type: Option<&LemmaType> = if provisional.is_undetermined() {
291 None
292 } else {
293 Some(provisional)
294 };
295
296 if let Some(lhs) = lhs_declared_type {
297 if let Some(msg) = reference_kind_mismatch_message(
298 lhs,
299 &target_type,
300 reference_path,
301 target_data_path,
302 "target",
303 ) {
304 errors.push(reference_error(&self.main_spec, source, msg));
305 continue;
306 }
307 }
308
309 let mut merged = match lhs_declared_type {
314 Some(lhs) => lhs.clone(),
315 None => target_type.clone(),
316 };
317 let mut captured_default: Option<ValueKind> = None;
318 if let Some(constraints) = local_constraints {
319 match apply_constraints_to_spec(
320 &self.main_spec,
321 merged.specifications.clone(),
322 constraints,
323 source,
324 &mut captured_default,
325 ) {
326 Ok(specs) => merged.specifications = specs,
327 Err(errs) => {
328 errors.extend(errs);
329 continue;
330 }
331 }
332 }
333
334 updates.push((reference_path.clone(), merged, captured_default));
335 }
336
337 for (path, new_type, new_default) in updates {
338 if let Some(DataDefinition::Reference {
339 resolved_type,
340 local_default,
341 ..
342 }) = self.data.get_mut(&path)
343 {
344 *resolved_type = new_type;
345 if new_default.is_some() {
346 *local_default = new_default;
347 }
348 } else {
349 unreachable!("BUG: reference path disappeared between collect and update phases");
350 }
351 }
352
353 if errors.is_empty() {
354 Ok(())
355 } else {
356 Err(errors)
357 }
358 }
359
360 fn resolve_rule_reference_types(
369 &mut self,
370 computed_rule_types: &HashMap<RulePath, LemmaType>,
371 ) -> Result<(), Vec<Error>> {
372 let mut errors: Vec<Error> = Vec::new();
373 let mut updates: Vec<(DataPath, LemmaType, Option<ValueKind>)> = Vec::new();
374
375 for (reference_path, entry) in &self.data {
376 let DataDefinition::Reference {
377 target,
378 resolved_type: provisional,
379 local_constraints,
380 source,
381 ..
382 } = entry
383 else {
384 continue;
385 };
386
387 let target_rule_path = match target {
388 ReferenceTarget::Rule(path) => path,
389 ReferenceTarget::Data(_) => continue,
390 };
391
392 let Some(target_type) = computed_rule_types.get(target_rule_path) else {
393 errors.push(reference_error(
394 &self.main_spec,
395 source,
396 format!(
397 "Data reference '{}' target rule '{}' does not exist",
398 reference_path, target_rule_path
399 ),
400 ));
401 continue;
402 };
403
404 if target_type.vetoed() || target_type.is_undetermined() {
409 let mut merged = target_type.clone();
410 let mut captured_default: Option<ValueKind> = None;
411 if let Some(constraints) = local_constraints {
412 match apply_constraints_to_spec(
413 &self.main_spec,
414 merged.specifications.clone(),
415 constraints,
416 source,
417 &mut captured_default,
418 ) {
419 Ok(specs) => merged.specifications = specs,
420 Err(errs) => {
421 errors.extend(errs);
422 continue;
423 }
424 }
425 }
426 updates.push((reference_path.clone(), merged, captured_default));
427 continue;
428 }
429
430 let lhs_declared_type: Option<&LemmaType> = if provisional.is_undetermined() {
431 None
432 } else {
433 Some(provisional)
434 };
435
436 if let Some(lhs) = lhs_declared_type {
437 if let Some(msg) = reference_kind_mismatch_message(
438 lhs,
439 target_type,
440 reference_path,
441 target_rule_path,
442 "target rule",
443 ) {
444 errors.push(reference_error(&self.main_spec, source, msg));
445 continue;
446 }
447 }
448
449 let mut merged = match lhs_declared_type {
452 Some(lhs) => lhs.clone(),
453 None => target_type.clone(),
454 };
455 let mut captured_default: Option<ValueKind> = None;
456 if let Some(constraints) = local_constraints {
457 match apply_constraints_to_spec(
458 &self.main_spec,
459 merged.specifications.clone(),
460 constraints,
461 source,
462 &mut captured_default,
463 ) {
464 Ok(specs) => merged.specifications = specs,
465 Err(errs) => {
466 errors.extend(errs);
467 continue;
468 }
469 }
470 }
471
472 updates.push((reference_path.clone(), merged, captured_default));
473 }
474
475 for (path, new_type, new_default) in updates {
476 if let Some(DataDefinition::Reference {
477 resolved_type,
478 local_default,
479 ..
480 }) = self.data.get_mut(&path)
481 {
482 *resolved_type = new_type;
483 if new_default.is_some() {
484 *local_default = new_default;
485 }
486 } else {
487 unreachable!(
488 "BUG: rule-target reference path disappeared between collect and update phases"
489 );
490 }
491 }
492
493 if errors.is_empty() {
494 Ok(())
495 } else {
496 Err(errors)
497 }
498 }
499
500 fn add_rule_reference_dependency_edges(&mut self) {
510 let reference_to_rule: HashMap<DataPath, RulePath> =
511 self.transitive_reference_to_rule_map();
512
513 if reference_to_rule.is_empty() {
514 return;
515 }
516
517 let mut updates: Vec<(RulePath, RulePath)> = Vec::new();
518 for (rule_path, rule_node) in &self.rules {
519 let mut found: BTreeSet<RulePath> = BTreeSet::new();
520 for (cond, result) in &rule_node.branches {
521 if let Some(c) = cond {
522 collect_rule_reference_dependencies(c, &reference_to_rule, &mut found);
523 }
524 collect_rule_reference_dependencies(result, &reference_to_rule, &mut found);
525 }
526 for target in found {
527 updates.push((rule_path.clone(), target));
528 }
529 }
530
531 for (rule_path, target) in updates {
532 if let Some(node) = self.rules.get_mut(&rule_path) {
533 node.depends_on_rules.insert(target);
534 }
535 }
536 }
537
538 fn transitive_reference_to_rule_map(&self) -> HashMap<DataPath, RulePath> {
545 let mut out: HashMap<DataPath, RulePath> = HashMap::new();
546 for (path, def) in &self.data {
547 if !matches!(def, DataDefinition::Reference { .. }) {
548 continue;
549 }
550 let mut visited: HashSet<DataPath> = HashSet::new();
551 let mut cursor: DataPath = path.clone();
552 loop {
553 if !visited.insert(cursor.clone()) {
554 break;
555 }
556 let Some(DataDefinition::Reference { target, .. }) = self.data.get(&cursor) else {
557 break;
558 };
559 match target {
560 ReferenceTarget::Data(next) => cursor = next.clone(),
561 ReferenceTarget::Rule(rule_path) => {
562 out.insert(path.clone(), rule_path.clone());
563 break;
564 }
565 }
566 }
567 }
568 out
569 }
570
571 fn compute_reference_evaluation_order(&self) -> Result<Vec<DataPath>, Vec<Error>> {
578 let reference_paths: Vec<DataPath> = self
579 .data
580 .iter()
581 .filter_map(|(p, d)| match d {
582 DataDefinition::Reference {
583 target: ReferenceTarget::Data(_),
584 ..
585 } => Some(p.clone()),
586 _ => None,
587 })
588 .collect();
589
590 if reference_paths.is_empty() {
591 return Ok(Vec::new());
592 }
593
594 let reference_set: BTreeSet<DataPath> = reference_paths.iter().cloned().collect();
595 let mut in_degree: BTreeMap<DataPath, usize> = BTreeMap::new();
596 let mut dependents: BTreeMap<DataPath, Vec<DataPath>> = BTreeMap::new();
597 for p in &reference_paths {
598 in_degree.insert(p.clone(), 0);
599 dependents.insert(p.clone(), Vec::new());
600 }
601
602 for p in &reference_paths {
603 let Some(DataDefinition::Reference { target, .. }) = self.data.get(p) else {
604 unreachable!("BUG: reference entry lost between collect and walk");
605 };
606 if let ReferenceTarget::Data(target_path) = target {
607 if reference_set.contains(target_path) {
608 *in_degree
609 .get_mut(p)
610 .expect("BUG: reference missing in_degree") += 1;
611 dependents
612 .get_mut(target_path)
613 .expect("BUG: reference missing dependents list")
614 .push(p.clone());
615 }
616 }
617 }
618
619 let mut queue: VecDeque<DataPath> = in_degree
620 .iter()
621 .filter(|(_, d)| **d == 0)
622 .map(|(p, _)| p.clone())
623 .collect();
624
625 let mut result: Vec<DataPath> = Vec::new();
626 while let Some(path) = queue.pop_front() {
627 result.push(path.clone());
628 if let Some(deps) = dependents.get(&path) {
629 for dependent in deps.clone() {
630 let degree = in_degree
631 .get_mut(&dependent)
632 .expect("BUG: reference dependent missing in_degree");
633 *degree -= 1;
634 if *degree == 0 {
635 queue.push_back(dependent);
636 }
637 }
638 }
639 }
640
641 if result.len() != reference_paths.len() {
642 let cycle_members: Vec<DataPath> = reference_paths
643 .iter()
644 .filter(|p| !result.contains(p))
645 .cloned()
646 .collect();
647 let cycle_display: String = cycle_members
648 .iter()
649 .map(|p| p.to_string())
650 .collect::<Vec<_>>()
651 .join(", ");
652 let errors: Vec<Error> = cycle_members
653 .iter()
654 .filter_map(|p| {
655 self.data.get(p).map(|entry| {
656 reference_error(
657 &self.main_spec,
658 entry.source(),
659 format!("Circular data reference ({})", cycle_display),
660 )
661 })
662 })
663 .collect();
664 return Err(errors);
665 }
666
667 Ok(result)
668 }
669
670 fn topological_sort(&self) -> Result<Vec<RulePath>, Vec<Error>> {
671 let mut in_degree: BTreeMap<RulePath, usize> = BTreeMap::new();
672 let mut dependents: BTreeMap<RulePath, Vec<RulePath>> = BTreeMap::new();
673 let mut queue = VecDeque::new();
674 let mut result = Vec::new();
675
676 for rule_path in self.rules.keys() {
677 in_degree.insert(rule_path.clone(), 0);
678 dependents.insert(rule_path.clone(), Vec::new());
679 }
680
681 for (rule_path, rule_node) in &self.rules {
682 for dependency in &rule_node.depends_on_rules {
683 if self.rules.contains_key(dependency) {
684 if let Some(degree) = in_degree.get_mut(rule_path) {
685 *degree += 1;
686 }
687 if let Some(deps) = dependents.get_mut(dependency) {
688 deps.push(rule_path.clone());
689 }
690 }
691 }
692 }
693
694 for (rule_path, degree) in &in_degree {
695 if *degree == 0 {
696 queue.push_back(rule_path.clone());
697 }
698 }
699
700 while let Some(rule_path) = queue.pop_front() {
701 result.push(rule_path.clone());
702
703 if let Some(dependent_rules) = dependents.get(&rule_path) {
704 for dependent in dependent_rules {
705 if let Some(degree) = in_degree.get_mut(dependent) {
706 *degree -= 1;
707 if *degree == 0 {
708 queue.push_back(dependent.clone());
709 }
710 }
711 }
712 }
713 }
714
715 if result.len() != self.rules.len() {
716 let missing: Vec<RulePath> = self
717 .rules
718 .keys()
719 .filter(|rule| !result.contains(rule))
720 .cloned()
721 .collect();
722 let cycle: Vec<Source> = missing
723 .iter()
724 .filter_map(|rule| self.rules.get(rule).map(|n| n.source.clone()))
725 .collect();
726
727 if cycle.is_empty() {
728 unreachable!(
729 "BUG: circular dependency detected but no sources could be collected ({} missing rules)",
730 missing.len()
731 );
732 }
733 let rules_involved: String = missing
734 .iter()
735 .map(|rp| rp.rule.as_str())
736 .collect::<Vec<_>>()
737 .join(", ");
738 let message = format!("Circular dependency (rules: {})", rules_involved);
739 let errors: Vec<Error> = cycle
740 .into_iter()
741 .map(|source| {
742 Error::validation_with_context(
743 message.clone(),
744 Some(source),
745 None::<String>,
746 Some(Arc::clone(&self.main_spec)),
747 None,
748 )
749 })
750 .collect();
751 return Err(errors);
752 }
753
754 Ok(result)
755 }
756}
757
758#[derive(Debug)]
759pub(crate) struct RuleNode {
760 pub branches: Vec<(Option<Expression>, Expression)>,
763 pub source: Source,
764
765 pub depends_on_rules: BTreeSet<RulePath>,
766
767 pub rule_type: LemmaType,
770
771 pub spec_arc: Arc<LemmaSpec>,
773}
774
775type ResolvedTypesMap = Vec<(Arc<LemmaRepository>, Arc<LemmaSpec>, ResolvedSpecTypes)>;
776
777struct GraphBuilder<'a> {
778 data: IndexMap<DataPath, DataDefinition>,
779 rules: BTreeMap<RulePath, RuleNode>,
780 context: &'a Context,
781 local_types: ResolvedTypesMap,
782 errors: Vec<Error>,
783 main_spec: Arc<LemmaSpec>,
784 main_repository: Arc<ast::LemmaRepository>,
785}
786
787fn reference_error(main_spec: &Arc<LemmaSpec>, source: &Source, message: String) -> Error {
788 Error::validation_with_context(
789 message,
790 Some(source.clone()),
791 None::<String>,
792 Some(Arc::clone(main_spec)),
793 None,
794 )
795}
796
797fn reference_kind_mismatch_message<P: fmt::Display>(
814 lhs: &LemmaType,
815 target_type: &LemmaType,
816 reference_path: &DataPath,
817 target_path: &P,
818 target_kind_label: &str,
819) -> Option<String> {
820 if !lhs.has_same_base_type(target_type) {
821 return Some(format!(
822 "Data reference '{}' type mismatch: declared as '{}' but {} '{}' is '{}'",
823 reference_path,
824 lhs.name(),
825 target_kind_label,
826 target_path,
827 target_type.name(),
828 ));
829 }
830 if lhs.is_scale() && !lhs.same_scale_family(target_type) {
831 let lhs_family = lhs.scale_family_name().expect(
832 "BUG: declared scale data must carry a family name; \
833 anonymous scale types only arise from runtime synthesis \
834 and never appear as a reference's LHS-declared type",
835 );
836 let target_family = target_type.scale_family_name().expect(
837 "BUG: declared scale data must carry a family name; \
838 anonymous scale types only arise from runtime synthesis \
839 and never appear as a reference target's schema type",
840 );
841 return Some(format!(
842 "Data reference '{}' scale family mismatch: declared as '{}' (family '{}') but {} '{}' is '{}' (family '{}')",
843 reference_path,
844 lhs.name(),
845 lhs_family,
846 target_kind_label,
847 target_path,
848 target_type.name(),
849 target_family,
850 ));
851 }
852 None
853}
854
855fn apply_constraints_to_spec(
860 spec: &Arc<LemmaSpec>,
861 mut specs: TypeSpecification,
862 constraints: &[Constraint],
863 source: &crate::parsing::source::Source,
864 declared_default: &mut Option<ValueKind>,
865) -> Result<TypeSpecification, Vec<Error>> {
866 let mut errors = Vec::new();
867 for (command, args) in constraints {
868 let specs_clone = specs.clone();
869 let mut default_before = declared_default.clone();
870 match specs.apply_constraint(*command, args, &mut default_before) {
871 Ok(updated_specs) => {
872 specs = updated_specs;
873 *declared_default = default_before;
874 }
875 Err(e) => {
876 errors.push(Error::validation_with_context(
877 format!("Failed to apply constraint '{}': {}", command, e),
878 Some(source.clone()),
879 None::<String>,
880 Some(Arc::clone(spec)),
881 None,
882 ));
883 specs = specs_clone;
884 }
885 }
886 }
887 if !errors.is_empty() {
888 return Err(errors);
889 }
890 Ok(specs)
891}
892
893impl Graph {
894 pub(crate) fn build(
896 context: &Context,
897 repository: &Arc<LemmaRepository>,
898 main_spec: &Arc<LemmaSpec>,
899 dag: &[(Arc<LemmaRepository>, Arc<LemmaSpec>)],
900 effective: &EffectiveDate,
901 ) -> Result<(Graph, ResolvedTypesMap), Vec<Error>> {
902 let mut type_resolver = TypeResolver::new(context);
903
904 let mut type_errors: Vec<Error> = Vec::new();
905 for (repo, spec) in dag {
906 type_errors.extend(type_resolver.register_all(repo, spec));
907 }
908
909 let (data, rules, graph_errors, local_types) = {
910 let mut builder = GraphBuilder {
911 data: IndexMap::new(),
912 rules: BTreeMap::new(),
913 context,
914 local_types: Vec::new(),
915 errors: Vec::new(),
916 main_spec: Arc::clone(main_spec),
917 main_repository: Arc::clone(repository),
918 };
919
920 builder.build_spec(
921 main_spec,
922 repository,
923 Vec::new(),
924 HashMap::new(),
925 effective,
926 &mut type_resolver,
927 )?;
928
929 (
930 builder.data,
931 builder.rules,
932 builder.errors,
933 builder.local_types,
934 )
935 };
936
937 let mut graph = Graph {
938 data,
939 rules,
940 execution_order: Vec::new(),
941 reference_evaluation_order: Vec::new(),
942 main_spec: Arc::clone(main_spec),
943 };
944
945 let validation_errors = match graph.validate(&local_types) {
946 Ok(()) => Vec::new(),
947 Err(errors) => errors,
948 };
949
950 let mut all_errors = type_errors;
951 all_errors.extend(graph_errors);
952 all_errors.extend(validation_errors);
953
954 if all_errors.is_empty() {
955 Ok((graph, local_types))
956 } else {
957 Err(all_errors)
958 }
959 }
960
961 fn validate(&mut self, resolved_types: &ResolvedTypesMap) -> Result<(), Vec<Error>> {
962 let mut errors = Vec::new();
963
964 if let Err(structural_errors) = check_all_rule_references_exist(self) {
966 errors.extend(structural_errors);
967 }
968 if let Err(collision_errors) = check_data_and_rule_name_collisions(self) {
969 errors.extend(collision_errors);
970 }
971
972 if let Err(reference_errors) = self.resolve_data_reference_types() {
976 errors.extend(reference_errors);
977 }
978
979 let reference_order = match self.compute_reference_evaluation_order() {
983 Ok(order) => order,
984 Err(circular_errors) => {
985 errors.extend(circular_errors);
986 return Err(errors);
987 }
988 };
989
990 self.add_rule_reference_dependency_edges();
996
997 let execution_order = match self.topological_sort() {
998 Ok(order) => order,
999 Err(circular_errors) => {
1000 errors.extend(circular_errors);
1001 return Err(errors);
1002 }
1003 };
1004
1005 let inferred_types = infer_rule_types(self, &execution_order, resolved_types);
1013
1014 if let Err(rule_reference_errors) = self.resolve_rule_reference_types(&inferred_types) {
1019 errors.extend(rule_reference_errors);
1020 }
1021
1022 if let Err(type_errors) =
1024 check_rule_types(self, &execution_order, &inferred_types, resolved_types)
1025 {
1026 errors.extend(type_errors);
1027 }
1028
1029 if !errors.is_empty() {
1030 return Err(errors);
1031 }
1032
1033 apply_inferred_types(self, inferred_types);
1035 self.execution_order = execution_order;
1036 self.reference_evaluation_order = reference_order;
1037 Ok(())
1038 }
1039}
1040
1041impl<'a> GraphBuilder<'a> {
1042 fn engine_error(&self, message: impl Into<String>, source: &Source) -> Error {
1043 Error::validation_with_context(
1044 message.into(),
1045 Some(source.clone()),
1046 None::<String>,
1047 Some(Arc::clone(&self.main_spec)),
1048 None,
1049 )
1050 }
1051
1052 fn process_meta_fields(&mut self, spec: &LemmaSpec) {
1053 let mut seen = HashSet::new();
1054 for field in &spec.meta_fields {
1055 if field.key == "title" && !matches!(field.value, MetaValue::Literal(Value::Text(_))) {
1057 self.errors.push(self.engine_error(
1058 "Meta 'title' must be a text literal",
1059 &field.source_location,
1060 ));
1061 }
1062
1063 if !seen.insert(field.key.clone()) {
1064 self.errors.push(self.engine_error(
1065 format!("Duplicate meta key '{}'", field.key),
1066 &field.source_location,
1067 ));
1068 }
1069 }
1070 }
1071
1072 fn resolve_spec_ref(
1073 &self,
1074 spec_ref: &ast::SpecRef,
1075 effective: &EffectiveDate,
1076 ) -> Result<(Arc<LemmaRepository>, Arc<LemmaSpec>), Error> {
1077 discovery::resolve_spec_ref(
1078 self.context,
1079 spec_ref,
1080 &self.main_repository,
1081 effective,
1082 &self.main_spec.name,
1083 None,
1084 Some(Arc::clone(&self.main_spec)),
1085 )
1086 }
1087
1088 fn resolve_data_binding(
1095 &mut self,
1096 data: &LemmaData,
1097 current_segment_names: &[String],
1098 parent_spec: &Arc<LemmaSpec>,
1099 effective: &EffectiveDate,
1100 ) -> Option<(Vec<String>, BindingValue, Source)> {
1101 let binding_path_display = format!(
1102 "{}.{}",
1103 data.reference.segments.join("."),
1104 data.reference.name
1105 );
1106
1107 let mut walk_spec = Arc::clone(parent_spec);
1108
1109 for segment in &data.reference.segments {
1110 let Some(seg_data) = walk_spec
1111 .data
1112 .iter()
1113 .find(|f| f.reference.segments.is_empty() && f.reference.name == *segment)
1114 else {
1115 self.errors.push(self.engine_error(
1116 format!(
1117 "Data binding path '{}': data '{}' not found in spec '{}'",
1118 binding_path_display, segment, walk_spec.name
1119 ),
1120 &data.source_location,
1121 ));
1122 return None;
1123 };
1124
1125 let spec_ref = match &seg_data.value {
1126 ParsedDataValue::Import(sr) => sr,
1127 _ => {
1128 self.errors.push(self.engine_error(
1129 format!(
1130 "Data binding path '{}': '{}' in spec '{}' is not a spec reference",
1131 binding_path_display, segment, walk_spec.name
1132 ),
1133 &data.source_location,
1134 ));
1135 return None;
1136 }
1137 };
1138
1139 walk_spec = match self.resolve_spec_ref(spec_ref, effective) {
1140 Ok((_, arc)) => arc,
1141 Err(e) => {
1142 self.errors.push(e);
1143 return None;
1144 }
1145 };
1146 }
1147
1148 let mut binding_key: Vec<String> = current_segment_names.to_vec();
1150 binding_key.extend(data.reference.segments.iter().cloned());
1151 binding_key.push(data.reference.name.clone());
1152
1153 let binding_value = match &data.value {
1154 ParsedDataValue::Definition { value: Some(v), .. }
1155 if data.value.is_definition_literal_only() =>
1156 {
1157 BindingValue::Literal(v.clone())
1158 }
1159 ParsedDataValue::Reference {
1160 target,
1161 constraints,
1162 } => {
1163 let resolved_target = self.resolve_reference_target_in_spec(
1164 target,
1165 &data.source_location,
1166 parent_spec,
1167 current_segment_names,
1168 effective,
1169 )?;
1170 BindingValue::Reference {
1171 target: resolved_target,
1172 constraints: constraints.clone(),
1173 }
1174 }
1175 ParsedDataValue::Import(_) => {
1176 unreachable!(
1177 "BUG: build_data_bindings must reject Import bindings before calling resolve_data_binding"
1178 );
1179 }
1180 ParsedDataValue::Definition { .. } => {
1181 unreachable!(
1182 "BUG: build_data_bindings must reject non-literal Definition bindings before calling resolve_data_binding"
1183 );
1184 }
1185 };
1186
1187 Some((binding_key, binding_value, data.source_location.clone()))
1188 }
1189
1190 fn resolve_reference_target_in_spec(
1197 &mut self,
1198 reference: &ast::Reference,
1199 reference_source: &Source,
1200 containing_spec_arc: &Arc<LemmaSpec>,
1201 containing_segments_names: &[String],
1202 effective: &EffectiveDate,
1203 ) -> Option<ReferenceTarget> {
1204 let containing_data_map: HashMap<String, LemmaData> = containing_spec_arc
1205 .data
1206 .iter()
1207 .filter(|d| d.reference.is_local())
1208 .map(|d| (d.reference.name.clone(), d.clone()))
1209 .collect();
1210
1211 let containing_rule_names: HashSet<&str> = containing_spec_arc
1212 .rules
1213 .iter()
1214 .map(|r| r.name.as_str())
1215 .collect();
1216
1217 let containing_segments: Vec<PathSegment> = containing_segments_names
1218 .iter()
1219 .map(|name| PathSegment {
1220 data: name.clone(),
1221 spec: containing_spec_arc.name.clone(),
1222 })
1223 .collect();
1224
1225 if reference.segments.is_empty() {
1226 let is_data = containing_data_map.contains_key(&reference.name);
1227 let is_rule = containing_rule_names.contains(reference.name.as_str());
1228 if is_data && is_rule {
1229 self.errors.push(self.engine_error(
1230 format!(
1231 "Reference target '{}' is ambiguous: both a data and a rule in spec '{}'",
1232 reference.name, containing_spec_arc.name
1233 ),
1234 reference_source,
1235 ));
1236 return None;
1237 }
1238 if is_data {
1239 return Some(ReferenceTarget::Data(DataPath {
1240 segments: containing_segments,
1241 data: reference.name.clone(),
1242 }));
1243 }
1244 if is_rule {
1245 return Some(ReferenceTarget::Rule(RulePath {
1246 segments: containing_segments,
1247 rule: reference.name.clone(),
1248 }));
1249 }
1250 self.errors.push(self.engine_error(
1251 format!(
1252 "Reference target '{}' not found in spec '{}'",
1253 reference.name, containing_spec_arc.name
1254 ),
1255 reference_source,
1256 ));
1257 return None;
1258 }
1259
1260 let (resolved_segments, target_spec_arc) = self.resolve_path_segments(
1261 &reference.segments,
1262 reference_source,
1263 containing_data_map,
1264 containing_segments,
1265 effective,
1266 )?;
1267
1268 let target_data_names: HashSet<&str> = target_spec_arc
1269 .data
1270 .iter()
1271 .filter(|d| d.reference.is_local())
1272 .map(|d| d.reference.name.as_str())
1273 .collect();
1274 let target_rule_names: HashSet<&str> = target_spec_arc
1275 .rules
1276 .iter()
1277 .map(|r| r.name.as_str())
1278 .collect();
1279 let is_data = target_data_names.contains(reference.name.as_str());
1280 let is_rule = target_rule_names.contains(reference.name.as_str());
1281
1282 if is_data && is_rule {
1283 self.errors.push(self.engine_error(
1284 format!(
1285 "Reference target '{}' is ambiguous: both a data and a rule in spec '{}'",
1286 reference.name, target_spec_arc.name
1287 ),
1288 reference_source,
1289 ));
1290 return None;
1291 }
1292 if is_data {
1293 return Some(ReferenceTarget::Data(DataPath {
1294 segments: resolved_segments,
1295 data: reference.name.clone(),
1296 }));
1297 }
1298 if is_rule {
1299 return Some(ReferenceTarget::Rule(RulePath {
1300 segments: resolved_segments,
1301 rule: reference.name.clone(),
1302 }));
1303 }
1304
1305 self.errors.push(self.engine_error(
1306 format!(
1307 "Reference target '{}' not found in spec '{}'",
1308 reference.name, target_spec_arc.name
1309 ),
1310 reference_source,
1311 ));
1312 None
1313 }
1314
1315 fn build_data_bindings(
1321 &mut self,
1322 spec: &LemmaSpec,
1323 current_segment_names: &[String],
1324 spec_arc: &Arc<LemmaSpec>,
1325 effective: &EffectiveDate,
1326 ) -> Result<DataBindings, Vec<Error>> {
1327 let mut bindings: DataBindings = HashMap::new();
1328 let mut errors: Vec<Error> = Vec::new();
1329
1330 for data in &spec.data {
1331 if data.reference.segments.is_empty() {
1332 continue; }
1334
1335 let binding_path_display = format!(
1336 "{}.{}",
1337 data.reference.segments.join("."),
1338 data.reference.name
1339 );
1340
1341 if matches!(&data.value, ParsedDataValue::Import(_)) {
1343 errors.push(self.engine_error(
1344 format!(
1345 "Data binding '{}' cannot override a spec reference — only literal values can be bound to nested data",
1346 binding_path_display
1347 ),
1348 &data.source_location,
1349 ));
1350 continue;
1351 }
1352
1353 if let ParsedDataValue::Definition { .. } = &data.value {
1355 if !data.value.is_definition_literal_only() {
1356 errors.push(self.engine_error(
1357 format!(
1358 "Data binding '{}' must provide a literal value, not a data definition",
1359 binding_path_display
1360 ),
1361 &data.source_location,
1362 ));
1363 continue;
1364 }
1365 }
1366
1367 if let Some((binding_key, binding_value, source)) =
1368 self.resolve_data_binding(data, current_segment_names, spec_arc, effective)
1369 {
1370 if let Some((_, existing_source)) = bindings.get(&binding_key) {
1371 errors.push(self.engine_error(
1372 format!(
1373 "Duplicate data binding for '{}' (previously bound at {}:{})",
1374 binding_key.join("."),
1375 existing_source.source_type,
1376 existing_source.span.line
1377 ),
1378 &data.source_location,
1379 ));
1380 } else {
1381 bindings.insert(binding_key, (binding_value, source));
1382 }
1383 }
1384 }
1386
1387 if !errors.is_empty() {
1388 return Err(errors);
1389 }
1390
1391 Ok(bindings)
1392 }
1393
1394 fn add_data(
1400 &mut self,
1401 data: &LemmaData,
1402 current_segments: &[PathSegment],
1403 data_bindings: &DataBindings,
1404 current_spec_arc: &Arc<LemmaSpec>,
1405 used_binding_keys: &mut HashSet<Vec<String>>,
1406 effective: &EffectiveDate,
1407 ) {
1408 let data_path = DataPath {
1409 segments: current_segments.to_vec(),
1410 data: data.reference.name.clone(),
1411 };
1412
1413 if self.data.contains_key(&data_path) {
1415 self.errors.push(self.engine_error(
1416 format!("Duplicate data '{}'", data_path.data),
1417 &data.source_location,
1418 ));
1419 return;
1420 }
1421
1422 let binding_key: Vec<String> = current_segments
1424 .iter()
1425 .map(|s| s.data.clone())
1426 .chain(std::iter::once(data.reference.name.clone()))
1427 .collect();
1428
1429 let binding_override: Option<(BindingValue, Source)> =
1433 data_bindings.get(&binding_key).map(|(v, s)| {
1434 used_binding_keys.insert(binding_key.clone());
1435 (v.clone(), s.clone())
1436 });
1437
1438 let (original_schema_type, original_declared_default) =
1439 if matches!(&data.value, ParsedDataValue::Definition { .. })
1440 && data.value.definition_needs_type_resolution()
1441 {
1442 let resolved = self
1443 .local_types
1444 .iter()
1445 .find(|(_, s, _)| Arc::ptr_eq(s, current_spec_arc))
1446 .map(|(_, _, t)| t)
1447 .expect("BUG: no resolved types for spec during add_local_data");
1448 let lemma_type = resolved
1449 .named_types
1450 .get(&data.reference.name)
1451 .expect("BUG: type not in named_types. TypeResolver should have registered it")
1452 .clone();
1453 let declared = resolved
1454 .declared_defaults
1455 .get(&data.reference.name)
1456 .cloned();
1457 (Some(lemma_type), declared)
1458 } else {
1459 (None, None)
1460 };
1461
1462 if let Some((binding_value, binding_source)) = binding_override {
1463 self.add_data_from_binding(
1464 data_path,
1465 binding_value,
1466 binding_source,
1467 original_schema_type,
1468 current_spec_arc,
1469 );
1470 return;
1471 }
1472
1473 let effective_source = data.source_location.clone();
1474
1475 match &data.value {
1476 ParsedDataValue::Definition { .. } if data.value.is_definition_literal_only() => {
1477 let ParsedDataValue::Definition {
1478 value: Some(value), ..
1479 } = &data.value
1480 else {
1481 unreachable!("BUG: literal-only Definition must carry value");
1482 };
1483 self.insert_literal_data(
1484 data_path,
1485 value,
1486 original_schema_type,
1487 effective_source,
1488 current_spec_arc,
1489 );
1490 }
1491 ParsedDataValue::Definition { .. } => {
1492 let resolved_type = original_schema_type.unwrap_or_else(|| {
1493 unreachable!(
1494 "BUG: Definition without schema — TypeResolver should have registered it"
1495 )
1496 });
1497
1498 self.data.insert(
1499 data_path,
1500 DataDefinition::TypeDeclaration {
1501 resolved_type,
1502 declared_default: original_declared_default,
1503 source: effective_source,
1504 },
1505 );
1506 }
1507 ParsedDataValue::Import(spec_ref) => {
1508 let effective_spec_arc = match self.resolve_spec_ref(spec_ref, effective) {
1509 Ok((_, arc)) => arc,
1510 Err(e) => {
1511 self.errors.push(e);
1512 return;
1513 }
1514 };
1515
1516 self.data.insert(
1517 data_path,
1518 DataDefinition::Import {
1519 spec: Arc::clone(&effective_spec_arc),
1520 source: effective_source,
1521 },
1522 );
1523 }
1524 ParsedDataValue::Reference {
1525 target,
1526 constraints,
1527 } => {
1528 let current_segment_names: Vec<String> =
1529 current_segments.iter().map(|s| s.data.clone()).collect();
1530 let Some(resolved_target) = self.resolve_reference_target_in_spec(
1531 target,
1532 &effective_source,
1533 current_spec_arc,
1534 ¤t_segment_names,
1535 effective,
1536 ) else {
1537 return;
1538 };
1539 let provisional_type = original_schema_type
1540 .clone()
1541 .unwrap_or_else(LemmaType::undetermined_type);
1542 self.data.insert(
1543 data_path,
1544 DataDefinition::Reference {
1545 target: resolved_target,
1546 resolved_type: provisional_type,
1547 local_constraints: constraints.clone(),
1548 local_default: None,
1549 source: effective_source,
1550 },
1551 );
1552 }
1553 }
1554 }
1555
1556 fn insert_literal_data(
1560 &mut self,
1561 data_path: DataPath,
1562 value: &ast::Value,
1563 declared_schema_type: Option<LemmaType>,
1564 effective_source: Source,
1565 current_spec_arc: &Arc<LemmaSpec>,
1566 ) {
1567 let semantic_value = match value_to_semantic(value) {
1568 Ok(s) => s,
1569 Err(e) => {
1570 self.errors.push(self.engine_error(e, &effective_source));
1571 return;
1572 }
1573 };
1574 let inferred_type = match value {
1575 Value::Text(_) => primitive_text().clone(),
1576 Value::Number(_) => primitive_number().clone(),
1577 Value::Scale(_, unit) => {
1578 match self
1579 .local_types
1580 .iter()
1581 .find(|(_, s, _)| Arc::ptr_eq(s, current_spec_arc))
1582 .map(|(_, _, t)| t)
1583 .and_then(|dt| dt.unit_index.get(unit))
1584 {
1585 Some(lt) => lt.clone(),
1586 None => {
1587 self.errors.push(self.engine_error(
1588 format!("Scale literal uses unknown unit '{}' for this spec", unit),
1589 &effective_source,
1590 ));
1591 return;
1592 }
1593 }
1594 }
1595 Value::Boolean(_) => primitive_boolean().clone(),
1596 Value::Date(_) => primitive_date().clone(),
1597 Value::Time(_) => primitive_time().clone(),
1598 Value::Duration(_, _) => primitive_duration().clone(),
1599 Value::Ratio(_, _) => primitive_ratio().clone(),
1600 };
1601 let schema_type = declared_schema_type.unwrap_or(inferred_type);
1602 let literal_value = LiteralValue {
1603 value: semantic_value,
1604 lemma_type: schema_type,
1605 };
1606 self.data.insert(
1607 data_path,
1608 DataDefinition::Value {
1609 value: literal_value,
1610 source: effective_source,
1611 },
1612 );
1613 }
1614
1615 fn add_data_from_binding(
1618 &mut self,
1619 data_path: DataPath,
1620 binding_value: BindingValue,
1621 binding_source: Source,
1622 declared_schema_type: Option<LemmaType>,
1623 current_spec_arc: &Arc<LemmaSpec>,
1624 ) {
1625 match binding_value {
1626 BindingValue::Literal(value) => {
1627 self.insert_literal_data(
1628 data_path,
1629 &value,
1630 declared_schema_type,
1631 binding_source,
1632 current_spec_arc,
1633 );
1634 }
1635 BindingValue::Reference {
1636 target,
1637 constraints,
1638 } => {
1639 let provisional_type =
1640 declared_schema_type.unwrap_or_else(LemmaType::undetermined_type);
1641 self.data.insert(
1642 data_path,
1643 DataDefinition::Reference {
1644 target,
1645 resolved_type: provisional_type,
1646 local_constraints: constraints,
1647 local_default: None,
1648 source: binding_source,
1649 },
1650 );
1651 }
1652 }
1653 }
1654
1655 fn resolve_path_segments(
1657 &mut self,
1658 segments: &[String],
1659 reference_source: &Source,
1660 mut current_data_map: HashMap<String, LemmaData>,
1661 mut path_segments: Vec<PathSegment>,
1662 effective: &EffectiveDate,
1663 ) -> Option<(Vec<PathSegment>, Arc<LemmaSpec>)> {
1664 let mut last_arc: Option<Arc<LemmaSpec>> = None;
1665
1666 for segment in segments.iter() {
1667 let data_ref =
1668 match current_data_map.get(segment) {
1669 Some(f) => f,
1670 None => {
1671 self.errors.push(self.engine_error(
1672 format!("Data '{}' not found", segment),
1673 reference_source,
1674 ));
1675 return None;
1676 }
1677 };
1678
1679 if let ParsedDataValue::Import(original_spec_ref) = &data_ref.value {
1680 let arc = match self.resolve_spec_ref(original_spec_ref, effective) {
1681 Ok((_, a)) => a,
1682 Err(e) => {
1683 self.errors.push(e);
1684 return None;
1685 }
1686 };
1687
1688 path_segments.push(PathSegment {
1689 data: segment.clone(),
1690 spec: arc.name.clone(),
1691 });
1692 current_data_map = arc
1693 .data
1694 .iter()
1695 .map(|f| (f.reference.name.clone(), f.clone()))
1696 .collect();
1697 last_arc = Some(arc);
1698 } else {
1699 self.errors.push(self.engine_error(
1700 format!("Data '{}' is not a spec reference", segment),
1701 reference_source,
1702 ));
1703 return None;
1704 }
1705 }
1706
1707 let final_arc = last_arc.unwrap_or_else(|| {
1708 unreachable!(
1709 "BUG: resolve_path_segments called with empty segments should not reach here"
1710 )
1711 });
1712 Some((path_segments, final_arc))
1713 }
1714
1715 fn build_spec(
1716 &mut self,
1717 spec_arc: &Arc<LemmaSpec>,
1718 spec_repository: &Arc<LemmaRepository>,
1719 current_segments: Vec<PathSegment>,
1720 data_bindings: DataBindings,
1721 effective: &EffectiveDate,
1722 type_resolver: &mut TypeResolver<'a>,
1723 ) -> Result<(), Vec<Error>> {
1724 let spec = spec_arc.as_ref();
1725
1726 if current_segments.is_empty() {
1727 self.process_meta_fields(spec);
1728 }
1729
1730 for data in spec.data.iter() {
1733 if let ParsedDataValue::Import(spec_ref) = &data.value {
1734 if spec_ref.name == spec.name {
1735 self.errors.push(self.engine_error(
1736 format!(
1737 "spec '{}' cannot reference '{}' (same repository name)",
1738 spec.name, spec_ref
1739 ),
1740 &data.source_location,
1741 ));
1742 }
1743 }
1744 }
1745 let current_segment_names: Vec<String> =
1746 current_segments.iter().map(|s| s.data.clone()).collect();
1747
1748 let this_spec_bindings =
1750 match self.build_data_bindings(spec, ¤t_segment_names, spec_arc, effective) {
1751 Ok(bindings) => bindings,
1752 Err(errors) => {
1753 self.errors.extend(errors);
1754 HashMap::new()
1755 }
1756 };
1757
1758 let data_map: HashMap<String, &LemmaData> = spec
1760 .data
1761 .iter()
1762 .map(|data| (data.reference.name.clone(), data))
1763 .collect();
1764
1765 if !self
1766 .local_types
1767 .iter()
1768 .any(|(_, s, _)| Arc::ptr_eq(s, spec_arc))
1769 {
1770 if !type_resolver.is_registered(spec_arc) {
1774 return Ok(());
1775 }
1776 match type_resolver.resolve_and_validate(spec_arc, effective) {
1777 Ok(resolved_types) => {
1778 self.local_types.push((
1779 Arc::clone(spec_repository),
1780 Arc::clone(spec_arc),
1781 resolved_types,
1782 ));
1783 }
1784 Err(es) => {
1785 self.errors.extend(es);
1786 return Ok(());
1787 }
1788 }
1789 }
1790
1791 for data in &spec.data {
1792 if let ParsedDataValue::Definition {
1793 from: Some(from_ref),
1794 ..
1795 } = &data.value
1796 {
1797 match self.resolve_spec_ref(from_ref, effective) {
1798 Ok((source_repo, source_arc)) => {
1799 if !self
1800 .local_types
1801 .iter()
1802 .any(|(_, s, _)| Arc::ptr_eq(s, &source_arc))
1803 {
1804 match type_resolver.resolve_and_validate(&source_arc, effective) {
1805 Ok(resolved_types) => {
1806 self.local_types.push((
1807 source_repo,
1808 source_arc,
1809 resolved_types,
1810 ));
1811 }
1812 Err(es) => self.errors.extend(es),
1813 }
1814 }
1815 }
1816 Err(e) => self.errors.push(e),
1817 }
1818 }
1819 }
1820
1821 let mut used_binding_keys: HashSet<Vec<String>> = HashSet::new();
1823 for data in &spec.data {
1824 if !data.reference.segments.is_empty() {
1825 continue; }
1827 if let ParsedDataValue::Import(spec_ref) = &data.value {
1828 if spec_ref.name == spec.name {
1829 continue; }
1831 }
1832 self.add_data(
1833 data,
1834 ¤t_segments,
1835 &data_bindings,
1836 spec_arc,
1837 &mut used_binding_keys,
1838 effective,
1839 );
1840 }
1841
1842 for data in &spec.data {
1843 if !data.reference.segments.is_empty() {
1844 continue;
1845 }
1846 if let ParsedDataValue::Import(spec_ref) = &data.value {
1847 if spec_ref.name == spec.name {
1848 continue; }
1850 let nested_effective = spec_ref.at(effective);
1851 let (nested_repo, nested_arc) = match self.resolve_spec_ref(spec_ref, effective) {
1852 Ok(pair) => pair,
1853 Err(e) => {
1854 self.errors.push(e);
1855 continue;
1856 }
1857 };
1858 let mut nested_segments = current_segments.clone();
1859 nested_segments.push(PathSegment {
1860 data: data.reference.name.clone(),
1861 spec: nested_arc.name.clone(),
1862 });
1863
1864 let nested_segment_names: Vec<String> =
1865 nested_segments.iter().map(|s| s.data.clone()).collect();
1866 let mut combined_bindings = this_spec_bindings.clone();
1867 for (key, value_and_source) in &data_bindings {
1868 if key.len() > nested_segment_names.len()
1869 && key[..nested_segment_names.len()] == nested_segment_names[..]
1870 && !combined_bindings.contains_key(key)
1871 {
1872 combined_bindings.insert(key.clone(), value_and_source.clone());
1873 }
1874 }
1875
1876 if let Err(errs) = self.build_spec(
1877 &nested_arc,
1878 &nested_repo,
1879 nested_segments,
1880 combined_bindings,
1881 &nested_effective,
1882 type_resolver,
1883 ) {
1884 self.errors.extend(errs);
1885 }
1886 }
1887 }
1888
1889 let expected_key_len = current_segments.len() + 1;
1892 for (binding_key, (_, binding_source)) in &data_bindings {
1893 if binding_key.len() == expected_key_len
1894 && binding_key[..current_segments.len()]
1895 .iter()
1896 .zip(current_segments.iter())
1897 .all(|(a, b)| a == &b.data)
1898 && !used_binding_keys.contains(binding_key)
1899 {
1900 self.errors.push(self.engine_error(
1901 format!(
1902 "Data binding targets a data that does not exist in the referenced spec: '{}'",
1903 binding_key.join(".")
1904 ),
1905 binding_source,
1906 ));
1907 }
1908 }
1909
1910 let rule_names: HashSet<&str> = spec.rules.iter().map(|r| r.name.as_str()).collect();
1911 for rule in &spec.rules {
1912 self.add_rule(
1913 rule,
1914 spec_arc,
1915 &data_map,
1916 ¤t_segments,
1917 &rule_names,
1918 effective,
1919 );
1920 }
1921
1922 Ok(())
1923 }
1924
1925 fn add_rule(
1926 &mut self,
1927 rule: &LemmaRule,
1928 current_spec_arc: &Arc<LemmaSpec>,
1929 data_map: &HashMap<String, &LemmaData>,
1930 current_segments: &[PathSegment],
1931 rule_names: &HashSet<&str>,
1932 effective: &EffectiveDate,
1933 ) {
1934 let rule_path = RulePath {
1935 segments: current_segments.to_vec(),
1936 rule: rule.name.clone(),
1937 };
1938
1939 if self.rules.contains_key(&rule_path) {
1940 let rule_source = &rule.source_location;
1941 self.errors.push(
1942 self.engine_error(format!("Duplicate rule '{}'", rule_path.rule), rule_source),
1943 );
1944 return;
1945 }
1946
1947 let mut branches = Vec::new();
1948 let mut depends_on_rules = BTreeSet::new();
1949
1950 let converted_expression = match self.convert_expression_and_extract_dependencies(
1951 &rule.expression,
1952 current_spec_arc,
1953 data_map,
1954 current_segments,
1955 &mut depends_on_rules,
1956 rule_names,
1957 effective,
1958 ) {
1959 Some(expr) => expr,
1960 None => return,
1961 };
1962 branches.push((None, converted_expression));
1963
1964 for unless_clause in &rule.unless_clauses {
1965 let converted_condition = match self.convert_expression_and_extract_dependencies(
1966 &unless_clause.condition,
1967 current_spec_arc,
1968 data_map,
1969 current_segments,
1970 &mut depends_on_rules,
1971 rule_names,
1972 effective,
1973 ) {
1974 Some(expr) => expr,
1975 None => return,
1976 };
1977 let converted_result = match self.convert_expression_and_extract_dependencies(
1978 &unless_clause.result,
1979 current_spec_arc,
1980 data_map,
1981 current_segments,
1982 &mut depends_on_rules,
1983 rule_names,
1984 effective,
1985 ) {
1986 Some(expr) => expr,
1987 None => return,
1988 };
1989 branches.push((Some(converted_condition), converted_result));
1990 }
1991
1992 let rule_node = RuleNode {
1993 branches,
1994 source: rule.source_location.clone(),
1995 depends_on_rules,
1996 rule_type: LemmaType::veto_type(),
1997 spec_arc: Arc::clone(current_spec_arc),
1998 };
1999
2000 self.rules.insert(rule_path, rule_node);
2001 }
2002
2003 #[allow(clippy::too_many_arguments)]
2005 fn convert_binary_operands(
2006 &mut self,
2007 left: &ast::Expression,
2008 right: &ast::Expression,
2009 current_spec_arc: &Arc<LemmaSpec>,
2010 data_map: &HashMap<String, &LemmaData>,
2011 current_segments: &[PathSegment],
2012 depends_on_rules: &mut BTreeSet<RulePath>,
2013 rule_names: &HashSet<&str>,
2014 effective: &EffectiveDate,
2015 ) -> Option<(Expression, Expression)> {
2016 let converted_left = self.convert_expression_and_extract_dependencies(
2017 left,
2018 current_spec_arc,
2019 data_map,
2020 current_segments,
2021 depends_on_rules,
2022 rule_names,
2023 effective,
2024 )?;
2025 let converted_right = self.convert_expression_and_extract_dependencies(
2026 right,
2027 current_spec_arc,
2028 data_map,
2029 current_segments,
2030 depends_on_rules,
2031 rule_names,
2032 effective,
2033 )?;
2034 Some((converted_left, converted_right))
2035 }
2036
2037 #[allow(clippy::too_many_arguments)]
2039 fn convert_expression_and_extract_dependencies(
2040 &mut self,
2041 expr: &ast::Expression,
2042 current_spec_arc: &Arc<LemmaSpec>,
2043 data_map: &HashMap<String, &LemmaData>,
2044 current_segments: &[PathSegment],
2045 depends_on_rules: &mut BTreeSet<RulePath>,
2046 rule_names: &HashSet<&str>,
2047 effective: &EffectiveDate,
2048 ) -> Option<Expression> {
2049 let expr_src = expr
2050 .source_location
2051 .as_ref()
2052 .expect("BUG: AST expression missing source location");
2053 match &expr.kind {
2054 ast::ExpressionKind::Reference(r) => {
2055 let expr_source = expr_src;
2056 let (segments, target_arc_opt) = if r.segments.is_empty() {
2057 (current_segments.to_vec(), None)
2058 } else {
2059 let data_map_owned: HashMap<String, LemmaData> = data_map
2060 .iter()
2061 .map(|(k, v)| (k.clone(), (*v).clone()))
2062 .collect();
2063 let (segs, arc) = self.resolve_path_segments(
2064 &r.segments,
2065 expr_source,
2066 data_map_owned,
2067 current_segments.to_vec(),
2068 effective,
2069 )?;
2070 (segs, Some(arc))
2071 };
2072
2073 let (is_data, is_rule, target_spec_name_opt) = match &target_arc_opt {
2074 None => {
2075 let is_data = data_map.contains_key(&r.name);
2076 let is_rule = rule_names.contains(r.name.as_str());
2077 (is_data, is_rule, None)
2078 }
2079 Some(target_arc) => {
2080 let target_spec = target_arc.as_ref();
2081 let target_data_names: HashSet<&str> = target_spec
2082 .data
2083 .iter()
2084 .filter(|f| f.reference.is_local())
2085 .map(|f| f.reference.name.as_str())
2086 .collect();
2087 let target_rule_names: HashSet<&str> =
2088 target_spec.rules.iter().map(|r| r.name.as_str()).collect();
2089 let is_data = target_data_names.contains(r.name.as_str());
2090 let is_rule = target_rule_names.contains(r.name.as_str());
2091 (is_data, is_rule, Some(target_spec.name.as_str()))
2092 }
2093 };
2094
2095 if is_data && is_rule {
2096 self.errors.push(self.engine_error(
2097 format!("'{}' is both a data and a rule", r.name),
2098 expr_source,
2099 ));
2100 return None;
2101 }
2102 if is_data {
2103 let data_path = DataPath {
2104 segments,
2105 data: r.name.clone(),
2106 };
2107 return Some(Expression {
2108 kind: ExpressionKind::DataPath(data_path),
2109 source_location: expr.source_location.clone(),
2110 });
2111 }
2112 if is_rule {
2113 let rule_path = RulePath {
2114 segments,
2115 rule: r.name.clone(),
2116 };
2117 depends_on_rules.insert(rule_path.clone());
2118 return Some(Expression {
2119 kind: ExpressionKind::RulePath(rule_path),
2120 source_location: expr.source_location.clone(),
2121 });
2122 }
2123 let msg = match target_spec_name_opt {
2124 Some(s) => format!("Reference '{}' not found in spec '{}'", r.name, s),
2125 None => format!("Reference '{}' not found", r.name),
2126 };
2127 self.errors.push(self.engine_error(msg, expr_source));
2128 None
2129 }
2130
2131 ast::ExpressionKind::LogicalAnd(left, right) => {
2132 let (l, r) = self.convert_binary_operands(
2133 left,
2134 right,
2135 current_spec_arc,
2136 data_map,
2137 current_segments,
2138 depends_on_rules,
2139 rule_names,
2140 effective,
2141 )?;
2142 Some(Expression {
2143 kind: ExpressionKind::LogicalAnd(Arc::new(l), Arc::new(r)),
2144 source_location: expr.source_location.clone(),
2145 })
2146 }
2147
2148 ast::ExpressionKind::Arithmetic(left, op, right) => {
2149 let (l, r) = self.convert_binary_operands(
2150 left,
2151 right,
2152 current_spec_arc,
2153 data_map,
2154 current_segments,
2155 depends_on_rules,
2156 rule_names,
2157 effective,
2158 )?;
2159 Some(Expression {
2160 kind: ExpressionKind::Arithmetic(Arc::new(l), op.clone(), Arc::new(r)),
2161 source_location: expr.source_location.clone(),
2162 })
2163 }
2164
2165 ast::ExpressionKind::Comparison(left, op, right) => {
2166 let (l, r) = self.convert_binary_operands(
2167 left,
2168 right,
2169 current_spec_arc,
2170 data_map,
2171 current_segments,
2172 depends_on_rules,
2173 rule_names,
2174 effective,
2175 )?;
2176 Some(Expression {
2177 kind: ExpressionKind::Comparison(Arc::new(l), op.clone(), Arc::new(r)),
2178 source_location: expr.source_location.clone(),
2179 })
2180 }
2181
2182 ast::ExpressionKind::UnitConversion(value, target) => {
2183 let converted_value = self.convert_expression_and_extract_dependencies(
2184 value,
2185 current_spec_arc,
2186 data_map,
2187 current_segments,
2188 depends_on_rules,
2189 rule_names,
2190 effective,
2191 )?;
2192
2193 let resolved_spec_types = self
2194 .local_types
2195 .iter()
2196 .find(|(_, s, _)| Arc::ptr_eq(s, current_spec_arc))
2197 .map(|(_, _, t)| t);
2198 let unit_index = resolved_spec_types.map(|dt| &dt.unit_index);
2199 let semantic_target = match conversion_target_to_semantic(target, unit_index) {
2200 Ok(t) => t,
2201 Err(msg) => {
2202 let full_msg = unit_index
2205 .map(|idx| {
2206 let valid: Vec<&str> = idx.keys().map(String::as_str).collect();
2207 format!("{} Valid units: {}", msg, valid.join(", "))
2208 })
2209 .unwrap_or(msg);
2210 self.errors.push(Error::validation_with_context(
2211 full_msg,
2212 expr.source_location.clone(),
2213 None::<String>,
2214 Some(Arc::clone(&self.main_spec)),
2215 None,
2216 ));
2217 return None;
2218 }
2219 };
2220
2221 Some(Expression {
2222 kind: ExpressionKind::UnitConversion(
2223 Arc::new(converted_value),
2224 semantic_target,
2225 ),
2226 source_location: expr.source_location.clone(),
2227 })
2228 }
2229
2230 ast::ExpressionKind::LogicalNegation(operand, neg_type) => {
2231 let converted_operand = self.convert_expression_and_extract_dependencies(
2232 operand,
2233 current_spec_arc,
2234 data_map,
2235 current_segments,
2236 depends_on_rules,
2237 rule_names,
2238 effective,
2239 )?;
2240 Some(Expression {
2241 kind: ExpressionKind::LogicalNegation(
2242 Arc::new(converted_operand),
2243 neg_type.clone(),
2244 ),
2245 source_location: expr.source_location.clone(),
2246 })
2247 }
2248
2249 ast::ExpressionKind::MathematicalComputation(op, operand) => {
2250 let converted_operand = self.convert_expression_and_extract_dependencies(
2251 operand,
2252 current_spec_arc,
2253 data_map,
2254 current_segments,
2255 depends_on_rules,
2256 rule_names,
2257 effective,
2258 )?;
2259 Some(Expression {
2260 kind: ExpressionKind::MathematicalComputation(
2261 op.clone(),
2262 Arc::new(converted_operand),
2263 ),
2264 source_location: expr.source_location.clone(),
2265 })
2266 }
2267
2268 ast::ExpressionKind::Literal(value) => {
2269 let semantic_value = match value_to_semantic(value) {
2271 Ok(v) => v,
2272 Err(e) => {
2273 self.errors.push(self.engine_error(e, expr_src));
2274 return None;
2275 }
2276 };
2277 let lemma_type = match value {
2279 Value::Text(_) => primitive_text().clone(),
2280 Value::Number(_) => primitive_number().clone(),
2281 Value::Scale(_, unit) => {
2282 match self
2283 .local_types
2284 .iter()
2285 .find(|(_, s, _)| Arc::ptr_eq(s, current_spec_arc))
2286 .map(|(_, _, t)| t)
2287 .and_then(|dt| dt.unit_index.get(unit))
2288 {
2289 Some(lt) => lt.clone(),
2290 None => {
2291 self.errors.push(self.engine_error(
2292 format!(
2293 "Scale literal uses unknown unit '{}' for this spec",
2294 unit
2295 ),
2296 expr_src,
2297 ));
2298 return None;
2299 }
2300 }
2301 }
2302 Value::Boolean(_) => primitive_boolean().clone(),
2303 Value::Date(_) => primitive_date().clone(),
2304 Value::Time(_) => primitive_time().clone(),
2305 Value::Duration(_, _) => primitive_duration().clone(),
2306 Value::Ratio(_, _) => primitive_ratio().clone(),
2307 };
2308 let literal_value = LiteralValue {
2309 value: semantic_value,
2310 lemma_type,
2311 };
2312 Some(Expression {
2313 kind: ExpressionKind::Literal(Box::new(literal_value)),
2314 source_location: expr.source_location.clone(),
2315 })
2316 }
2317
2318 ast::ExpressionKind::Veto(veto_expression) => Some(Expression {
2319 kind: ExpressionKind::Veto(veto_expression.clone()),
2320 source_location: expr.source_location.clone(),
2321 }),
2322
2323 ast::ExpressionKind::UnresolvedUnitLiteral(value, unit) => {
2324 if let Some(lt) = self
2325 .local_types
2326 .iter()
2327 .find(|(_, s, _)| Arc::ptr_eq(s, current_spec_arc))
2328 .map(|(_, _, t)| t)
2329 .and_then(|dt| dt.unit_index.get(unit))
2330 {
2331 let semantic_value = ValueKind::Scale(*value, unit.clone());
2332 let literal_value = LiteralValue {
2333 value: semantic_value,
2334 lemma_type: lt.clone(),
2335 };
2336 Some(Expression {
2337 kind: ExpressionKind::Literal(Box::new(literal_value)),
2338 source_location: expr.source_location.clone(),
2339 })
2340 } else {
2341 self.errors
2342 .push(self.engine_error(format!("Unknown unit '{}'", unit), expr_src));
2343 None
2344 }
2345 }
2346
2347 ast::ExpressionKind::Now => Some(Expression {
2348 kind: ExpressionKind::Now,
2349 source_location: expr.source_location.clone(),
2350 }),
2351
2352 ast::ExpressionKind::DateRelative(kind, date_expr, tolerance) => {
2353 let converted_date = self.convert_expression_and_extract_dependencies(
2354 date_expr,
2355 current_spec_arc,
2356 data_map,
2357 current_segments,
2358 depends_on_rules,
2359 rule_names,
2360 effective,
2361 )?;
2362 let converted_tolerance = match tolerance {
2363 Some(tol) => Some(Arc::new(self.convert_expression_and_extract_dependencies(
2364 tol,
2365 current_spec_arc,
2366 data_map,
2367 current_segments,
2368 depends_on_rules,
2369 rule_names,
2370 effective,
2371 )?)),
2372 None => None,
2373 };
2374 Some(Expression {
2375 kind: ExpressionKind::DateRelative(
2376 *kind,
2377 Arc::new(converted_date),
2378 converted_tolerance,
2379 ),
2380 source_location: expr.source_location.clone(),
2381 })
2382 }
2383
2384 ast::ExpressionKind::DateCalendar(kind, unit, date_expr) => {
2385 let converted_date = self.convert_expression_and_extract_dependencies(
2386 date_expr,
2387 current_spec_arc,
2388 data_map,
2389 current_segments,
2390 depends_on_rules,
2391 rule_names,
2392 effective,
2393 )?;
2394 Some(Expression {
2395 kind: ExpressionKind::DateCalendar(*kind, *unit, Arc::new(converted_date)),
2396 source_location: expr.source_location.clone(),
2397 })
2398 }
2399 }
2400 }
2401}
2402
2403fn find_types_by_spec<'b>(
2406 types: &'b ResolvedTypesMap,
2407 spec_arc: &Arc<LemmaSpec>,
2408) -> Option<&'b ResolvedSpecTypes> {
2409 types
2410 .iter()
2411 .find(|(_, s, _)| Arc::ptr_eq(s, spec_arc))
2412 .map(|(_, _, t)| t)
2413}
2414
2415fn compute_arithmetic_result_type(left_type: LemmaType, right_type: LemmaType) -> LemmaType {
2416 compute_arithmetic_result_type_recursive(left_type, right_type, false)
2417}
2418
2419fn compute_arithmetic_result_type_recursive(
2420 left_type: LemmaType,
2421 right_type: LemmaType,
2422 swapped: bool,
2423) -> LemmaType {
2424 match (&left_type.specifications, &right_type.specifications) {
2425 (TypeSpecification::Veto { .. }, _) | (_, TypeSpecification::Veto { .. }) => {
2426 LemmaType::veto_type()
2427 }
2428 (TypeSpecification::Undetermined, _) => LemmaType::undetermined_type(),
2429
2430 (TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => {
2431 primitive_duration().clone()
2432 }
2433 (TypeSpecification::Date { .. }, TypeSpecification::Time { .. }) => {
2434 primitive_duration().clone()
2435 }
2436 (TypeSpecification::Time { .. }, TypeSpecification::Time { .. }) => {
2437 primitive_duration().clone()
2438 }
2439
2440 _ if left_type == right_type => left_type,
2441
2442 (TypeSpecification::Date { .. }, TypeSpecification::Duration { .. }) => left_type,
2443 (TypeSpecification::Time { .. }, TypeSpecification::Duration { .. }) => left_type,
2444
2445 (TypeSpecification::Scale { .. }, TypeSpecification::Ratio { .. }) => left_type,
2446 (TypeSpecification::Scale { .. }, TypeSpecification::Number { .. }) => left_type,
2447 (TypeSpecification::Scale { .. }, TypeSpecification::Duration { .. }) => {
2448 primitive_number().clone()
2449 }
2450 (TypeSpecification::Scale { .. }, TypeSpecification::Scale { .. }) => left_type,
2451
2452 (TypeSpecification::Duration { .. }, TypeSpecification::Number { .. }) => left_type,
2453 (TypeSpecification::Duration { .. }, TypeSpecification::Ratio { .. }) => left_type,
2454 (TypeSpecification::Duration { .. }, TypeSpecification::Duration { .. }) => {
2455 primitive_duration().clone()
2456 }
2457
2458 (TypeSpecification::Number { .. }, TypeSpecification::Ratio { .. }) => {
2459 primitive_number().clone()
2460 }
2461 (TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => {
2462 primitive_number().clone()
2463 }
2464
2465 (TypeSpecification::Ratio { .. }, TypeSpecification::Ratio { .. }) => left_type,
2466
2467 _ => {
2468 if swapped {
2469 LemmaType::undetermined_type()
2470 } else {
2471 compute_arithmetic_result_type_recursive(right_type, left_type, true)
2472 }
2473 }
2474 }
2475}
2476
2477fn infer_expression_type(
2484 expression: &Expression,
2485 graph: &Graph,
2486 computed_rule_types: &HashMap<RulePath, LemmaType>,
2487 resolved_types: &ResolvedTypesMap,
2488 spec_arc: &Arc<LemmaSpec>,
2489) -> LemmaType {
2490 match &expression.kind {
2491 ExpressionKind::Literal(literal_value) => literal_value.as_ref().get_type().clone(),
2492
2493 ExpressionKind::DataPath(data_path) => {
2494 infer_data_type(data_path, graph, computed_rule_types)
2495 }
2496
2497 ExpressionKind::RulePath(rule_path) => computed_rule_types
2498 .get(rule_path)
2499 .cloned()
2500 .unwrap_or_else(LemmaType::undetermined_type),
2501
2502 ExpressionKind::LogicalAnd(left, right) => {
2503 let left_type =
2504 infer_expression_type(left, graph, computed_rule_types, resolved_types, spec_arc);
2505 let right_type =
2506 infer_expression_type(right, graph, computed_rule_types, resolved_types, spec_arc);
2507 if left_type.vetoed() || right_type.vetoed() {
2508 return LemmaType::veto_type();
2509 }
2510 if left_type.is_undetermined() || right_type.is_undetermined() {
2511 return LemmaType::undetermined_type();
2512 }
2513 primitive_boolean().clone()
2514 }
2515
2516 ExpressionKind::LogicalNegation(operand, _) => {
2517 let operand_type = infer_expression_type(
2518 operand,
2519 graph,
2520 computed_rule_types,
2521 resolved_types,
2522 spec_arc,
2523 );
2524 if operand_type.vetoed() {
2525 return LemmaType::veto_type();
2526 }
2527 if operand_type.is_undetermined() {
2528 return LemmaType::undetermined_type();
2529 }
2530 primitive_boolean().clone()
2531 }
2532
2533 ExpressionKind::Comparison(left, _op, right) => {
2534 let left_type =
2535 infer_expression_type(left, graph, computed_rule_types, resolved_types, spec_arc);
2536 let right_type =
2537 infer_expression_type(right, graph, computed_rule_types, resolved_types, spec_arc);
2538 if left_type.vetoed() || right_type.vetoed() {
2539 return LemmaType::veto_type();
2540 }
2541 if left_type.is_undetermined() || right_type.is_undetermined() {
2542 return LemmaType::undetermined_type();
2543 }
2544 primitive_boolean().clone()
2545 }
2546
2547 ExpressionKind::Arithmetic(left, _operator, right) => {
2548 let left_type =
2549 infer_expression_type(left, graph, computed_rule_types, resolved_types, spec_arc);
2550 let right_type =
2551 infer_expression_type(right, graph, computed_rule_types, resolved_types, spec_arc);
2552 compute_arithmetic_result_type(left_type, right_type)
2553 }
2554
2555 ExpressionKind::UnitConversion(source_expression, target) => {
2556 let source_type = infer_expression_type(
2557 source_expression,
2558 graph,
2559 computed_rule_types,
2560 resolved_types,
2561 spec_arc,
2562 );
2563 if source_type.vetoed() {
2564 return LemmaType::veto_type();
2565 }
2566 if source_type.is_undetermined() {
2567 return LemmaType::undetermined_type();
2568 }
2569 match target {
2570 SemanticConversionTarget::Duration(_) => primitive_duration().clone(),
2571 SemanticConversionTarget::ScaleUnit(unit_name) => {
2572 if source_type.is_number() {
2573 find_types_by_spec(resolved_types, spec_arc)
2574 .and_then(|dt| dt.unit_index.get(unit_name))
2575 .cloned()
2576 .unwrap_or_else(LemmaType::undetermined_type)
2577 } else {
2578 source_type
2579 }
2580 }
2581 SemanticConversionTarget::RatioUnit(unit_name) => {
2582 if source_type.is_number() {
2583 find_types_by_spec(resolved_types, spec_arc)
2584 .and_then(|dt| dt.unit_index.get(unit_name))
2585 .cloned()
2586 .unwrap_or_else(LemmaType::undetermined_type)
2587 } else {
2588 source_type
2589 }
2590 }
2591 }
2592 }
2593
2594 ExpressionKind::MathematicalComputation(_, operand) => {
2595 let operand_type = infer_expression_type(
2596 operand,
2597 graph,
2598 computed_rule_types,
2599 resolved_types,
2600 spec_arc,
2601 );
2602 if operand_type.vetoed() {
2603 return LemmaType::veto_type();
2604 }
2605 if operand_type.is_undetermined() {
2606 return LemmaType::undetermined_type();
2607 }
2608 primitive_number().clone()
2609 }
2610
2611 ExpressionKind::Veto(_) => LemmaType::veto_type(),
2612
2613 ExpressionKind::Now => primitive_date().clone(),
2614
2615 ExpressionKind::DateRelative(..) | ExpressionKind::DateCalendar(..) => {
2616 primitive_boolean().clone()
2617 }
2618 }
2619}
2620
2621fn infer_data_type(
2630 data_path: &DataPath,
2631 graph: &Graph,
2632 computed_rule_types: &HashMap<RulePath, LemmaType>,
2633) -> LemmaType {
2634 let entry = match graph.data().get(data_path) {
2635 Some(e) => e,
2636 None => return LemmaType::undetermined_type(),
2637 };
2638 match entry {
2639 DataDefinition::Value { value, .. } => value.lemma_type.clone(),
2640 DataDefinition::TypeDeclaration { resolved_type, .. } => resolved_type.clone(),
2641 DataDefinition::Reference {
2642 target: ReferenceTarget::Rule(target_rule),
2643 resolved_type,
2644 ..
2645 } => {
2646 if !resolved_type.is_undetermined() {
2647 resolved_type.clone()
2648 } else {
2649 computed_rule_types
2650 .get(target_rule)
2651 .cloned()
2652 .unwrap_or_else(LemmaType::undetermined_type)
2653 }
2654 }
2655 DataDefinition::Reference { resolved_type, .. } => resolved_type.clone(),
2656 DataDefinition::Import { .. } => LemmaType::undetermined_type(),
2657 }
2658}
2659
2660fn collect_rule_reference_dependencies(
2667 expression: &Expression,
2668 reference_to_rule: &HashMap<DataPath, RulePath>,
2669 out: &mut BTreeSet<RulePath>,
2670) {
2671 let mut paths: HashSet<DataPath> = HashSet::new();
2672 expression.kind.collect_data_paths(&mut paths);
2673 for path in paths {
2674 if let Some(target_rule) = reference_to_rule.get(&path) {
2675 out.insert(target_rule.clone());
2676 }
2677 }
2678}
2679
2680fn engine_error_at_graph(graph: &Graph, source: &Source, message: impl Into<String>) -> Error {
2685 Error::validation_with_context(
2686 message.into(),
2687 Some(source.clone()),
2688 None::<String>,
2689 Some(Arc::clone(&graph.main_spec)),
2690 None,
2691 )
2692}
2693
2694fn check_logical_operands(
2695 graph: &Graph,
2696 left_type: &LemmaType,
2697 right_type: &LemmaType,
2698 source: &Source,
2699) -> Result<(), Vec<Error>> {
2700 if left_type.vetoed() || right_type.vetoed() {
2701 return Ok(());
2702 }
2703 let mut errors = Vec::new();
2704 if !left_type.is_boolean() {
2705 errors.push(engine_error_at_graph(
2706 graph,
2707 source,
2708 format!(
2709 "Logical operation requires boolean operands, got {:?} for left operand",
2710 left_type
2711 ),
2712 ));
2713 }
2714 if !right_type.is_boolean() {
2715 errors.push(engine_error_at_graph(
2716 graph,
2717 source,
2718 format!(
2719 "Logical operation requires boolean operands, got {:?} for right operand",
2720 right_type
2721 ),
2722 ));
2723 }
2724 if errors.is_empty() {
2725 Ok(())
2726 } else {
2727 Err(errors)
2728 }
2729}
2730
2731fn check_logical_operand(
2732 graph: &Graph,
2733 operand_type: &LemmaType,
2734 source: &Source,
2735) -> Result<(), Vec<Error>> {
2736 if operand_type.vetoed() {
2737 return Ok(());
2738 }
2739 if !operand_type.is_boolean() {
2740 Err(vec![engine_error_at_graph(
2741 graph,
2742 source,
2743 format!(
2744 "Logical negation requires boolean operand, got {:?}",
2745 operand_type
2746 ),
2747 )])
2748 } else {
2749 Ok(())
2750 }
2751}
2752
2753fn check_comparison_types(
2754 graph: &Graph,
2755 left_type: &LemmaType,
2756 op: &ComparisonComputation,
2757 right_type: &LemmaType,
2758 source: &Source,
2759) -> Result<(), Vec<Error>> {
2760 if left_type.vetoed() || right_type.vetoed() {
2761 return Ok(());
2762 }
2763 let is_equality_only = matches!(op, ComparisonComputation::Is | ComparisonComputation::IsNot);
2764
2765 if left_type.is_boolean() && right_type.is_boolean() {
2766 if !is_equality_only {
2767 return Err(vec![engine_error_at_graph(
2768 graph,
2769 source,
2770 format!("Can only use 'is' and 'is not' with booleans (got {})", op),
2771 )]);
2772 }
2773 return Ok(());
2774 }
2775
2776 if left_type.is_text() && right_type.is_text() {
2777 if !is_equality_only {
2778 return Err(vec![engine_error_at_graph(
2779 graph,
2780 source,
2781 format!("Can only use 'is' and 'is not' with text (got {})", op),
2782 )]);
2783 }
2784 return Ok(());
2785 }
2786
2787 if left_type.is_number() && right_type.is_number() {
2788 return Ok(());
2789 }
2790
2791 if left_type.is_ratio() && right_type.is_ratio() {
2792 return Ok(());
2793 }
2794
2795 if left_type.is_date() && right_type.is_date() {
2796 return Ok(());
2797 }
2798
2799 if left_type.is_time() && right_type.is_time() {
2800 return Ok(());
2801 }
2802
2803 if left_type.is_scale() && right_type.is_scale() {
2804 if !left_type.same_scale_family(right_type) {
2805 return Err(vec![engine_error_at_graph(
2806 graph,
2807 source,
2808 format!(
2809 "Cannot compare different scale types: {} and {}",
2810 left_type.name(),
2811 right_type.name()
2812 ),
2813 )]);
2814 }
2815 return Ok(());
2816 }
2817
2818 if left_type.is_duration() && right_type.is_duration() {
2819 return Ok(());
2820 }
2821 if left_type.is_duration() && right_type.is_number() {
2822 return Ok(());
2823 }
2824 if left_type.is_number() && right_type.is_duration() {
2825 return Ok(());
2826 }
2827
2828 Err(vec![engine_error_at_graph(
2829 graph,
2830 source,
2831 format!("Cannot compare {:?} with {:?}", left_type, right_type),
2832 )])
2833}
2834
2835fn check_arithmetic_types(
2836 graph: &Graph,
2837 left_type: &LemmaType,
2838 right_type: &LemmaType,
2839 operator: &ArithmeticComputation,
2840 source: &Source,
2841) -> Result<(), Vec<Error>> {
2842 if left_type.vetoed() || right_type.vetoed() {
2843 return Ok(());
2844 }
2845 if left_type.is_date() || left_type.is_time() || right_type.is_date() || right_type.is_time() {
2847 let both_temporal = (left_type.is_date() || left_type.is_time())
2848 && (right_type.is_date() || right_type.is_time());
2849 let one_is_duration = left_type.is_duration() || right_type.is_duration();
2850 let valid = matches!(
2851 operator,
2852 ArithmeticComputation::Add | ArithmeticComputation::Subtract
2853 ) && (both_temporal || one_is_duration);
2854 if !valid {
2855 return Err(vec![engine_error_at_graph(
2856 graph,
2857 source,
2858 format!(
2859 "Cannot apply '{}' to {} and {}.",
2860 operator,
2861 left_type.name(),
2862 right_type.name()
2863 ),
2864 )]);
2865 }
2866 return Ok(());
2867 }
2868
2869 if left_type.is_scale() && right_type.is_scale() && !left_type.same_scale_family(right_type) {
2871 return Err(vec![engine_error_at_graph(
2872 graph,
2873 source,
2874 format!(
2875 "Cannot {} different scale types: {} and {}. Operations between different scale types produce ambiguous result units.",
2876 match operator {
2877 ArithmeticComputation::Add => "add",
2878 ArithmeticComputation::Subtract => "subtract",
2879 ArithmeticComputation::Multiply => "multiply",
2880 ArithmeticComputation::Divide => "divide",
2881 ArithmeticComputation::Modulo => "modulo",
2882 ArithmeticComputation::Power => "power",
2883 },
2884 left_type.name(),
2885 right_type.name()
2886 ),
2887 )]);
2888 }
2889
2890 let left_valid = left_type.is_scale()
2892 || left_type.is_number()
2893 || left_type.is_duration()
2894 || left_type.is_ratio();
2895 let right_valid = right_type.is_scale()
2896 || right_type.is_number()
2897 || right_type.is_duration()
2898 || right_type.is_ratio();
2899
2900 if !left_valid || !right_valid {
2901 return Err(vec![engine_error_at_graph(
2902 graph,
2903 source,
2904 format!(
2905 "Cannot apply '{}' to {} and {}.",
2906 operator,
2907 left_type.name(),
2908 right_type.name()
2909 ),
2910 )]);
2911 }
2912
2913 if left_type.has_same_base_type(right_type) {
2915 return Ok(());
2916 }
2917
2918 let pair = |a: fn(&LemmaType) -> bool, b: fn(&LemmaType) -> bool| {
2919 (a(left_type) && b(right_type)) || (b(left_type) && a(right_type))
2920 };
2921
2922 let allowed = match operator {
2923 ArithmeticComputation::Multiply => {
2924 pair(LemmaType::is_scale, LemmaType::is_number)
2925 || pair(LemmaType::is_scale, LemmaType::is_ratio)
2926 || pair(LemmaType::is_scale, LemmaType::is_duration)
2927 || pair(LemmaType::is_duration, LemmaType::is_number)
2928 || pair(LemmaType::is_duration, LemmaType::is_ratio)
2929 || pair(LemmaType::is_number, LemmaType::is_ratio)
2930 }
2931 ArithmeticComputation::Divide => {
2932 pair(LemmaType::is_scale, LemmaType::is_number)
2933 || pair(LemmaType::is_scale, LemmaType::is_ratio)
2934 || pair(LemmaType::is_scale, LemmaType::is_duration)
2935 || (left_type.is_duration() && right_type.is_number())
2936 || (left_type.is_duration() && right_type.is_ratio())
2937 || pair(LemmaType::is_number, LemmaType::is_ratio)
2938 }
2939 ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
2940 pair(LemmaType::is_scale, LemmaType::is_number)
2941 || pair(LemmaType::is_scale, LemmaType::is_ratio)
2942 || pair(LemmaType::is_duration, LemmaType::is_number)
2943 || pair(LemmaType::is_duration, LemmaType::is_ratio)
2944 || pair(LemmaType::is_number, LemmaType::is_ratio)
2945 }
2946 ArithmeticComputation::Power => {
2947 (left_type.is_number()
2948 || left_type.is_scale()
2949 || left_type.is_ratio()
2950 || left_type.is_duration())
2951 && (right_type.is_number() || right_type.is_ratio())
2952 }
2953 ArithmeticComputation::Modulo => right_type.is_number() || right_type.is_ratio(),
2954 };
2955
2956 if !allowed {
2957 return Err(vec![engine_error_at_graph(
2958 graph,
2959 source,
2960 format!(
2961 "Cannot apply '{}' to {} and {}.",
2962 operator,
2963 left_type.name(),
2964 right_type.name(),
2965 ),
2966 )]);
2967 }
2968
2969 Ok(())
2970}
2971
2972fn check_unit_conversion_types(
2973 graph: &Graph,
2974 source_type: &LemmaType,
2975 target: &SemanticConversionTarget,
2976 resolved_types: &ResolvedTypesMap,
2977 source: &Source,
2978 spec_arc: &Arc<LemmaSpec>,
2979) -> Result<(), Vec<Error>> {
2980 if source_type.vetoed() {
2981 return Ok(());
2982 }
2983 match target {
2984 SemanticConversionTarget::ScaleUnit(unit_name)
2985 | SemanticConversionTarget::RatioUnit(unit_name) => {
2986 let unit_check: Option<(bool, Vec<&str>)> = match (&source_type.specifications, target)
2987 {
2988 (
2989 TypeSpecification::Scale { units, .. },
2990 SemanticConversionTarget::ScaleUnit(_),
2991 ) => {
2992 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
2993 let found = units.iter().any(|u| u.name.eq_ignore_ascii_case(unit_name));
2994 Some((found, valid))
2995 }
2996 (
2997 TypeSpecification::Ratio { units, .. },
2998 SemanticConversionTarget::RatioUnit(_),
2999 ) => {
3000 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
3001 let found = units.iter().any(|u| u.name.eq_ignore_ascii_case(unit_name));
3002 Some((found, valid))
3003 }
3004 _ => None,
3005 };
3006
3007 match unit_check {
3008 Some((true, _)) => Ok(()),
3009 Some((false, valid)) => Err(vec![engine_error_at_graph(
3010 graph,
3011 source,
3012 format!(
3013 "Unknown unit '{}' for type {}. Valid units: {}",
3014 unit_name,
3015 source_type.name(),
3016 valid.join(", ")
3017 ),
3018 )]),
3019 None if source_type.is_number() => {
3020 if find_types_by_spec(resolved_types, spec_arc)
3021 .and_then(|dt| dt.unit_index.get(unit_name))
3022 .is_none()
3023 {
3024 Err(vec![engine_error_at_graph(
3025 graph,
3026 source,
3027 format!("Unknown unit '{}' in spec '{}'.", unit_name, spec_arc.name),
3028 )])
3029 } else {
3030 Ok(())
3031 }
3032 }
3033 None => Err(vec![engine_error_at_graph(
3034 graph,
3035 source,
3036 format!(
3037 "Cannot convert {} to unit '{}'.",
3038 source_type.name(),
3039 unit_name
3040 ),
3041 )]),
3042 }
3043 }
3044 SemanticConversionTarget::Duration(_) => {
3045 if !source_type.is_duration() && !source_type.is_numeric() {
3046 Err(vec![engine_error_at_graph(
3047 graph,
3048 source,
3049 format!("Cannot convert {} to duration.", source_type.name()),
3050 )])
3051 } else {
3052 Ok(())
3053 }
3054 }
3055 }
3056}
3057
3058fn check_mathematical_operand(
3059 graph: &Graph,
3060 operand_type: &LemmaType,
3061 source: &Source,
3062) -> Result<(), Vec<Error>> {
3063 if operand_type.vetoed() {
3064 return Ok(());
3065 }
3066 if !operand_type.is_number() {
3067 Err(vec![engine_error_at_graph(
3068 graph,
3069 source,
3070 format!(
3071 "Mathematical function requires number operand, got {:?}",
3072 operand_type
3073 ),
3074 )])
3075 } else {
3076 Ok(())
3077 }
3078}
3079
3080fn check_all_rule_references_exist(graph: &Graph) -> Result<(), Vec<Error>> {
3082 let mut errors = Vec::new();
3083 let existing_rules: HashSet<&RulePath> = graph.rules().keys().collect();
3084 for (rule_path, rule_node) in graph.rules() {
3085 for dependency in &rule_node.depends_on_rules {
3086 if !existing_rules.contains(dependency) {
3087 errors.push(engine_error_at_graph(
3088 graph,
3089 &rule_node.source,
3090 format!(
3091 "Rule '{}' references non-existent rule '{}'",
3092 rule_path.rule, dependency.rule
3093 ),
3094 ));
3095 }
3096 }
3097 }
3098 if errors.is_empty() {
3099 Ok(())
3100 } else {
3101 Err(errors)
3102 }
3103}
3104
3105fn check_data_and_rule_name_collisions(graph: &Graph) -> Result<(), Vec<Error>> {
3107 let mut errors = Vec::new();
3108 for rule_path in graph.rules().keys() {
3109 let data_path = DataPath::new(rule_path.segments.clone(), rule_path.rule.clone());
3110 if graph.data().contains_key(&data_path) {
3111 let rule_node = graph.rules().get(rule_path).unwrap_or_else(|| {
3112 unreachable!(
3113 "BUG: rule '{}' missing from graph while validating name collisions",
3114 rule_path.rule
3115 )
3116 });
3117 errors.push(engine_error_at_graph(
3118 graph,
3119 &rule_node.source,
3120 format!(
3121 "Name collision: '{}' is defined as both a data and a rule",
3122 data_path
3123 ),
3124 ));
3125 }
3126 }
3127 if errors.is_empty() {
3128 Ok(())
3129 } else {
3130 Err(errors)
3131 }
3132}
3133
3134fn check_data_reference(
3136 data_path: &DataPath,
3137 graph: &Graph,
3138 data_source: &Source,
3139) -> Result<(), Vec<Error>> {
3140 let entry = match graph.data().get(data_path) {
3141 Some(e) => e,
3142 None => {
3143 return Err(vec![engine_error_at_graph(
3144 graph,
3145 data_source,
3146 format!("Unknown data reference '{}'", data_path),
3147 )]);
3148 }
3149 };
3150 match entry {
3151 DataDefinition::Value { .. }
3152 | DataDefinition::TypeDeclaration { .. }
3153 | DataDefinition::Reference { .. } => Ok(()),
3154 DataDefinition::Import { .. } => Err(vec![engine_error_at_graph(
3155 graph,
3156 entry.source(),
3157 format!(
3158 "Cannot compute type for spec reference data '{}'",
3159 data_path
3160 ),
3161 )]),
3162 }
3163}
3164
3165fn check_expression(
3169 expression: &Expression,
3170 graph: &Graph,
3171 inferred_types: &HashMap<RulePath, LemmaType>,
3172 resolved_types: &ResolvedTypesMap,
3173 spec_arc: &Arc<LemmaSpec>,
3174) -> Result<(), Vec<Error>> {
3175 let mut errors = Vec::new();
3176
3177 let collect = |result: Result<(), Vec<Error>>, errors: &mut Vec<Error>| {
3178 if let Err(errs) = result {
3179 errors.extend(errs);
3180 }
3181 };
3182
3183 match &expression.kind {
3184 ExpressionKind::Literal(_) => {}
3185
3186 ExpressionKind::DataPath(data_path) => {
3187 let data_source = expression
3188 .source_location
3189 .as_ref()
3190 .expect("BUG: expression missing source in check_expression");
3191 collect(
3192 check_data_reference(data_path, graph, data_source),
3193 &mut errors,
3194 );
3195 }
3196
3197 ExpressionKind::RulePath(_) => {}
3198
3199 ExpressionKind::LogicalAnd(left, right) => {
3200 collect(
3201 check_expression(left, graph, inferred_types, resolved_types, spec_arc),
3202 &mut errors,
3203 );
3204 collect(
3205 check_expression(right, graph, inferred_types, resolved_types, spec_arc),
3206 &mut errors,
3207 );
3208
3209 let left_type =
3210 infer_expression_type(left, graph, inferred_types, resolved_types, spec_arc);
3211 let right_type =
3212 infer_expression_type(right, graph, inferred_types, resolved_types, spec_arc);
3213 let expr_source = expression
3214 .source_location
3215 .as_ref()
3216 .expect("BUG: expression missing source in check_expression");
3217 collect(
3218 check_logical_operands(graph, &left_type, &right_type, expr_source),
3219 &mut errors,
3220 );
3221 }
3222
3223 ExpressionKind::LogicalNegation(operand, _) => {
3224 collect(
3225 check_expression(operand, graph, inferred_types, resolved_types, spec_arc),
3226 &mut errors,
3227 );
3228
3229 let operand_type =
3230 infer_expression_type(operand, graph, inferred_types, resolved_types, spec_arc);
3231 let expr_source = expression
3232 .source_location
3233 .as_ref()
3234 .expect("BUG: expression missing source in check_expression");
3235 collect(
3236 check_logical_operand(graph, &operand_type, expr_source),
3237 &mut errors,
3238 );
3239 }
3240
3241 ExpressionKind::Comparison(left, op, right) => {
3242 collect(
3243 check_expression(left, graph, inferred_types, resolved_types, spec_arc),
3244 &mut errors,
3245 );
3246 collect(
3247 check_expression(right, graph, inferred_types, resolved_types, spec_arc),
3248 &mut errors,
3249 );
3250
3251 let left_type =
3252 infer_expression_type(left, graph, inferred_types, resolved_types, spec_arc);
3253 let right_type =
3254 infer_expression_type(right, graph, inferred_types, resolved_types, spec_arc);
3255 let expr_source = expression
3256 .source_location
3257 .as_ref()
3258 .expect("BUG: expression missing source in check_expression");
3259 collect(
3260 check_comparison_types(graph, &left_type, op, &right_type, expr_source),
3261 &mut errors,
3262 );
3263 }
3264
3265 ExpressionKind::Arithmetic(left, operator, right) => {
3266 collect(
3267 check_expression(left, graph, inferred_types, resolved_types, spec_arc),
3268 &mut errors,
3269 );
3270 collect(
3271 check_expression(right, graph, inferred_types, resolved_types, spec_arc),
3272 &mut errors,
3273 );
3274
3275 let left_type =
3276 infer_expression_type(left, graph, inferred_types, resolved_types, spec_arc);
3277 let right_type =
3278 infer_expression_type(right, graph, inferred_types, resolved_types, spec_arc);
3279 let expr_source = expression
3280 .source_location
3281 .as_ref()
3282 .expect("BUG: expression missing source in check_expression");
3283 collect(
3284 check_arithmetic_types(graph, &left_type, &right_type, operator, expr_source),
3285 &mut errors,
3286 );
3287 }
3288
3289 ExpressionKind::UnitConversion(source_expression, target) => {
3290 collect(
3291 check_expression(
3292 source_expression,
3293 graph,
3294 inferred_types,
3295 resolved_types,
3296 spec_arc,
3297 ),
3298 &mut errors,
3299 );
3300
3301 let source_type = infer_expression_type(
3302 source_expression,
3303 graph,
3304 inferred_types,
3305 resolved_types,
3306 spec_arc,
3307 );
3308 let expr_source = expression
3309 .source_location
3310 .as_ref()
3311 .expect("BUG: expression missing source in check_expression");
3312 collect(
3313 check_unit_conversion_types(
3314 graph,
3315 &source_type,
3316 target,
3317 resolved_types,
3318 expr_source,
3319 spec_arc,
3320 ),
3321 &mut errors,
3322 );
3323
3324 if source_type.is_number() {
3325 match target {
3326 SemanticConversionTarget::ScaleUnit(unit_name)
3327 | SemanticConversionTarget::RatioUnit(unit_name) => {
3328 if find_types_by_spec(resolved_types, spec_arc)
3329 .and_then(|dt| dt.unit_index.get(unit_name))
3330 .is_none()
3331 {
3332 errors.push(engine_error_at_graph(
3333 graph,
3334 expr_source,
3335 format!(
3336 "Cannot resolve unit '{}' for spec '{}' (types may not have been resolved)",
3337 unit_name,
3338 spec_arc.name
3339 ),
3340 ));
3341 }
3342 }
3343 SemanticConversionTarget::Duration(_) => {}
3344 }
3345 }
3346 }
3347
3348 ExpressionKind::MathematicalComputation(_, operand) => {
3349 collect(
3350 check_expression(operand, graph, inferred_types, resolved_types, spec_arc),
3351 &mut errors,
3352 );
3353
3354 let operand_type =
3355 infer_expression_type(operand, graph, inferred_types, resolved_types, spec_arc);
3356 let expr_source = expression
3357 .source_location
3358 .as_ref()
3359 .expect("BUG: expression missing source in check_expression");
3360 collect(
3361 check_mathematical_operand(graph, &operand_type, expr_source),
3362 &mut errors,
3363 );
3364 }
3365
3366 ExpressionKind::Veto(_) => {}
3367
3368 ExpressionKind::Now => {}
3369
3370 ExpressionKind::DateRelative(_, date_expr, tolerance) => {
3371 collect(
3372 check_expression(date_expr, graph, inferred_types, resolved_types, spec_arc),
3373 &mut errors,
3374 );
3375
3376 let date_type =
3377 infer_expression_type(date_expr, graph, inferred_types, resolved_types, spec_arc);
3378 if !date_type.is_date() {
3379 let expr_source = expression
3380 .source_location
3381 .as_ref()
3382 .expect("BUG: expression missing source in check_expression");
3383 errors.push(engine_error_at_graph(
3384 graph,
3385 expr_source,
3386 format!(
3387 "Date sugar 'in past/future' requires a date expression, got type '{}'",
3388 date_type
3389 ),
3390 ));
3391 }
3392
3393 if let Some(tol) = tolerance {
3394 collect(
3395 check_expression(tol, graph, inferred_types, resolved_types, spec_arc),
3396 &mut errors,
3397 );
3398
3399 let tol_type =
3400 infer_expression_type(tol, graph, inferred_types, resolved_types, spec_arc);
3401 if !tol_type.is_duration() {
3402 let expr_source = expression
3403 .source_location
3404 .as_ref()
3405 .expect("BUG: expression missing source in check_expression");
3406 errors.push(engine_error_at_graph(
3407 graph,
3408 expr_source,
3409 format!(
3410 "Tolerance in date sugar must be a duration, got type '{}'",
3411 tol_type
3412 ),
3413 ));
3414 }
3415 }
3416 }
3417
3418 ExpressionKind::DateCalendar(_, _, date_expr) => {
3419 collect(
3420 check_expression(date_expr, graph, inferred_types, resolved_types, spec_arc),
3421 &mut errors,
3422 );
3423
3424 let date_type =
3425 infer_expression_type(date_expr, graph, inferred_types, resolved_types, spec_arc);
3426 if !date_type.is_date() {
3427 let expr_source = expression
3428 .source_location
3429 .as_ref()
3430 .expect("BUG: expression missing source in check_expression");
3431 errors.push(engine_error_at_graph(
3432 graph,
3433 expr_source,
3434 format!(
3435 "Calendar sugar requires a date expression, got type '{}'",
3436 date_type
3437 ),
3438 ));
3439 }
3440 }
3441 }
3442
3443 if errors.is_empty() {
3444 Ok(())
3445 } else {
3446 Err(errors)
3447 }
3448}
3449
3450fn check_rule_types(
3456 graph: &Graph,
3457 execution_order: &[RulePath],
3458 inferred_types: &HashMap<RulePath, LemmaType>,
3459 resolved_types: &ResolvedTypesMap,
3460) -> Result<(), Vec<Error>> {
3461 let mut errors = Vec::new();
3462
3463 let collect = |result: Result<(), Vec<Error>>, errors: &mut Vec<Error>| {
3464 if let Err(errs) = result {
3465 errors.extend(errs);
3466 }
3467 };
3468
3469 for rule_path in execution_order {
3470 let rule_node = match graph.rules().get(rule_path) {
3471 Some(node) => node,
3472 None => continue,
3473 };
3474 let branches = &rule_node.branches;
3475 let spec_arc = &rule_node.spec_arc;
3476
3477 if branches.is_empty() {
3478 continue;
3479 }
3480
3481 let (_, default_result) = &branches[0];
3482 collect(
3483 check_expression(
3484 default_result,
3485 graph,
3486 inferred_types,
3487 resolved_types,
3488 spec_arc,
3489 ),
3490 &mut errors,
3491 );
3492 let default_type = infer_expression_type(
3493 default_result,
3494 graph,
3495 inferred_types,
3496 resolved_types,
3497 spec_arc,
3498 );
3499
3500 let mut non_veto_type: Option<LemmaType> = None;
3501 if !default_type.vetoed() && !default_type.is_undetermined() {
3502 non_veto_type = Some(default_type.clone());
3503 }
3504
3505 for (branch_index, (condition, result)) in branches.iter().enumerate().skip(1) {
3506 if let Some(condition_expression) = condition {
3507 collect(
3508 check_expression(
3509 condition_expression,
3510 graph,
3511 inferred_types,
3512 resolved_types,
3513 spec_arc,
3514 ),
3515 &mut errors,
3516 );
3517 let condition_type = infer_expression_type(
3518 condition_expression,
3519 graph,
3520 inferred_types,
3521 resolved_types,
3522 spec_arc,
3523 );
3524 if !condition_type.is_boolean() && !condition_type.is_undetermined() {
3525 let condition_source = condition_expression
3526 .source_location
3527 .as_ref()
3528 .expect("BUG: condition expression missing source in check_rule_types");
3529 errors.push(engine_error_at_graph(
3530 graph,
3531 condition_source,
3532 format!(
3533 "Unless clause condition in rule '{}' must be boolean, got {:?}",
3534 rule_path.rule, condition_type
3535 ),
3536 ));
3537 }
3538 }
3539
3540 collect(
3541 check_expression(result, graph, inferred_types, resolved_types, spec_arc),
3542 &mut errors,
3543 );
3544 let result_type =
3545 infer_expression_type(result, graph, inferred_types, resolved_types, spec_arc);
3546
3547 if !result_type.vetoed() && !result_type.is_undetermined() {
3548 if non_veto_type.is_none() {
3549 non_veto_type = Some(result_type.clone());
3550 } else if let Some(ref existing_type) = non_veto_type {
3551 if !existing_type.has_same_base_type(&result_type) {
3552 let Some(rule_node) = graph.rules().get(rule_path) else {
3553 unreachable!(
3554 "BUG: rule type validation referenced missing rule '{}'",
3555 rule_path.rule
3556 );
3557 };
3558 let rule_source = &rule_node.source;
3559 let default_expr = &branches[0].1;
3560
3561 let mut location_parts = vec![format!(
3562 "{}:{}:{}",
3563 rule_source.source_type, rule_source.span.line, rule_source.span.col
3564 )];
3565
3566 if let Some(loc) = &default_expr.source_location {
3567 location_parts.push(format!(
3568 "default branch at {}:{}:{}",
3569 loc.source_type, loc.span.line, loc.span.col
3570 ));
3571 }
3572 if let Some(loc) = &result.source_location {
3573 location_parts.push(format!(
3574 "unless clause {} at {}:{}:{}",
3575 branch_index, loc.source_type, loc.span.line, loc.span.col
3576 ));
3577 }
3578
3579 errors.push(Error::validation_with_context(
3580 format!("Type mismatch in rule '{}' in spec '{}' ({}): default branch returns {}, but unless clause {} returns {}. All branches must return the same primitive type.",
3581 rule_path.rule,
3582 spec_arc.name,
3583 location_parts.join(", "),
3584 existing_type.name(),
3585 branch_index,
3586 result_type.name()),
3587 Some(rule_source.clone()),
3588 None::<String>,
3589 Some(Arc::clone(&graph.main_spec)),
3590 None,
3591 ));
3592 }
3593 }
3594 }
3595 }
3596 }
3597
3598 if errors.is_empty() {
3599 Ok(())
3600 } else {
3601 Err(errors)
3602 }
3603}
3604
3605fn apply_inferred_types(graph: &mut Graph, inferred_types: HashMap<RulePath, LemmaType>) {
3613 for (rule_path, rule_type) in inferred_types {
3614 if let Some(rule_node) = graph.rules_mut().get_mut(&rule_path) {
3615 rule_node.rule_type = rule_type;
3616 }
3617 }
3618}
3619
3620fn infer_rule_types(
3624 graph: &Graph,
3625 execution_order: &[RulePath],
3626 resolved_types: &ResolvedTypesMap,
3627) -> HashMap<RulePath, LemmaType> {
3628 let mut computed_types: HashMap<RulePath, LemmaType> = HashMap::new();
3629
3630 for rule_path in execution_order {
3631 let rule_node = match graph.rules().get(rule_path) {
3632 Some(node) => node,
3633 None => continue,
3634 };
3635 let branches = &rule_node.branches;
3636 let spec_arc = &rule_node.spec_arc;
3637
3638 if branches.is_empty() {
3639 continue;
3640 }
3641
3642 let (_, default_result) = &branches[0];
3643 let default_type = infer_expression_type(
3644 default_result,
3645 graph,
3646 &computed_types,
3647 resolved_types,
3648 spec_arc,
3649 );
3650
3651 let mut non_veto_type: Option<LemmaType> = None;
3652 if !default_type.vetoed() && !default_type.is_undetermined() {
3653 non_veto_type = Some(default_type.clone());
3654 }
3655
3656 for (_branch_index, (_condition, result)) in branches.iter().enumerate().skip(1) {
3657 let result_type =
3658 infer_expression_type(result, graph, &computed_types, resolved_types, spec_arc);
3659 if !result_type.vetoed() && !result_type.is_undetermined() && non_veto_type.is_none() {
3660 non_veto_type = Some(result_type.clone());
3661 }
3662 }
3663
3664 let rule_type = non_veto_type.unwrap_or_else(LemmaType::veto_type);
3665 computed_types.insert(rule_path.clone(), rule_type);
3666 }
3667
3668 computed_types
3669}
3670
3671#[cfg(test)]
3672mod tests {
3673 use super::*;
3674
3675 use crate::parsing::ast::{BooleanValue, Reference, Span, Value};
3676
3677 fn test_source() -> Source {
3678 Source::new(
3679 crate::parsing::source::SourceType::Volatile,
3680 Span {
3681 start: 0,
3682 end: 0,
3683 line: 1,
3684 col: 0,
3685 },
3686 )
3687 }
3688
3689 fn build_graph(main_spec: &LemmaSpec, all_specs: &[LemmaSpec]) -> Result<Graph, Vec<Error>> {
3690 use crate::engine::Context;
3691 use crate::planning::discovery;
3692
3693 let mut ctx = Context::new();
3694 let repository = ctx.workspace();
3695 for s in all_specs {
3696 if let Err(e) = ctx.insert_spec(Arc::clone(&repository), Arc::new(s.clone())) {
3697 return Err(vec![e]);
3698 }
3699 }
3700 let effective = EffectiveDate::from_option(main_spec.effective_from().cloned());
3701 let main_spec_arc = ctx
3702 .spec_set(&repository, main_spec.name.as_str())
3703 .and_then(|ss| ss.get_exact(main_spec.effective_from()).cloned())
3704 .expect("main_spec must be in all_specs");
3705 let dag =
3706 discovery::build_dag_for_spec(&ctx, &main_spec_arc, &effective).map_err(
3707 |e| match e {
3708 discovery::DagError::Cycle(es) | discovery::DagError::Other(es) => es,
3709 },
3710 )?;
3711 match Graph::build(&ctx, &repository, &main_spec_arc, &dag, &effective) {
3712 Ok((graph, _types)) => Ok(graph),
3713 Err(errors) => Err(errors),
3714 }
3715 }
3716
3717 fn create_test_spec(name: &str) -> LemmaSpec {
3718 LemmaSpec::new(name.to_string())
3719 }
3720
3721 fn create_literal_data(name: &str, value: Value) -> LemmaData {
3722 LemmaData {
3723 reference: Reference {
3724 segments: Vec::new(),
3725 name: name.to_string(),
3726 },
3727 value: ParsedDataValue::Definition {
3728 base: None,
3729 constraints: None,
3730 from: None,
3731 value: Some(value),
3732 },
3733 source_location: test_source(),
3734 }
3735 }
3736
3737 fn create_literal_expr(value: Value) -> ast::Expression {
3738 ast::Expression {
3739 kind: ast::ExpressionKind::Literal(value),
3740 source_location: Some(test_source()),
3741 }
3742 }
3743
3744 #[test]
3745 fn should_reject_data_binding_into_non_spec_data() {
3746 let mut spec = create_test_spec("test");
3751 spec = spec.add_data(create_literal_data("x", Value::Number(1.into())));
3752
3753 spec = spec.add_data(LemmaData {
3755 reference: Reference::from_path(vec!["x".to_string(), "y".to_string()]),
3756 value: ParsedDataValue::Definition {
3757 base: None,
3758 constraints: None,
3759 from: None,
3760 value: Some(Value::Number(2.into())),
3761 },
3762 source_location: test_source(),
3763 });
3764
3765 let result = build_graph(&spec, &[spec.clone()]);
3766 assert!(
3767 result.is_err(),
3768 "Overriding x.y must fail when x is not a spec reference"
3769 );
3770 }
3771
3772 #[test]
3773 fn should_reject_data_and_rule_name_collision() {
3774 let mut spec = create_test_spec("test");
3779 spec = spec.add_data(create_literal_data("x", Value::Number(1.into())));
3780 spec = spec.add_rule(LemmaRule {
3781 name: "x".to_string(),
3782 expression: create_literal_expr(Value::Number(2.into())),
3783 unless_clauses: Vec::new(),
3784 source_location: test_source(),
3785 });
3786
3787 let result = build_graph(&spec, &[spec.clone()]);
3788 assert!(
3789 result.is_err(),
3790 "Data and rule name collisions should be rejected"
3791 );
3792 }
3793
3794 #[test]
3795 fn test_duplicate_data() {
3796 let mut spec = create_test_spec("test");
3797 spec = spec.add_data(create_literal_data(
3798 "age",
3799 Value::Number(rust_decimal::Decimal::from(25)),
3800 ));
3801 spec = spec.add_data(create_literal_data(
3802 "age",
3803 Value::Number(rust_decimal::Decimal::from(30)),
3804 ));
3805
3806 let result = build_graph(&spec, &[spec.clone()]);
3807 assert!(result.is_err(), "Should detect duplicate data");
3808
3809 let errors = result.unwrap_err();
3810 assert!(errors
3811 .iter()
3812 .any(|e| e.to_string().contains("Duplicate data") && e.to_string().contains("age")));
3813 }
3814
3815 #[test]
3816 fn test_duplicate_rule() {
3817 let mut spec = create_test_spec("test");
3818
3819 let rule1 = LemmaRule {
3820 name: "test_rule".to_string(),
3821 expression: create_literal_expr(Value::Boolean(BooleanValue::True)),
3822 unless_clauses: Vec::new(),
3823 source_location: test_source(),
3824 };
3825 let rule2 = LemmaRule {
3826 name: "test_rule".to_string(),
3827 expression: create_literal_expr(Value::Boolean(BooleanValue::False)),
3828 unless_clauses: Vec::new(),
3829 source_location: test_source(),
3830 };
3831
3832 spec = spec.add_rule(rule1);
3833 spec = spec.add_rule(rule2);
3834
3835 let result = build_graph(&spec, &[spec.clone()]);
3836 assert!(result.is_err(), "Should detect duplicate rule");
3837
3838 let errors = result.unwrap_err();
3839 assert!(errors.iter().any(
3840 |e| e.to_string().contains("Duplicate rule") && e.to_string().contains("test_rule")
3841 ));
3842 }
3843
3844 #[test]
3845 fn test_missing_data_reference() {
3846 let mut spec = create_test_spec("test");
3847
3848 let missing_data_expr = ast::Expression {
3849 kind: ast::ExpressionKind::Reference(Reference {
3850 segments: Vec::new(),
3851 name: "nonexistent".to_string(),
3852 }),
3853 source_location: Some(test_source()),
3854 };
3855
3856 let rule = LemmaRule {
3857 name: "test_rule".to_string(),
3858 expression: missing_data_expr,
3859 unless_clauses: Vec::new(),
3860 source_location: test_source(),
3861 };
3862 spec = spec.add_rule(rule);
3863
3864 let result = build_graph(&spec, &[spec.clone()]);
3865 assert!(result.is_err(), "Should detect missing data");
3866
3867 let errors = result.unwrap_err();
3868 assert!(errors
3869 .iter()
3870 .any(|e| e.to_string().contains("Reference 'nonexistent' not found")));
3871 }
3872
3873 #[test]
3874 fn test_missing_spec_reference() {
3875 let mut spec = create_test_spec("test");
3876
3877 let data = LemmaData {
3878 reference: Reference {
3879 segments: Vec::new(),
3880 name: "contract".to_string(),
3881 },
3882 value: ParsedDataValue::Import(crate::parsing::ast::SpecRef::same_repository(
3883 "nonexistent",
3884 )),
3885 source_location: test_source(),
3886 };
3887 spec = spec.add_data(data);
3888
3889 let result = build_graph(&spec, &[spec.clone()]);
3890 assert!(result.is_err(), "Should detect missing spec");
3891
3892 let errors = result.unwrap_err();
3893 assert!(
3894 errors.iter().any(|e| e.to_string().contains("nonexistent")),
3895 "Error should mention nonexistent spec: {:?}",
3896 errors.iter().map(|e| e.to_string()).collect::<Vec<_>>()
3897 );
3898 }
3899
3900 #[test]
3901 fn test_data_reference_conversion() {
3902 let mut spec = create_test_spec("test");
3903 spec = spec.add_data(create_literal_data(
3904 "age",
3905 Value::Number(rust_decimal::Decimal::from(25)),
3906 ));
3907
3908 let age_expr = ast::Expression {
3909 kind: ast::ExpressionKind::Reference(Reference {
3910 segments: Vec::new(),
3911 name: "age".to_string(),
3912 }),
3913 source_location: Some(test_source()),
3914 };
3915
3916 let rule = LemmaRule {
3917 name: "test_rule".to_string(),
3918 expression: age_expr,
3919 unless_clauses: Vec::new(),
3920 source_location: test_source(),
3921 };
3922 spec = spec.add_rule(rule);
3923
3924 let result = build_graph(&spec, &[spec.clone()]);
3925 assert!(result.is_ok(), "Should build graph successfully");
3926
3927 let graph = result.unwrap();
3928 let rule_node = graph.rules().values().next().unwrap();
3929
3930 assert!(matches!(
3931 rule_node.branches[0].1.kind,
3932 ExpressionKind::DataPath(_)
3933 ));
3934 }
3935
3936 #[test]
3937 fn test_rule_reference_conversion() {
3938 let mut spec = create_test_spec("test");
3939
3940 let rule1_expr = ast::Expression {
3941 kind: ast::ExpressionKind::Reference(Reference {
3942 segments: Vec::new(),
3943 name: "age".to_string(),
3944 }),
3945 source_location: Some(test_source()),
3946 };
3947
3948 let rule1 = LemmaRule {
3949 name: "rule1".to_string(),
3950 expression: rule1_expr,
3951 unless_clauses: Vec::new(),
3952 source_location: test_source(),
3953 };
3954 spec = spec.add_rule(rule1);
3955
3956 let rule2_expr = ast::Expression {
3957 kind: ast::ExpressionKind::Reference(Reference {
3958 segments: Vec::new(),
3959 name: "rule1".to_string(),
3960 }),
3961 source_location: Some(test_source()),
3962 };
3963
3964 let rule2 = LemmaRule {
3965 name: "rule2".to_string(),
3966 expression: rule2_expr,
3967 unless_clauses: Vec::new(),
3968 source_location: test_source(),
3969 };
3970 spec = spec.add_rule(rule2);
3971
3972 spec = spec.add_data(create_literal_data(
3973 "age",
3974 Value::Number(rust_decimal::Decimal::from(25)),
3975 ));
3976
3977 let result = build_graph(&spec, &[spec.clone()]);
3978 assert!(result.is_ok(), "Should build graph successfully");
3979
3980 let graph = result.unwrap();
3981 let rule2_node = graph
3982 .rules()
3983 .get(&RulePath {
3984 segments: Vec::new(),
3985 rule: "rule2".to_string(),
3986 })
3987 .unwrap();
3988
3989 assert_eq!(rule2_node.depends_on_rules.len(), 1);
3990 assert!(matches!(
3991 rule2_node.branches[0].1.kind,
3992 ExpressionKind::RulePath(_)
3993 ));
3994 }
3995
3996 #[test]
3997 fn test_collect_multiple_errors() {
3998 let mut spec = create_test_spec("test");
3999 spec = spec.add_data(create_literal_data(
4000 "age",
4001 Value::Number(rust_decimal::Decimal::from(25)),
4002 ));
4003 spec = spec.add_data(create_literal_data(
4004 "age",
4005 Value::Number(rust_decimal::Decimal::from(30)),
4006 ));
4007
4008 let missing_data_expr = ast::Expression {
4009 kind: ast::ExpressionKind::Reference(Reference {
4010 segments: Vec::new(),
4011 name: "nonexistent".to_string(),
4012 }),
4013 source_location: Some(test_source()),
4014 };
4015
4016 let rule = LemmaRule {
4017 name: "test_rule".to_string(),
4018 expression: missing_data_expr,
4019 unless_clauses: Vec::new(),
4020 source_location: test_source(),
4021 };
4022 spec = spec.add_rule(rule);
4023
4024 let result = build_graph(&spec, &[spec.clone()]);
4025 assert!(result.is_err(), "Should collect multiple errors");
4026
4027 let errors = result.unwrap_err();
4028 assert!(errors.len() >= 2, "Should have at least 2 errors");
4029 assert!(errors
4030 .iter()
4031 .any(|e| e.to_string().contains("Duplicate data")));
4032 assert!(errors
4033 .iter()
4034 .any(|e| e.to_string().contains("Reference 'nonexistent' not found")));
4035 }
4036
4037 #[test]
4038 fn test_type_registration_collects_multiple_errors() {
4039 use crate::parsing::ast::{DataValue, ParentType, PrimitiveKind, SpecRef};
4040
4041 let type_source = Source::new(
4042 crate::parsing::source::SourceType::Volatile,
4043 Span {
4044 start: 0,
4045 end: 0,
4046 line: 1,
4047 col: 0,
4048 },
4049 );
4050 let spec_a = create_test_spec("spec_a")
4051 .with_source_type(crate::parsing::source::SourceType::Volatile)
4052 .add_data(LemmaData {
4053 reference: Reference::local("dep".to_string()),
4054 value: DataValue::Import(SpecRef::same_repository("spec_b")),
4055 source_location: type_source.clone(),
4056 })
4057 .add_data(LemmaData {
4058 reference: Reference::local("money".to_string()),
4059 value: DataValue::Definition {
4060 base: Some(ParentType::Primitive {
4061 primitive: PrimitiveKind::Number,
4062 }),
4063 constraints: None,
4064 from: None,
4065 value: None,
4066 },
4067 source_location: type_source.clone(),
4068 })
4069 .add_data(LemmaData {
4070 reference: Reference::local("money".to_string()),
4071 value: DataValue::Definition {
4072 base: Some(ParentType::Primitive {
4073 primitive: PrimitiveKind::Number,
4074 }),
4075 constraints: None,
4076 from: None,
4077 value: None,
4078 },
4079 source_location: type_source,
4080 });
4081
4082 let type_source_b = Source::new(
4083 crate::parsing::source::SourceType::Volatile,
4084 Span {
4085 start: 0,
4086 end: 0,
4087 line: 1,
4088 col: 0,
4089 },
4090 );
4091 let spec_b = create_test_spec("spec_b")
4092 .with_source_type(crate::parsing::source::SourceType::Volatile)
4093 .add_data(LemmaData {
4094 reference: Reference::local("length".to_string()),
4095 value: DataValue::Definition {
4096 base: Some(ParentType::Primitive {
4097 primitive: PrimitiveKind::Number,
4098 }),
4099 constraints: None,
4100 from: None,
4101 value: None,
4102 },
4103 source_location: type_source_b.clone(),
4104 })
4105 .add_data(LemmaData {
4106 reference: Reference::local("length".to_string()),
4107 value: DataValue::Definition {
4108 base: Some(ParentType::Primitive {
4109 primitive: PrimitiveKind::Number,
4110 }),
4111 constraints: None,
4112 from: None,
4113 value: None,
4114 },
4115 source_location: type_source_b,
4116 });
4117
4118 let mut sources = HashMap::new();
4119 sources.insert(
4120 crate::parsing::source::SourceType::Volatile.to_string(),
4121 "spec spec_a\nuses dep: spec_b\ndata money: number\ndata money: number".to_string(),
4122 );
4123 sources.insert(
4124 crate::parsing::source::SourceType::Volatile.to_string(),
4125 "spec spec_b\ndata length: number\ndata length: number".to_string(),
4126 );
4127
4128 let result = build_graph(&spec_a, &[spec_a.clone(), spec_b.clone()]);
4129 assert!(
4130 result.is_err(),
4131 "Should fail with duplicate type/data errors"
4132 );
4133 }
4134
4135 #[test]
4140 fn spec_ref_resolves_to_single_spec_by_name() {
4141 let code = r#"spec myspec
4142data x: 10
4143
4144spec consumer
4145uses m: myspec
4146rule result: m.x"#;
4147 let specs = crate::parse(
4148 code,
4149 crate::parsing::source::SourceType::Volatile,
4150 &crate::ResourceLimits::default(),
4151 )
4152 .unwrap()
4153 .into_flattened_specs();
4154 let consumer = specs.iter().find(|d| d.name == "consumer").unwrap();
4155
4156 let graph = build_graph(consumer, &specs).unwrap();
4157 let data_path = DataPath {
4158 segments: vec![PathSegment {
4159 data: "m".to_string(),
4160 spec: "myspec".to_string(),
4161 }],
4162 data: "x".to_string(),
4163 };
4164 assert!(
4165 graph.data.contains_key(&data_path),
4166 "Ref should resolve to myspec. Data: {:?}",
4167 graph.data.keys().collect::<Vec<_>>()
4168 );
4169 }
4170
4171 #[test]
4172 fn spec_ref_to_nonexistent_spec_is_error() {
4173 let code = r#"spec myspec
4174data x: 10
4175
4176spec consumer
4177uses m: nonexistent
4178rule result: m.x"#;
4179 let specs = crate::parse(
4180 code,
4181 crate::parsing::source::SourceType::Volatile,
4182 &crate::ResourceLimits::default(),
4183 )
4184 .unwrap()
4185 .into_flattened_specs();
4186 let consumer = specs.iter().find(|d| d.name == "consumer").unwrap();
4187 let result = build_graph(consumer, &specs);
4188 assert!(result.is_err(), "Should fail for non-existent spec");
4189 }
4190
4191 #[test]
4196 fn self_reference_is_error() {
4197 let code = "spec myspec\nuses m: myspec";
4198 let specs = crate::parse(
4199 code,
4200 crate::parsing::source::SourceType::Volatile,
4201 &crate::ResourceLimits::default(),
4202 )
4203 .unwrap()
4204 .into_flattened_specs();
4205 let result = build_graph(&specs[0], &specs);
4206 assert!(result.is_err(), "Self-reference should be an error");
4207 let errors = result.unwrap_err();
4208 assert!(
4209 errors.iter().any(|e| {
4210 let s = e.to_string();
4211 s.contains("cycle") || s.contains("myspec")
4212 }),
4213 "Error should mention cycle or self-referencing spec: {:?}",
4214 errors.iter().map(|e| e.to_string()).collect::<Vec<_>>()
4215 );
4216 }
4217}
4218
4219#[derive(Debug, Clone)]
4226pub struct ResolvedSpecTypes {
4227 pub named_types: HashMap<String, LemmaType>,
4229
4230 pub declared_defaults: HashMap<String, ValueKind>,
4235
4236 pub unit_index: HashMap<String, LemmaType>,
4239}
4240
4241#[derive(Debug, Clone, PartialEq)]
4243pub(crate) struct DataTypeDef {
4244 pub parent: ParentType,
4245 pub constraints: Option<Vec<Constraint>>,
4246 pub from: Option<ast::SpecRef>,
4247 pub source: crate::parsing::source::Source,
4248 pub name: String,
4249 pub bound_literal: Option<ast::Value>,
4251}
4252
4253#[derive(Debug, Clone)]
4255pub(crate) struct ResolvedParentSpec {
4256 pub spec: Arc<LemmaSpec>,
4257}
4258
4259#[derive(Debug, Clone)]
4264pub(crate) struct TypeResolver<'a> {
4265 data_types: Vec<(Arc<LemmaSpec>, HashMap<String, DataTypeDef>)>,
4266 context: &'a Context,
4267 all_registered_specs: Vec<(Arc<LemmaRepository>, Arc<LemmaSpec>)>,
4268}
4269
4270fn inferred_parent_type_from_literal(value: &ast::Value) -> ParentType {
4272 match value {
4273 ast::Value::Number(_) => ParentType::Primitive {
4274 primitive: PrimitiveKind::Number,
4275 },
4276 ast::Value::Text(_) => ParentType::Primitive {
4277 primitive: PrimitiveKind::Text,
4278 },
4279 ast::Value::Boolean(_) => ParentType::Primitive {
4280 primitive: PrimitiveKind::Boolean,
4281 },
4282 ast::Value::Date(_) => ParentType::Primitive {
4283 primitive: PrimitiveKind::Date,
4284 },
4285 ast::Value::Time(_) => ParentType::Primitive {
4286 primitive: PrimitiveKind::Time,
4287 },
4288 ast::Value::Duration(_, _) => ParentType::Primitive {
4289 primitive: PrimitiveKind::Duration,
4290 },
4291 ast::Value::Scale(_, _) => ParentType::Primitive {
4292 primitive: PrimitiveKind::Scale,
4293 },
4294 ast::Value::Ratio(_, _) => ParentType::Primitive {
4295 primitive: PrimitiveKind::Ratio,
4296 },
4297 }
4298}
4299
4300impl<'a> TypeResolver<'a> {
4301 pub fn new(context: &'a Context) -> Self {
4302 TypeResolver {
4303 data_types: Vec::new(),
4304 context,
4305 all_registered_specs: Vec::new(),
4306 }
4307 }
4308
4309 pub fn is_registered(&self, spec: &Arc<LemmaSpec>) -> bool {
4310 self.all_registered_specs
4311 .iter()
4312 .any(|(_, s)| Arc::ptr_eq(s, spec))
4313 }
4314
4315 pub fn register_all(
4317 &mut self,
4318 repository: &Arc<LemmaRepository>,
4319 spec: &Arc<LemmaSpec>,
4320 ) -> Vec<Error> {
4321 if !self
4322 .all_registered_specs
4323 .iter()
4324 .any(|(_, s)| Arc::ptr_eq(s, spec))
4325 {
4326 self.all_registered_specs
4327 .push((Arc::clone(repository), Arc::clone(spec)));
4328 }
4329
4330 let mut errors = Vec::new();
4331 for data in &spec.data {
4332 match &data.value {
4333 ParsedDataValue::Definition {
4334 base,
4335 constraints,
4336 from,
4337 value,
4338 } => {
4339 if matches!(
4340 (
4341 base.as_ref(),
4342 constraints.as_ref(),
4343 from.as_ref(),
4344 value.as_ref(),
4345 ),
4346 (
4347 None,
4348 None,
4349 None,
4350 Some(Value::Scale(_, _) | Value::Ratio(_, _)),
4351 )
4352 ) {
4353 continue;
4354 }
4355 let name = &data.reference.name;
4356 let parent = match (base.as_ref(), value.as_ref()) {
4357 (Some(b), _) => b.clone(),
4358 (None, Some(v)) => inferred_parent_type_from_literal(v),
4359 (None, None) => {
4360 errors.push(Error::validation_with_context(
4361 format!(
4362 "Data '{name}' in spec '{}' must declare a type or a literal value",
4363 spec.name
4364 ),
4365 Some(data.source_location.clone()),
4366 None::<String>,
4367 Some(Arc::clone(spec)),
4368 None,
4369 ));
4370 continue;
4371 }
4372 };
4373 let ftd = DataTypeDef {
4374 parent,
4375 constraints: constraints.clone(),
4376 from: from.clone(),
4377 source: data.source_location.clone(),
4378 name: name.clone(),
4379 bound_literal: value.clone(),
4380 };
4381 if let Err(e) = self.register_type(spec, ftd) {
4382 errors.push(e);
4383 }
4384 }
4385 ParsedDataValue::Reference { .. } | ParsedDataValue::Import(_) => {}
4386 }
4387 }
4388 errors
4389 }
4390
4391 pub fn register_type(&mut self, spec: &Arc<LemmaSpec>, def: DataTypeDef) -> Result<(), Error> {
4393 let spec_types = if let Some(pos) = self
4394 .data_types
4395 .iter()
4396 .position(|(s, _)| Arc::ptr_eq(s, spec))
4397 {
4398 &mut self.data_types[pos].1
4399 } else {
4400 self.data_types.push((Arc::clone(spec), HashMap::new()));
4401 let last = self.data_types.len() - 1;
4402 &mut self.data_types[last].1
4403 };
4404 if spec_types.contains_key(&def.name) {
4405 return Err(Error::validation_with_context(
4406 format!(
4407 "Duplicate data '{}' (already declared in spec '{}')",
4408 def.name, spec.name
4409 ),
4410 Some(def.source.clone()),
4411 None::<String>,
4412 Some(Arc::clone(spec)),
4413 None,
4414 ));
4415 }
4416 spec_types.insert(def.name.clone(), def);
4417 Ok(())
4418 }
4419
4420 pub fn resolve_and_validate(
4423 &self,
4424 spec: &Arc<LemmaSpec>,
4425 at: &EffectiveDate,
4426 ) -> Result<ResolvedSpecTypes, Vec<Error>> {
4427 let resolved_types = self.resolve_types_internal(spec, at)?;
4428 let mut errors = Vec::new();
4429
4430 for (type_name, lemma_type) in &resolved_types.named_types {
4431 let source = self
4432 .data_types
4433 .iter()
4434 .find(|(s, _)| Arc::ptr_eq(s, spec))
4435 .and_then(|(_, defs)| defs.get(type_name))
4436 .map(|ftd| ftd.source.clone())
4437 .unwrap_or_else(|| {
4438 unreachable!(
4439 "BUG: resolved type '{}' has no corresponding DataTypeDef in spec '{}'",
4440 type_name, spec.name
4441 )
4442 });
4443 let mut spec_errors = validate_type_specifications(
4444 &lemma_type.specifications,
4445 resolved_types.declared_defaults.get(type_name),
4446 type_name,
4447 &source,
4448 Some(Arc::clone(spec)),
4449 );
4450 errors.append(&mut spec_errors);
4451 }
4452
4453 if errors.is_empty() {
4454 Ok(resolved_types)
4455 } else {
4456 Err(errors)
4457 }
4458 }
4459
4460 fn resolve_types_internal(
4465 &self,
4466 spec: &Arc<LemmaSpec>,
4467 at: &EffectiveDate,
4468 ) -> Result<ResolvedSpecTypes, Vec<Error>> {
4469 let mut named_types = HashMap::new();
4470 let mut declared_defaults: HashMap<String, ValueKind> = HashMap::new();
4471 let mut visited: Vec<(Arc<LemmaSpec>, String)> = Vec::new();
4472
4473 if let Some((_, spec_types)) = self.data_types.iter().find(|(s, _)| Arc::ptr_eq(s, spec)) {
4474 for type_name in spec_types.keys() {
4475 match self.resolve_type_internal(spec, type_name, &mut visited, at) {
4476 Ok(Some((resolved_type, declared_default))) => {
4477 named_types.insert(type_name.clone(), resolved_type);
4478 if let Some(dv) = declared_default {
4479 declared_defaults.insert(type_name.clone(), dv);
4480 }
4481 }
4482 Ok(None) => {
4483 unreachable!(
4484 "BUG: registered type '{}' could not be resolved (spec='{}')",
4485 type_name, spec.name
4486 );
4487 }
4488 Err(es) => return Err(es),
4489 }
4490 visited.clear();
4491 }
4492 }
4493
4494 let mut unit_index_tmp: HashMap<String, (LemmaType, Option<DataTypeDef>)> = HashMap::new();
4496 let mut errors = Vec::new();
4497
4498 let prim_ratio = semantics::primitive_ratio();
4499 for unit in Self::extract_units_from_type(&prim_ratio.specifications) {
4500 unit_index_tmp.insert(unit, (prim_ratio.clone(), None));
4501 }
4502
4503 for (type_name, resolved_type) in &named_types {
4504 let data_type_def = self
4505 .data_types
4506 .iter()
4507 .find(|(s, _)| Arc::ptr_eq(s, spec))
4508 .and_then(|(_, defs)| defs.get(type_name.as_str()))
4509 .expect("BUG: type was resolved but not in registry");
4510 let e: Result<(), Error> = if resolved_type.is_scale() {
4511 Self::add_scale_units_to_index(
4512 spec,
4513 &mut unit_index_tmp,
4514 resolved_type,
4515 data_type_def,
4516 )
4517 } else if resolved_type.is_ratio() {
4518 Self::add_ratio_units_to_index(
4519 spec,
4520 &mut unit_index_tmp,
4521 resolved_type,
4522 data_type_def,
4523 )
4524 } else {
4525 Ok(())
4526 };
4527 if let Err(e) = e {
4528 errors.push(e);
4529 }
4530 }
4531
4532 if !errors.is_empty() {
4533 return Err(errors);
4534 }
4535
4536 let unit_index = unit_index_tmp
4537 .into_iter()
4538 .map(|(k, (lt, _))| (k, lt))
4539 .collect();
4540
4541 Ok(ResolvedSpecTypes {
4542 named_types,
4543 declared_defaults,
4544 unit_index,
4545 })
4546 }
4547
4548 fn resolve_type_internal(
4549 &self,
4550 spec: &Arc<LemmaSpec>,
4551 name: &str,
4552 visited: &mut Vec<(Arc<LemmaSpec>, String)>,
4553 at: &EffectiveDate,
4554 ) -> Result<Option<(LemmaType, Option<ValueKind>)>, Vec<Error>> {
4555 if visited
4556 .iter()
4557 .any(|(s, n)| Arc::ptr_eq(s, spec) && n == name)
4558 {
4559 let source_location = self
4560 .data_types
4561 .iter()
4562 .find(|(s, _)| Arc::ptr_eq(s, spec))
4563 .and_then(|(_, dt)| dt.get(name))
4564 .map(|ftd| ftd.source.clone())
4565 .unwrap_or_else(|| {
4566 unreachable!(
4567 "BUG: circular dependency detected for type '{}::{}' but type definition not found in registry",
4568 spec.name, name
4569 )
4570 });
4571 return Err(vec![Error::validation_with_context(
4572 format!(
4573 "Circular dependency detected in type resolution: {}::{}",
4574 spec.name, name
4575 ),
4576 Some(source_location),
4577 None::<String>,
4578 Some(Arc::clone(spec)),
4579 None,
4580 )]);
4581 }
4582 visited.push((Arc::clone(spec), name.to_string()));
4583
4584 let ftd = match self
4585 .data_types
4586 .iter()
4587 .find(|(s, _)| Arc::ptr_eq(s, spec))
4588 .and_then(|(_, dt)| dt.get(name))
4589 {
4590 Some(def) => def.clone(),
4591 None => {
4592 if let Some(pos) = visited
4593 .iter()
4594 .position(|(s, n)| Arc::ptr_eq(s, spec) && n == name)
4595 {
4596 visited.remove(pos);
4597 }
4598 return Ok(None);
4599 }
4600 };
4601
4602 let parent = ftd.parent.clone();
4603 let from = ftd.from.clone();
4604 let constraints = ftd.constraints.clone();
4605
4606 let (parent_specs, parent_declared_default) = match self.resolve_parent(
4607 spec,
4608 &parent,
4609 &from,
4610 visited,
4611 &ftd.source,
4612 at,
4613 ) {
4614 Ok(Some(pair)) => pair,
4615 Ok(None) => {
4616 if let Some(pos) = visited
4617 .iter()
4618 .position(|(s, n)| Arc::ptr_eq(s, spec) && n == name)
4619 {
4620 visited.remove(pos);
4621 }
4622 return Err(vec![Error::validation_with_context(
4623 format!("Unknown parent '{}' for data definition. Parent must be defined before use. Valid primitive types are: boolean, scale, number, ratio, text, date, time, duration, percent", parent),
4624 Some(ftd.source.clone()),
4625 None::<String>,
4626 Some(Arc::clone(spec)),
4627 None,
4628 )]);
4629 }
4630 Err(es) => {
4631 if let Some(pos) = visited
4632 .iter()
4633 .position(|(s, n)| Arc::ptr_eq(s, spec) && n == name)
4634 {
4635 visited.remove(pos);
4636 }
4637 return Err(es);
4638 }
4639 };
4640
4641 let mut declared_default = parent_declared_default;
4642 let final_specs = if let Some(constraints) = &constraints {
4643 match apply_constraints_to_spec(
4644 spec,
4645 parent_specs,
4646 constraints,
4647 &ftd.source,
4648 &mut declared_default,
4649 ) {
4650 Ok(specs) => specs,
4651 Err(errors) => {
4652 if let Some(pos) = visited
4653 .iter()
4654 .position(|(s, n)| Arc::ptr_eq(s, spec) && n == name)
4655 {
4656 visited.remove(pos);
4657 }
4658 return Err(errors);
4659 }
4660 }
4661 } else {
4662 parent_specs
4663 };
4664
4665 if let Some(pos) = visited
4666 .iter()
4667 .position(|(s, n)| Arc::ptr_eq(s, spec) && n == name)
4668 {
4669 visited.remove(pos);
4670 }
4671
4672 let extends = {
4673 let parent_name = parent.to_string();
4674 let parent_spec = match self.get_spec_arc_for_parent(spec, &from, &ftd.source, at) {
4675 Ok(x) => x,
4676 Err(e) => return Err(vec![e]),
4677 };
4678 let family = match &parent_spec {
4679 Some(r) => {
4680 if matches!(parent, ParentType::Primitive { .. }) {
4681 name.to_string()
4682 } else {
4683 match self.resolve_type_internal(&r.spec, &parent_name, visited, at) {
4684 Ok(Some((parent_type, _))) => parent_type
4685 .scale_family_name()
4686 .map(String::from)
4687 .unwrap_or_else(|| name.to_string()),
4688 Ok(None) => name.to_string(),
4689 Err(es) => return Err(es),
4690 }
4691 }
4692 }
4693 None => name.to_string(),
4694 };
4695 let defining_spec = if from.is_some() {
4696 match &parent_spec {
4697 Some(r) => TypeDefiningSpec::Import {
4698 spec: Arc::clone(&r.spec),
4699 },
4700 None => unreachable!(
4701 "BUG: from.is_some() but get_spec_arc_for_parent returned Ok(None)"
4702 ),
4703 }
4704 } else {
4705 TypeDefiningSpec::Local
4706 };
4707 TypeExtends::Custom {
4708 parent: parent_name,
4709 family,
4710 defining_spec,
4711 }
4712 };
4713
4714 let declared_default = match &ftd.bound_literal {
4715 Some(lit) => match semantics::value_to_semantic(lit) {
4716 Ok(vk) => Some(vk),
4717 Err(message) => {
4718 return Err(vec![Error::validation_with_context(
4719 message,
4720 Some(ftd.source.clone()),
4721 None::<String>,
4722 Some(Arc::clone(spec)),
4723 None,
4724 )]);
4725 }
4726 },
4727 None => declared_default,
4728 };
4729
4730 Ok(Some((
4731 LemmaType {
4732 name: Some(parent.to_string()),
4733 specifications: final_specs,
4734 extends,
4735 },
4736 declared_default,
4737 )))
4738 }
4739
4740 fn resolve_parent(
4741 &self,
4742 spec: &Arc<LemmaSpec>,
4743 parent: &ParentType,
4744 from: &Option<crate::parsing::ast::SpecRef>,
4745 visited: &mut Vec<(Arc<LemmaSpec>, String)>,
4746 source: &crate::parsing::source::Source,
4747 at: &EffectiveDate,
4748 ) -> Result<Option<(TypeSpecification, Option<ValueKind>)>, Vec<Error>> {
4749 if let ParentType::Primitive { primitive: kind } = parent {
4750 return Ok(Some((semantics::type_spec_for_primitive(*kind), None)));
4751 }
4752
4753 let parent_name = match parent {
4754 ParentType::Custom { name } => name.as_str(),
4755 ParentType::Primitive { .. } => unreachable!("already returned above"),
4756 };
4757
4758 let parent_spec = match self.get_spec_arc_for_parent(spec, from, source, at) {
4759 Ok(x) => x,
4760 Err(e) => return Err(vec![e]),
4761 };
4762 let result = match &parent_spec {
4763 Some(r) => self.resolve_type_internal(&r.spec, parent_name, visited, at),
4764 None => Ok(None),
4765 };
4766 match result {
4767 Ok(Some((t, declared_default))) => Ok(Some((t.specifications, declared_default))),
4768 Ok(None) => {
4769 let type_exists = parent_spec
4770 .as_ref()
4771 .and_then(|r| {
4772 self.data_types
4773 .iter()
4774 .find(|(s, _)| Arc::ptr_eq(s, &r.spec))
4775 .map(|(_, m)| m)
4776 })
4777 .map(|spec_types| spec_types.contains_key(parent_name))
4778 .unwrap_or(false);
4779
4780 if !type_exists {
4781 if from.is_none()
4782 && spec.data.iter().any(|d| {
4783 d.reference.is_local()
4784 && d.reference.name == parent_name
4785 && matches!(&d.value, ParsedDataValue::Import(_))
4786 })
4787 {
4788 return Err(vec![Error::validation_with_context(
4789 format!(
4790 "'{}' is a spec reference and cannot carry a value: a spec reference is not a type and cannot be referenced from a data declaration",
4791 parent_name
4792 ),
4793 Some(source.clone()),
4794 Some(format!(
4795 "To reference data inside the spec, use a dotted path like '{}.<data_name>'",
4796 parent_name
4797 )),
4798 Some(Arc::clone(spec)),
4799 None,
4800 )]);
4801 }
4802 let suggestion = if parent_spec.is_some() {
4803 from.as_ref().map(|r| {
4804 let qualifier = r.repository.as_ref()
4805 .map(|q| format!(" from {}", q.name))
4806 .unwrap_or_default();
4807 format!(
4808 "Data '{}' is not defined in spec '{}'{qualifier}. Only data declarations (not rules) can be imported with `from`.",
4809 parent_name, r.name,
4810 )
4811 })
4812 } else {
4813 from.as_ref()
4814 .and_then(|r| r.repository.as_ref())
4815 .filter(|q| q.is_registry())
4816 .map(|q| {
4817 format!(
4818 "Run `lemma fetch --all` or `lemma fetch {}` to fetch this dependency.",
4819 q.name
4820 )
4821 })
4822 };
4823 Err(vec![Error::validation_with_context(
4824 format!("Unknown parent '{}' for data definition. Parent must be defined before use. Valid primitive types are: boolean, scale, number, ratio, text, date, time, duration, percent", parent),
4825 Some(source.clone()),
4826 suggestion,
4827 Some(Arc::clone(spec)),
4828 None,
4829 )])
4830 } else {
4831 Ok(None)
4832 }
4833 }
4834 Err(es) => Err(es),
4835 }
4836 }
4837
4838 fn get_spec_arc_for_parent(
4839 &self,
4840 spec: &Arc<LemmaSpec>,
4841 from: &Option<crate::parsing::ast::SpecRef>,
4842 import_site: &crate::parsing::source::Source,
4843 at: &EffectiveDate,
4844 ) -> Result<Option<ResolvedParentSpec>, Error> {
4845 match from {
4846 Some(from_ref) => self
4847 .resolve_spec_for_import(spec, from_ref, import_site, at)
4848 .map(|(_, arc)| Some(ResolvedParentSpec { spec: arc })),
4849 None => Ok(Some(ResolvedParentSpec {
4850 spec: Arc::clone(spec),
4851 })),
4852 }
4853 }
4854
4855 fn resolve_spec_for_import(
4856 &self,
4857 spec: &Arc<LemmaSpec>,
4858 from: &crate::parsing::ast::SpecRef,
4859 import_site: &crate::parsing::source::Source,
4860 at: &EffectiveDate,
4861 ) -> Result<(Arc<LemmaRepository>, Arc<LemmaSpec>), Error> {
4862 let consumer_repository = self
4863 .all_registered_specs
4864 .iter()
4865 .find(|(_, s)| Arc::ptr_eq(s, spec))
4866 .map(|(r, _)| Arc::clone(r))
4867 .unwrap_or_else(|| self.context.workspace());
4868 discovery::resolve_spec_ref(
4869 self.context,
4870 from,
4871 &consumer_repository,
4872 at,
4873 &spec.name,
4874 Some(import_site.clone()),
4875 Some(Arc::clone(spec)),
4876 )
4877 }
4878
4879 fn add_scale_units_to_index(
4884 spec: &Arc<LemmaSpec>,
4885 unit_index: &mut HashMap<String, (LemmaType, Option<DataTypeDef>)>,
4886 resolved_type: &LemmaType,
4887 defined_by: &DataTypeDef,
4888 ) -> Result<(), Error> {
4889 let units = Self::extract_units_from_type(&resolved_type.specifications);
4890 for unit in units {
4891 if let Some((existing_type, existing_def)) = unit_index.get(&unit) {
4892 let same_type = existing_def.as_ref() == Some(defined_by);
4893
4894 if same_type {
4895 return Err(Error::validation_with_context(
4896 format!(
4897 "Unit '{}' is defined more than once in type '{}'",
4898 unit, defined_by.name
4899 ),
4900 Some(defined_by.source.clone()),
4901 None::<String>,
4902 Some(Arc::clone(spec)),
4903 None,
4904 ));
4905 }
4906
4907 let existing_name: String = existing_def
4908 .as_ref()
4909 .map(|d| d.name.clone())
4910 .unwrap_or_else(|| existing_type.name());
4911 let current_extends_existing = resolved_type
4912 .extends
4913 .parent_name()
4914 .map(|p| p == existing_name.as_str())
4915 .unwrap_or(false);
4916 let existing_extends_current = existing_type
4917 .extends
4918 .parent_name()
4919 .map(|p| p == defined_by.name.as_str())
4920 .unwrap_or(false);
4921
4922 if existing_type.is_scale()
4923 && (current_extends_existing || existing_extends_current)
4924 {
4925 if current_extends_existing {
4926 unit_index.insert(unit, (resolved_type.clone(), Some(defined_by.clone())));
4927 }
4928 continue;
4929 }
4930
4931 if existing_type.same_scale_family(resolved_type) {
4932 continue;
4933 }
4934
4935 return Err(Error::validation_with_context(
4936 format!(
4937 "Ambiguous unit '{}'. Defined in multiple types: '{}' and '{}'",
4938 unit, existing_name, defined_by.name
4939 ),
4940 Some(defined_by.source.clone()),
4941 None::<String>,
4942 Some(Arc::clone(spec)),
4943 None,
4944 ));
4945 }
4946 unit_index.insert(unit, (resolved_type.clone(), Some(defined_by.clone())));
4947 }
4948 Ok(())
4949 }
4950
4951 fn add_ratio_units_to_index(
4952 spec: &Arc<LemmaSpec>,
4953 unit_index: &mut HashMap<String, (LemmaType, Option<DataTypeDef>)>,
4954 resolved_type: &LemmaType,
4955 defined_by: &DataTypeDef,
4956 ) -> Result<(), Error> {
4957 let units = Self::extract_units_from_type(&resolved_type.specifications);
4958 for unit in units {
4959 if let Some((existing_type, existing_def)) = unit_index.get(&unit) {
4960 if existing_type.is_ratio() {
4961 continue;
4962 }
4963 let existing_name: String = existing_def
4964 .as_ref()
4965 .map(|d| d.name.clone())
4966 .unwrap_or_else(|| existing_type.name());
4967 return Err(Error::validation_with_context(
4968 format!(
4969 "Ambiguous unit '{}'. Defined in multiple types: '{}' and '{}'",
4970 unit, existing_name, defined_by.name
4971 ),
4972 Some(defined_by.source.clone()),
4973 None::<String>,
4974 Some(Arc::clone(spec)),
4975 None,
4976 ));
4977 }
4978 unit_index.insert(unit, (resolved_type.clone(), Some(defined_by.clone())));
4979 }
4980 Ok(())
4981 }
4982
4983 fn extract_units_from_type(specs: &TypeSpecification) -> Vec<String> {
4984 match specs {
4985 TypeSpecification::Scale { units, .. } => {
4986 units.iter().map(|unit| unit.name.clone()).collect()
4987 }
4988 TypeSpecification::Ratio { units, .. } => {
4989 units.iter().map(|unit| unit.name.clone()).collect()
4990 }
4991 _ => Vec::new(),
4992 }
4993 }
4994}
4995
4996#[cfg(test)]
4997mod type_resolution_tests {
4998 use super::*;
4999 use crate::parse;
5000 use crate::parsing::ast::{
5001 CommandArg, LemmaSpec, ParentType, PrimitiveKind, TypeConstraintCommand,
5002 };
5003 use crate::ResourceLimits;
5004 use rust_decimal::Decimal;
5005 use std::sync::Arc;
5006
5007 fn test_context_and_effective(
5008 specs: &[Arc<LemmaSpec>],
5009 ) -> (&'static Context, &'static EffectiveDate) {
5010 use crate::engine::Context;
5011 let mut ctx = Context::new();
5012 let repository = ctx.workspace();
5013 for s in specs {
5014 ctx.insert_spec(Arc::clone(&repository), Arc::clone(s))
5015 .unwrap();
5016 }
5017 let ctx = Box::leak(Box::new(ctx));
5018 let eff = Box::leak(Box::new(EffectiveDate::Origin));
5019 (ctx, eff)
5020 }
5021
5022 fn dag_and_spec() -> (Vec<Arc<LemmaSpec>>, Arc<LemmaSpec>) {
5023 let spec = LemmaSpec::new("test_spec".to_string());
5024 let arc = Arc::new(spec);
5025 let dag = vec![Arc::clone(&arc)];
5026 (dag, arc)
5027 }
5028
5029 fn resolver_for_code(code: &str) -> (TypeResolver<'static>, Vec<Arc<LemmaSpec>>) {
5030 let specs = parse(
5031 code,
5032 crate::parsing::source::SourceType::Volatile,
5033 &ResourceLimits::default(),
5034 )
5035 .unwrap()
5036 .into_flattened_specs();
5037 let spec_arcs: Vec<Arc<LemmaSpec>> = specs.iter().map(|s| Arc::new(s.clone())).collect();
5038 let (ctx, _) = test_context_and_effective(&spec_arcs);
5039 let repository = ctx.workspace();
5040 let mut resolver = TypeResolver::new(ctx);
5041 for spec_arc in &spec_arcs {
5042 resolver.register_all(&repository, spec_arc);
5043 }
5044 (resolver, spec_arcs)
5045 }
5046
5047 fn resolver_single_spec(code: &str) -> (TypeResolver<'static>, Arc<LemmaSpec>) {
5048 let (resolver, spec_arcs) = resolver_for_code(code);
5049 let spec_arc = spec_arcs.into_iter().next().expect("at least one spec");
5050 (resolver, spec_arc)
5051 }
5052
5053 #[test]
5054 fn test_type_spec_for_primitive_covers_all_variants() {
5055 use crate::parsing::ast::PrimitiveKind;
5056 use crate::planning::semantics::type_spec_for_primitive;
5057
5058 for kind in [
5059 PrimitiveKind::Boolean,
5060 PrimitiveKind::Scale,
5061 PrimitiveKind::Number,
5062 PrimitiveKind::Percent,
5063 PrimitiveKind::Ratio,
5064 PrimitiveKind::Text,
5065 PrimitiveKind::Date,
5066 PrimitiveKind::Time,
5067 PrimitiveKind::Duration,
5068 ] {
5069 let spec = type_spec_for_primitive(kind);
5070 assert!(
5071 !matches!(
5072 spec,
5073 crate::planning::semantics::TypeSpecification::Undetermined
5074 ),
5075 "type_spec_for_primitive({:?}) returned Undetermined",
5076 kind
5077 );
5078 }
5079 }
5080
5081 #[test]
5082 fn test_register_data_type_def() {
5083 let (dag, spec_arc) = dag_and_spec();
5084 let (ctx, _) = test_context_and_effective(&dag);
5085 let mut resolver = TypeResolver::new(ctx);
5086 let ftd = DataTypeDef {
5087 parent: ParentType::Primitive {
5088 primitive: PrimitiveKind::Number,
5089 },
5090 constraints: Some(vec![
5091 (
5092 TypeConstraintCommand::Minimum,
5093 vec![CommandArg::Literal(crate::literals::Value::Number(
5094 Decimal::ZERO,
5095 ))],
5096 ),
5097 (
5098 TypeConstraintCommand::Maximum,
5099 vec![CommandArg::Literal(crate::literals::Value::Number(
5100 Decimal::from(150),
5101 ))],
5102 ),
5103 ]),
5104 from: None,
5105 source: crate::parsing::source::Source::new(
5106 crate::parsing::source::SourceType::Volatile,
5107 crate::parsing::ast::Span {
5108 start: 0,
5109 end: 0,
5110 line: 1,
5111 col: 0,
5112 },
5113 ),
5114 name: "age".to_string(),
5115 bound_literal: None,
5116 };
5117
5118 let result = resolver.register_type(&spec_arc, ftd);
5119 assert!(result.is_ok());
5120 let resolved = resolver
5121 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
5122 .unwrap();
5123 assert!(resolved.named_types.contains_key("age"));
5124 }
5125
5126 #[test]
5127 fn test_register_duplicate_type_fails() {
5128 let (dag, spec_arc) = dag_and_spec();
5129 let (ctx, _) = test_context_and_effective(&dag);
5130 let mut resolver = TypeResolver::new(ctx);
5131 let ftd = DataTypeDef {
5132 parent: ParentType::Primitive {
5133 primitive: PrimitiveKind::Number,
5134 },
5135 constraints: None,
5136 from: None,
5137 source: crate::parsing::source::Source::new(
5138 crate::parsing::source::SourceType::Volatile,
5139 crate::parsing::ast::Span {
5140 start: 0,
5141 end: 0,
5142 line: 1,
5143 col: 0,
5144 },
5145 ),
5146 name: "money".to_string(),
5147 bound_literal: None,
5148 };
5149 resolver.register_type(&spec_arc, ftd.clone()).unwrap();
5150 let result = resolver.register_type(&spec_arc, ftd);
5151 assert!(result.is_err());
5152 }
5153
5154 #[test]
5155 fn test_resolve_custom_type_from_primitive() {
5156 let (dag, spec_arc) = dag_and_spec();
5157 let (ctx, _) = test_context_and_effective(&dag);
5158 let mut resolver = TypeResolver::new(ctx);
5159 let ftd = DataTypeDef {
5160 parent: ParentType::Primitive {
5161 primitive: PrimitiveKind::Number,
5162 },
5163 constraints: None,
5164 from: None,
5165 source: crate::parsing::source::Source::new(
5166 crate::parsing::source::SourceType::Volatile,
5167 crate::parsing::ast::Span {
5168 start: 0,
5169 end: 0,
5170 line: 1,
5171 col: 0,
5172 },
5173 ),
5174 name: "money".to_string(),
5175 bound_literal: None,
5176 };
5177
5178 resolver.register_type(&spec_arc, ftd).unwrap();
5179 let resolved = resolver
5180 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
5181 .unwrap();
5182
5183 assert!(resolved.named_types.contains_key("money"));
5184 let money_type = resolved.named_types.get("money").unwrap();
5185 assert_eq!(money_type.name, Some("number".to_string()));
5186 }
5187
5188 #[test]
5189 fn test_type_definition_resolution() {
5190 let (resolver, spec_arc) = resolver_single_spec(
5191 r#"spec test
5192data dice: number -> minimum 0 -> maximum 6"#,
5193 );
5194
5195 let resolved_types = resolver
5196 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
5197 .unwrap();
5198 let dice_type = resolved_types.named_types.get("dice").unwrap();
5199
5200 match &dice_type.specifications {
5201 TypeSpecification::Number {
5202 minimum, maximum, ..
5203 } => {
5204 assert_eq!(*minimum, Some(Decimal::from(0)));
5205 assert_eq!(*maximum, Some(Decimal::from(6)));
5206 }
5207 _ => panic!("Expected Number type specifications"),
5208 }
5209 }
5210
5211 #[test]
5212 fn test_type_definition_with_multiple_commands() {
5213 let (resolver, spec_arc) = resolver_single_spec(
5214 r#"spec test
5215data money: scale -> decimals 2 -> unit eur 1.0 -> unit usd 1.18"#,
5216 );
5217
5218 let resolved_types = resolver
5219 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
5220 .unwrap();
5221 let money_type = resolved_types.named_types.get("money").unwrap();
5222
5223 match &money_type.specifications {
5224 TypeSpecification::Scale {
5225 decimals, units, ..
5226 } => {
5227 assert_eq!(*decimals, Some(2));
5228 assert_eq!(units.len(), 2);
5229 assert!(units.iter().any(|u| u.name == "eur"));
5230 assert!(units.iter().any(|u| u.name == "usd"));
5231 }
5232 _ => panic!("Expected Scale type specifications"),
5233 }
5234 }
5235
5236 #[test]
5237 fn test_number_type_with_decimals() {
5238 let (resolver, spec_arc) = resolver_single_spec(
5239 r#"spec test
5240data price: number -> decimals 2 -> minimum 0"#,
5241 );
5242
5243 let resolved_types = resolver
5244 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
5245 .unwrap();
5246 let price_type = resolved_types.named_types.get("price").unwrap();
5247
5248 match &price_type.specifications {
5249 TypeSpecification::Number {
5250 decimals, minimum, ..
5251 } => {
5252 assert_eq!(*decimals, Some(2));
5253 assert_eq!(*minimum, Some(Decimal::from(0)));
5254 }
5255 _ => panic!("Expected Number type specifications with decimals"),
5256 }
5257 }
5258
5259 #[test]
5260 fn test_number_type_decimals_only() {
5261 let (resolver, spec_arc) = resolver_single_spec(
5262 r#"spec test
5263data precise_number: number -> decimals 4"#,
5264 );
5265
5266 let resolved_types = resolver
5267 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
5268 .unwrap();
5269 let precise_type = resolved_types.named_types.get("precise_number").unwrap();
5270
5271 match &precise_type.specifications {
5272 TypeSpecification::Number { decimals, .. } => {
5273 assert_eq!(*decimals, Some(4));
5274 }
5275 _ => panic!("Expected Number type with decimals 4"),
5276 }
5277 }
5278
5279 #[test]
5280 fn test_scale_type_decimals_only() {
5281 let (resolver, spec_arc) = resolver_single_spec(
5282 r#"spec test
5283data weight: scale -> unit kg 1 -> decimals 3"#,
5284 );
5285
5286 let resolved_types = resolver
5287 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
5288 .unwrap();
5289 let weight_type = resolved_types.named_types.get("weight").unwrap();
5290
5291 match &weight_type.specifications {
5292 TypeSpecification::Scale { decimals, .. } => {
5293 assert_eq!(*decimals, Some(3));
5294 }
5295 _ => panic!("Expected Scale type with decimals 3"),
5296 }
5297 }
5298
5299 #[test]
5300 fn test_ratio_type_accepts_optional_decimals_command() {
5301 let (resolver, spec_arc) = resolver_single_spec(
5302 r#"spec test
5303data ratio_type: ratio -> decimals 2"#,
5304 );
5305
5306 let resolved_types = resolver
5307 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
5308 .unwrap();
5309 let ratio_type = resolved_types.named_types.get("ratio_type").unwrap();
5310
5311 match &ratio_type.specifications {
5312 TypeSpecification::Ratio { decimals, .. } => {
5313 assert_eq!(
5314 *decimals,
5315 Some(2),
5316 "ratio type should accept decimals command"
5317 );
5318 }
5319 _ => panic!("Expected Ratio type with decimals 2"),
5320 }
5321 }
5322
5323 #[test]
5324 fn test_ratio_type_with_default_command() {
5325 let (resolver, spec_arc) = resolver_single_spec(
5326 r#"spec test
5327data percentage: ratio -> minimum 0 -> maximum 1 -> default 0.5"#,
5328 );
5329
5330 let resolved_types = resolver
5331 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
5332 .unwrap();
5333 let percentage_type = resolved_types.named_types.get("percentage").unwrap();
5334
5335 match &percentage_type.specifications {
5336 TypeSpecification::Ratio {
5337 minimum, maximum, ..
5338 } => {
5339 assert_eq!(
5340 *minimum,
5341 Some(Decimal::from(0)),
5342 "ratio type should have minimum 0"
5343 );
5344 assert_eq!(
5345 *maximum,
5346 Some(Decimal::from(1)),
5347 "ratio type should have maximum 1"
5348 );
5349 }
5350 _ => panic!("Expected Ratio type with minimum and maximum"),
5351 }
5352
5353 let declared = resolved_types
5354 .declared_defaults
5355 .get("percentage")
5356 .expect("declared default must be tracked for percentage");
5357 match declared {
5358 ValueKind::Ratio(v, _) => assert_eq!(*v, Decimal::from_i128_with_scale(5, 1)),
5359 other => panic!("expected Ratio declared default, got {:?}", other),
5360 }
5361 }
5362
5363 #[test]
5364 fn test_scale_extension_chain_same_family_units_allowed() {
5365 let (resolver, spec_arc) = resolver_single_spec(
5366 r#"spec test
5367data money: scale -> unit eur 1
5368data money2: money -> unit usd 1.24"#,
5369 );
5370
5371 let result = resolver.resolve_types_internal(&spec_arc, &EffectiveDate::Origin);
5372 assert!(
5373 result.is_ok(),
5374 "Scale extension chain should resolve: {:?}",
5375 result.err()
5376 );
5377
5378 let resolved = result.unwrap();
5379 assert!(
5380 resolved.unit_index.contains_key("eur"),
5381 "eur should be in unit_index"
5382 );
5383 assert!(
5384 resolved.unit_index.contains_key("usd"),
5385 "usd should be in unit_index"
5386 );
5387 let eur_type = resolved.unit_index.get("eur").unwrap();
5388 let usd_type = resolved.unit_index.get("usd").unwrap();
5389 assert_eq!(
5390 eur_type.name.as_deref(),
5391 Some("money"),
5392 "more derived type (money2) should own eur; its parent name is 'money'"
5393 );
5394 assert_eq!(
5395 usd_type.name.as_deref(),
5396 Some("money"),
5397 "usd defined on money2 whose parent is 'money'"
5398 );
5399 }
5400
5401 #[test]
5402 fn test_invalid_parent_type_in_named_type_should_error() {
5403 let (resolver, spec_arc) = resolver_single_spec(
5404 r#"spec test
5405data invalid: nonexistent_type -> minimum 0"#,
5406 );
5407
5408 let result = resolver.resolve_types_internal(&spec_arc, &EffectiveDate::Origin);
5409 assert!(result.is_err(), "Should reject invalid parent type");
5410
5411 let errs = result.unwrap_err();
5412 assert!(!errs.is_empty(), "expected at least one error");
5413 let error_msg = errs[0].to_string();
5414 assert!(
5415 error_msg.contains("Unknown parent") && error_msg.contains("nonexistent_type"),
5416 "Error should mention unknown type. Got: {}",
5417 error_msg
5418 );
5419 }
5420
5421 #[test]
5422 fn test_invalid_primitive_type_name_should_error() {
5423 let (resolver, spec_arc) = resolver_single_spec(
5424 r#"spec test
5425data invalid: choice -> option "a""#,
5426 );
5427
5428 let result = resolver.resolve_types_internal(&spec_arc, &EffectiveDate::Origin);
5429 assert!(result.is_err(), "Should reject invalid type base 'choice'");
5430
5431 let errs = result.unwrap_err();
5432 assert!(!errs.is_empty(), "expected at least one error");
5433 let error_msg = errs[0].to_string();
5434 assert!(
5435 error_msg.contains("Unknown parent") && error_msg.contains("choice"),
5436 "Error should mention unknown type 'choice'. Got: {}",
5437 error_msg
5438 );
5439 }
5440
5441 #[test]
5442 fn test_scale_extension_overrides_parent_unit_factors() {
5443 let (resolver, spec_arc) = resolver_single_spec(
5444 r#"spec test
5445data money: scale
5446 -> unit eur 1.00
5447 -> unit usd 1.19
5448
5449data money2: money
5450 -> unit eur 1.20
5451 -> unit usd 1.21
5452 -> unit gbp 1.30"#,
5453 );
5454
5455 let resolved = resolver
5456 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
5457 .expect("child constraints override inherited scale units");
5458 let money2 = resolved.named_types.get("money2").expect("money2");
5459
5460 match &money2.specifications {
5461 TypeSpecification::Scale { units, .. } => {
5462 assert_eq!(units.len(), 3);
5463 let eur = units.iter().find(|u| u.name == "eur").expect("eur");
5464 let usd = units.iter().find(|u| u.name == "usd").expect("usd");
5465 let gbp = units.iter().find(|u| u.name == "gbp").expect("gbp");
5466 assert_eq!(
5467 eur.value,
5468 Decimal::from_str_exact("1.20").expect("BUG: test literal decimal")
5469 );
5470 assert_eq!(
5471 usd.value,
5472 Decimal::from_str_exact("1.21").expect("BUG: test literal decimal")
5473 );
5474 assert_eq!(
5475 gbp.value,
5476 Decimal::from_str_exact("1.30").expect("BUG: test literal decimal")
5477 );
5478 }
5479 other => panic!("Expected Scale type specifications, got {:?}", other),
5480 }
5481 }
5482
5483 #[test]
5484 fn test_spec_level_unit_ambiguity_errors_are_reported() {
5485 let (resolver, spec_arc) = resolver_single_spec(
5486 r#"spec test
5487data money_a: scale
5488 -> unit eur 1.00
5489 -> unit usd 1.19
5490
5491data money_b: scale
5492 -> unit eur 1.00
5493 -> unit usd 1.20
5494
5495data length_a: scale
5496 -> unit meter 1.0
5497
5498data length_b: scale
5499 -> unit meter 1.0"#,
5500 );
5501
5502 let result = resolver.resolve_types_internal(&spec_arc, &EffectiveDate::Origin);
5503 assert!(
5504 result.is_err(),
5505 "Expected ambiguous unit definitions to error"
5506 );
5507
5508 let errs = result.unwrap_err();
5509 assert!(!errs.is_empty(), "expected at least one error");
5510 let error_msg = errs
5511 .iter()
5512 .map(ToString::to_string)
5513 .collect::<Vec<_>>()
5514 .join("; ");
5515 assert!(
5516 error_msg.contains("eur") || error_msg.contains("usd") || error_msg.contains("meter"),
5517 "Error should mention at least one ambiguous unit. Got: {}",
5518 error_msg
5519 );
5520 }
5521
5522 #[test]
5523 fn test_number_type_cannot_have_units() {
5524 let (resolver, spec_arc) = resolver_single_spec(
5525 r#"spec test
5526data price: number
5527 -> unit eur 1.00"#,
5528 );
5529
5530 let result = resolver.resolve_types_internal(&spec_arc, &EffectiveDate::Origin);
5531 assert!(result.is_err(), "Number types must reject unit commands");
5532
5533 let errs = result.unwrap_err();
5534 assert!(!errs.is_empty(), "expected at least one error");
5535 let error_msg = errs[0].to_string();
5536 assert!(
5537 error_msg.contains("unit") && error_msg.contains("number"),
5538 "Error should mention units are invalid on number. Got: {}",
5539 error_msg
5540 );
5541 }
5542
5543 #[test]
5544 fn test_extending_type_inherits_units() {
5545 let (resolver, spec_arc) = resolver_single_spec(
5546 r#"spec test
5547data money: scale
5548 -> unit eur 1.00
5549 -> unit usd 1.19
5550
5551data my_money: money
5552 -> unit gbp 1.30"#,
5553 );
5554
5555 let resolved = resolver
5556 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
5557 .unwrap();
5558 let my_money_type = resolved.named_types.get("my_money").unwrap();
5559
5560 match &my_money_type.specifications {
5561 TypeSpecification::Scale { units, .. } => {
5562 assert_eq!(units.len(), 3);
5563 assert!(units.iter().any(|u| u.name == "eur"));
5564 assert!(units.iter().any(|u| u.name == "usd"));
5565 assert!(units.iter().any(|u| u.name == "gbp"));
5566 }
5567 other => panic!("Expected Scale type specifications, got {:?}", other),
5568 }
5569 }
5570
5571 #[test]
5572 fn test_value_copy_scale_binding_can_override_unit_factor() {
5573 let (resolver, spec_arc) = resolver_single_spec(
5574 r#"spec test
5575data source_scale: scale
5576 -> unit usd 1.00
5577
5578data z: source_scale
5579 -> unit usd 1.19"#,
5580 );
5581
5582 let resolved = resolver
5583 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
5584 .expect("value-copy binding may refine inherited scale units");
5585 let z = resolved.named_types.get("z").expect("data z");
5586
5587 match &z.specifications {
5588 TypeSpecification::Scale { units, .. } => {
5589 let usd = units.iter().find(|u| u.name == "usd").expect("usd unit");
5590 assert_eq!(
5591 usd.value,
5592 Decimal::from_str_exact("1.19").expect("BUG: test literal decimal")
5593 );
5594 }
5595 other => panic!("Expected Scale type specifications, got {:?}", other),
5596 }
5597 }
5598
5599 #[test]
5600 fn test_duplicate_unit_in_same_type_last_factor_wins() {
5601 let (resolver, spec_arc) = resolver_single_spec(
5602 r#"spec test
5603data money: scale
5604 -> unit eur 1.00
5605 -> unit eur 1.19"#,
5606 );
5607
5608 let resolved = resolver
5609 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
5610 .expect("second unit constraint overrides conversion factor");
5611 let money = resolved.named_types.get("money").unwrap();
5612
5613 match &money.specifications {
5614 TypeSpecification::Scale { units, .. } => {
5615 assert_eq!(units.len(), 1);
5616 let eur = units.iter().find(|u| u.name == "eur").expect("eur unit");
5617 assert_eq!(
5618 eur.value,
5619 Decimal::from_str_exact("1.19").expect("BUG: test literal decimal")
5620 );
5621 }
5622 other => panic!("Expected Scale type specifications, got {:?}", other),
5623 }
5624 }
5625}
5626
5627pub fn validate_type_specifications(
5641 specs: &TypeSpecification,
5642 declared_default: Option<&ValueKind>,
5643 type_name: &str,
5644 source: &Source,
5645 spec_context: Option<Arc<LemmaSpec>>,
5646) -> Vec<Error> {
5647 let mut errors = Vec::new();
5648
5649 match specs {
5650 TypeSpecification::Scale {
5651 minimum,
5652 maximum,
5653 decimals,
5654 precision,
5655 units,
5656 ..
5657 } => {
5658 if let (Some(min), Some(max)) = (minimum, maximum) {
5660 if min > max {
5661 errors.push(Error::validation_with_context(
5662 format!(
5663 "Type '{}' has invalid range: minimum {} is greater than maximum {}",
5664 type_name, min, max
5665 ),
5666 Some(source.clone()),
5667 None::<String>,
5668 spec_context.clone(),
5669 None,
5670 ));
5671 }
5672 }
5673
5674 if let Some(d) = decimals {
5676 if *d > 28 {
5677 errors.push(Error::validation_with_context(
5678 format!(
5679 "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
5680 type_name, d
5681 ),
5682 Some(source.clone()),
5683 None::<String>,
5684 spec_context.clone(),
5685 None,
5686 ));
5687 }
5688 }
5689
5690 if let Some(prec) = precision {
5692 if *prec <= Decimal::ZERO {
5693 errors.push(Error::validation_with_context(
5694 format!(
5695 "Type '{}' has invalid precision: {}. Must be positive",
5696 type_name, prec
5697 ),
5698 Some(source.clone()),
5699 None::<String>,
5700 spec_context.clone(),
5701 None,
5702 ));
5703 }
5704 }
5705
5706 if let Some(ValueKind::Scale(def_value, def_unit)) = declared_default {
5707 if !units.iter().any(|u| u.name == *def_unit) {
5708 errors.push(Error::validation_with_context(
5709 format!(
5710 "Type '{}' default unit '{}' is not a valid unit. Valid units: {}",
5711 type_name,
5712 def_unit,
5713 units
5714 .iter()
5715 .map(|u| u.name.clone())
5716 .collect::<Vec<_>>()
5717 .join(", ")
5718 ),
5719 Some(source.clone()),
5720 None::<String>,
5721 spec_context.clone(),
5722 None,
5723 ));
5724 }
5725 if let Some(min) = minimum {
5726 if *def_value < *min {
5727 errors.push(Error::validation_with_context(
5728 format!(
5729 "Type '{}' default value {} {} is less than minimum {}",
5730 type_name, def_value, def_unit, min
5731 ),
5732 Some(source.clone()),
5733 None::<String>,
5734 spec_context.clone(),
5735 None,
5736 ));
5737 }
5738 }
5739 if let Some(max) = maximum {
5740 if *def_value > *max {
5741 errors.push(Error::validation_with_context(
5742 format!(
5743 "Type '{}' default value {} {} is greater than maximum {}",
5744 type_name, def_value, def_unit, max
5745 ),
5746 Some(source.clone()),
5747 None::<String>,
5748 spec_context.clone(),
5749 None,
5750 ));
5751 }
5752 }
5753 }
5754
5755 if units.is_empty() {
5757 errors.push(Error::validation_with_context(
5758 format!(
5759 "Type '{}' is a scale type but has no units. Scale types must define at least one unit (e.g. -> unit eur 1).",
5760 type_name
5761 ),
5762 Some(source.clone()),
5763 None::<String>,
5764 spec_context.clone(),
5765 None,
5766 ));
5767 }
5768
5769 if !units.is_empty() {
5771 let mut seen_names: Vec<String> = Vec::new();
5772 for unit in units.iter() {
5773 if unit.name.trim().is_empty() {
5775 errors.push(Error::validation_with_context(
5776 format!(
5777 "Type '{}' has a unit with empty name. Unit names cannot be empty.",
5778 type_name
5779 ),
5780 Some(source.clone()),
5781 None::<String>,
5782 spec_context.clone(),
5783 None,
5784 ));
5785 }
5786
5787 let lower_name = unit.name.to_lowercase();
5789 if seen_names
5790 .iter()
5791 .any(|seen| seen.to_lowercase() == lower_name)
5792 {
5793 errors.push(Error::validation_with_context(
5794 format!("Type '{}' has duplicate unit name '{}' (case-insensitive). Unit names must be unique within a type.", type_name, unit.name),
5795 Some(source.clone()),
5796 None::<String>,
5797 spec_context.clone(),
5798 None,
5799 ));
5800 } else {
5801 seen_names.push(unit.name.clone());
5802 }
5803
5804 if unit.value <= Decimal::ZERO {
5806 errors.push(Error::validation_with_context(
5807 format!("Type '{}' has unit '{}' with invalid value {}. Unit values must be positive (conversion factor relative to type base).", type_name, unit.name, unit.value),
5808 Some(source.clone()),
5809 None::<String>,
5810 spec_context.clone(),
5811 None,
5812 ));
5813 }
5814 }
5815 }
5816 }
5817 TypeSpecification::Number {
5818 minimum,
5819 maximum,
5820 decimals,
5821 precision,
5822 ..
5823 } => {
5824 if let (Some(min), Some(max)) = (minimum, maximum) {
5826 if min > max {
5827 errors.push(Error::validation_with_context(
5828 format!(
5829 "Type '{}' has invalid range: minimum {} is greater than maximum {}",
5830 type_name, min, max
5831 ),
5832 Some(source.clone()),
5833 None::<String>,
5834 spec_context.clone(),
5835 None,
5836 ));
5837 }
5838 }
5839
5840 if let Some(d) = decimals {
5842 if *d > 28 {
5843 errors.push(Error::validation_with_context(
5844 format!(
5845 "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
5846 type_name, d
5847 ),
5848 Some(source.clone()),
5849 None::<String>,
5850 spec_context.clone(),
5851 None,
5852 ));
5853 }
5854 }
5855
5856 if let Some(prec) = precision {
5858 if *prec <= Decimal::ZERO {
5859 errors.push(Error::validation_with_context(
5860 format!(
5861 "Type '{}' has invalid precision: {}. Must be positive",
5862 type_name, prec
5863 ),
5864 Some(source.clone()),
5865 None::<String>,
5866 spec_context.clone(),
5867 None,
5868 ));
5869 }
5870 }
5871
5872 if let Some(ValueKind::Number(def)) = declared_default {
5873 if let Some(min) = minimum {
5874 if *def < *min {
5875 errors.push(Error::validation_with_context(
5876 format!(
5877 "Type '{}' default value {} is less than minimum {}",
5878 type_name, def, min
5879 ),
5880 Some(source.clone()),
5881 None::<String>,
5882 spec_context.clone(),
5883 None,
5884 ));
5885 }
5886 }
5887 if let Some(max) = maximum {
5888 if *def > *max {
5889 errors.push(Error::validation_with_context(
5890 format!(
5891 "Type '{}' default value {} is greater than maximum {}",
5892 type_name, def, max
5893 ),
5894 Some(source.clone()),
5895 None::<String>,
5896 spec_context.clone(),
5897 None,
5898 ));
5899 }
5900 }
5901 }
5902 }
5904
5905 TypeSpecification::Ratio {
5906 minimum,
5907 maximum,
5908 decimals,
5909 units,
5910 ..
5911 } => {
5912 if let Some(d) = decimals {
5914 if *d > 28 {
5915 errors.push(Error::validation_with_context(
5916 format!(
5917 "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
5918 type_name, d
5919 ),
5920 Some(source.clone()),
5921 None::<String>,
5922 spec_context.clone(),
5923 None,
5924 ));
5925 }
5926 }
5927
5928 if let (Some(min), Some(max)) = (minimum, maximum) {
5930 if min > max {
5931 errors.push(Error::validation_with_context(
5932 format!(
5933 "Type '{}' has invalid range: minimum {} is greater than maximum {}",
5934 type_name, min, max
5935 ),
5936 Some(source.clone()),
5937 None::<String>,
5938 spec_context.clone(),
5939 None,
5940 ));
5941 }
5942 }
5943
5944 if let Some(ValueKind::Ratio(def, _)) = declared_default {
5945 if let Some(min) = minimum {
5946 if *def < *min {
5947 errors.push(Error::validation_with_context(
5948 format!(
5949 "Type '{}' default value {} is less than minimum {}",
5950 type_name, def, min
5951 ),
5952 Some(source.clone()),
5953 None::<String>,
5954 spec_context.clone(),
5955 None,
5956 ));
5957 }
5958 }
5959 if let Some(max) = maximum {
5960 if *def > *max {
5961 errors.push(Error::validation_with_context(
5962 format!(
5963 "Type '{}' default value {} is greater than maximum {}",
5964 type_name, def, max
5965 ),
5966 Some(source.clone()),
5967 None::<String>,
5968 spec_context.clone(),
5969 None,
5970 ));
5971 }
5972 }
5973 }
5974
5975 if !units.is_empty() {
5979 let mut seen_names: Vec<String> = Vec::new();
5980 for unit in units.iter() {
5981 if unit.name.trim().is_empty() {
5983 errors.push(Error::validation_with_context(
5984 format!(
5985 "Type '{}' has a unit with empty name. Unit names cannot be empty.",
5986 type_name
5987 ),
5988 Some(source.clone()),
5989 None::<String>,
5990 spec_context.clone(),
5991 None,
5992 ));
5993 }
5994
5995 let lower_name = unit.name.to_lowercase();
5997 if seen_names
5998 .iter()
5999 .any(|seen| seen.to_lowercase() == lower_name)
6000 {
6001 errors.push(Error::validation_with_context(
6002 format!("Type '{}' has duplicate unit name '{}' (case-insensitive). Unit names must be unique within a type.", type_name, unit.name),
6003 Some(source.clone()),
6004 None::<String>,
6005 spec_context.clone(),
6006 None,
6007 ));
6008 } else {
6009 seen_names.push(unit.name.clone());
6010 }
6011
6012 if unit.value <= Decimal::ZERO {
6014 errors.push(Error::validation_with_context(
6015 format!("Type '{}' has unit '{}' with invalid value {}. Unit values must be positive (conversion factor relative to type base).", type_name, unit.name, unit.value),
6016 Some(source.clone()),
6017 None::<String>,
6018 spec_context.clone(),
6019 None,
6020 ));
6021 }
6022 }
6023 }
6024 }
6025
6026 TypeSpecification::Text {
6027 length, options, ..
6028 } => {
6029 if let Some(ValueKind::Text(def)) = declared_default {
6030 let def_len = def.len();
6031
6032 if let Some(len) = length {
6033 if def_len != *len {
6034 errors.push(Error::validation_with_context(
6035 format!("Type '{}' default value length {} does not match required length {}", type_name, def_len, len),
6036 Some(source.clone()),
6037 None::<String>,
6038 spec_context.clone(),
6039 None,
6040 ));
6041 }
6042 }
6043 if !options.is_empty() && !options.contains(def) {
6044 errors.push(Error::validation_with_context(
6045 format!(
6046 "Type '{}' default value '{}' is not in allowed options: {:?}",
6047 type_name, def, options
6048 ),
6049 Some(source.clone()),
6050 None::<String>,
6051 spec_context.clone(),
6052 None,
6053 ));
6054 }
6055 }
6056 }
6057
6058 TypeSpecification::Date {
6059 minimum,
6060 maximum,
6061 ..
6062 } => {
6063 if let (Some(min), Some(max)) = (minimum, maximum) {
6065 let min_sem = semantics::date_time_to_semantic(min);
6066 let max_sem = semantics::date_time_to_semantic(max);
6067 if semantics::compare_semantic_dates(&min_sem, &max_sem) == Ordering::Greater {
6068 errors.push(Error::validation_with_context(
6069 format!(
6070 "Type '{}' has invalid date range: minimum {} is after maximum {}",
6071 type_name, min, max
6072 ),
6073 Some(source.clone()),
6074 None::<String>,
6075 spec_context.clone(),
6076 None,
6077 ));
6078 }
6079 }
6080
6081 if let Some(ValueKind::Date(def)) = declared_default {
6082 if let Some(min) = minimum {
6083 let min_sem = semantics::date_time_to_semantic(min);
6084 if semantics::compare_semantic_dates(def, &min_sem) == Ordering::Less {
6085 errors.push(Error::validation_with_context(
6086 format!(
6087 "Type '{}' default date {} is before minimum {}",
6088 type_name, def, min
6089 ),
6090 Some(source.clone()),
6091 None::<String>,
6092 spec_context.clone(),
6093 None,
6094 ));
6095 }
6096 }
6097 if let Some(max) = maximum {
6098 let max_sem = semantics::date_time_to_semantic(max);
6099 if semantics::compare_semantic_dates(def, &max_sem) == Ordering::Greater {
6100 errors.push(Error::validation_with_context(
6101 format!(
6102 "Type '{}' default date {} is after maximum {}",
6103 type_name, def, max
6104 ),
6105 Some(source.clone()),
6106 None::<String>,
6107 spec_context.clone(),
6108 None,
6109 ));
6110 }
6111 }
6112 }
6113 }
6114
6115 TypeSpecification::Time {
6116 minimum,
6117 maximum,
6118 ..
6119 } => {
6120 if let (Some(min), Some(max)) = (minimum, maximum) {
6122 let min_sem = semantics::time_to_semantic(min);
6123 let max_sem = semantics::time_to_semantic(max);
6124 if semantics::compare_semantic_times(&min_sem, &max_sem) == Ordering::Greater {
6125 errors.push(Error::validation_with_context(
6126 format!(
6127 "Type '{}' has invalid time range: minimum {} is after maximum {}",
6128 type_name, min, max
6129 ),
6130 Some(source.clone()),
6131 None::<String>,
6132 spec_context.clone(),
6133 None,
6134 ));
6135 }
6136 }
6137
6138 if let Some(ValueKind::Time(def)) = declared_default {
6139 if let Some(min) = minimum {
6140 let min_sem = semantics::time_to_semantic(min);
6141 if semantics::compare_semantic_times(def, &min_sem) == Ordering::Less {
6142 errors.push(Error::validation_with_context(
6143 format!(
6144 "Type '{}' default time {} is before minimum {}",
6145 type_name, def, min
6146 ),
6147 Some(source.clone()),
6148 None::<String>,
6149 spec_context.clone(),
6150 None,
6151 ));
6152 }
6153 }
6154 if let Some(max) = maximum {
6155 let max_sem = semantics::time_to_semantic(max);
6156 if semantics::compare_semantic_times(def, &max_sem) == Ordering::Greater {
6157 errors.push(Error::validation_with_context(
6158 format!(
6159 "Type '{}' default time {} is after maximum {}",
6160 type_name, def, max
6161 ),
6162 Some(source.clone()),
6163 None::<String>,
6164 spec_context.clone(),
6165 None,
6166 ));
6167 }
6168 }
6169 }
6170 }
6171
6172 TypeSpecification::Boolean { .. } | TypeSpecification::Duration { .. } => {
6173 }
6175 TypeSpecification::Veto { .. } => {
6176 }
6179 TypeSpecification::Undetermined => unreachable!(
6180 "BUG: validate_type_specification_constraints called with Undetermined sentinel type; this type exists only during type inference"
6181 ),
6182 }
6183
6184 errors
6185}
6186
6187#[cfg(test)]
6188mod validation_tests {
6189 use super::*;
6190 use crate::parsing::ast::{CommandArg, TypeConstraintCommand};
6191 use crate::planning::semantics::TypeSpecification;
6192 use rust_decimal::Decimal;
6193
6194 fn test_source() -> Source {
6195 Source::new(
6196 crate::parsing::source::SourceType::Volatile,
6197 crate::parsing::ast::Span {
6198 start: 0,
6199 end: 0,
6200 line: 1,
6201 col: 0,
6202 },
6203 )
6204 }
6205
6206 fn apply(
6207 specs: TypeSpecification,
6208 command: TypeConstraintCommand,
6209 args: &[CommandArg],
6210 ) -> TypeSpecification {
6211 let mut default = None;
6212 specs.apply_constraint(command, args, &mut default).unwrap()
6213 }
6214
6215 fn number_arg(n: i64) -> CommandArg {
6216 CommandArg::Literal(crate::literals::Value::Number(Decimal::from(n)))
6217 }
6218
6219 fn date_arg(s: &str) -> CommandArg {
6220 let dt = s.parse::<crate::literals::DateTimeValue>().expect("date");
6221 CommandArg::Literal(crate::literals::Value::Date(dt))
6222 }
6223
6224 fn time_arg(s: &str) -> CommandArg {
6225 let t = s.parse::<crate::literals::TimeValue>().expect("time");
6226 CommandArg::Literal(crate::literals::Value::Time(t))
6227 }
6228
6229 #[test]
6230 fn validate_number_minimum_greater_than_maximum() {
6231 let mut specs = TypeSpecification::number();
6232 specs = apply(specs, TypeConstraintCommand::Minimum, &[number_arg(100)]);
6233 specs = apply(specs, TypeConstraintCommand::Maximum, &[number_arg(50)]);
6234
6235 let src = test_source();
6236 let errors = validate_type_specifications(&specs, None, "test", &src, None);
6237 assert_eq!(errors.len(), 1);
6238 assert!(errors[0]
6239 .to_string()
6240 .contains("minimum 100 is greater than maximum 50"));
6241 }
6242
6243 #[test]
6244 fn validate_number_default_below_minimum() {
6245 let specs = TypeSpecification::Number {
6246 minimum: Some(Decimal::from(10)),
6247 maximum: None,
6248 decimals: None,
6249 precision: None,
6250 help: String::new(),
6251 };
6252 let default = ValueKind::Number(Decimal::from(5));
6253
6254 let src = test_source();
6255 let errors = validate_type_specifications(&specs, Some(&default), "test", &src, None);
6256 assert_eq!(errors.len(), 1);
6257 assert!(errors[0]
6258 .to_string()
6259 .contains("default value 5 is less than minimum 10"));
6260 }
6261
6262 #[test]
6263 fn validate_number_default_above_maximum() {
6264 let specs = TypeSpecification::Number {
6265 minimum: None,
6266 maximum: Some(Decimal::from(100)),
6267 decimals: None,
6268 precision: None,
6269 help: String::new(),
6270 };
6271 let default = ValueKind::Number(Decimal::from(150));
6272
6273 let src = test_source();
6274 let errors = validate_type_specifications(&specs, Some(&default), "test", &src, None);
6275 assert_eq!(errors.len(), 1);
6276 assert!(errors[0]
6277 .to_string()
6278 .contains("default value 150 is greater than maximum 100"));
6279 }
6280
6281 #[test]
6282 fn validate_number_default_valid() {
6283 let specs = TypeSpecification::Number {
6284 minimum: Some(Decimal::from(0)),
6285 maximum: Some(Decimal::from(100)),
6286 decimals: None,
6287 precision: None,
6288 help: String::new(),
6289 };
6290 let default = ValueKind::Number(Decimal::from(50));
6291
6292 let src = test_source();
6293 let errors = validate_type_specifications(&specs, Some(&default), "test", &src, None);
6294 assert!(errors.is_empty());
6295 }
6296
6297 #[test]
6298 fn text_minimum_command_is_rejected() {
6299 let specs = TypeSpecification::text();
6300 let res =
6301 specs.apply_constraint(TypeConstraintCommand::Minimum, &[number_arg(5)], &mut None);
6302 assert!(res.is_err());
6303 assert!(res
6304 .unwrap_err()
6305 .contains("Invalid command 'minimum' for text type"));
6306 }
6307
6308 #[test]
6309 fn text_maximum_command_is_rejected() {
6310 let specs = TypeSpecification::text();
6311 let res =
6312 specs.apply_constraint(TypeConstraintCommand::Maximum, &[number_arg(5)], &mut None);
6313 assert!(res.is_err());
6314 assert!(res
6315 .unwrap_err()
6316 .contains("Invalid command 'maximum' for text type"));
6317 }
6318
6319 #[test]
6320 fn validate_text_default_not_in_options() {
6321 let specs = TypeSpecification::Text {
6322 length: None,
6323 options: vec!["red".to_string(), "blue".to_string()],
6324 help: String::new(),
6325 };
6326 let default = ValueKind::Text("green".to_string());
6327
6328 let src = test_source();
6329 let errors = validate_type_specifications(&specs, Some(&default), "test", &src, None);
6330 assert_eq!(errors.len(), 1);
6331 assert!(errors[0]
6332 .to_string()
6333 .contains("default value 'green' is not in allowed options"));
6334 }
6335
6336 #[test]
6337 fn validate_ratio_minimum_greater_than_maximum() {
6338 let specs = TypeSpecification::Ratio {
6339 minimum: Some(Decimal::from(2)),
6340 maximum: Some(Decimal::from(1)),
6341 decimals: None,
6342 units: crate::planning::semantics::RatioUnits::new(),
6343 help: String::new(),
6344 };
6345
6346 let src = test_source();
6347 let errors = validate_type_specifications(&specs, None, "test", &src, None);
6348 assert_eq!(errors.len(), 1);
6349 assert!(errors[0]
6350 .to_string()
6351 .contains("minimum 2 is greater than maximum 1"));
6352 }
6353
6354 #[test]
6355 fn validate_date_minimum_after_maximum() {
6356 let mut specs = TypeSpecification::date();
6357 specs = apply(
6358 specs,
6359 TypeConstraintCommand::Minimum,
6360 &[date_arg("2024-12-31")],
6361 );
6362 specs = apply(
6363 specs,
6364 TypeConstraintCommand::Maximum,
6365 &[date_arg("2024-01-01")],
6366 );
6367
6368 let src = test_source();
6369 let errors = validate_type_specifications(&specs, None, "test", &src, None);
6370 assert_eq!(errors.len(), 1);
6371 assert!(
6372 errors[0].to_string().contains("minimum")
6373 && errors[0].to_string().contains("is after maximum")
6374 );
6375 }
6376
6377 #[test]
6378 fn validate_date_valid_range() {
6379 let mut specs = TypeSpecification::date();
6380 specs = apply(
6381 specs,
6382 TypeConstraintCommand::Minimum,
6383 &[date_arg("2024-01-01")],
6384 );
6385 specs = apply(
6386 specs,
6387 TypeConstraintCommand::Maximum,
6388 &[date_arg("2024-12-31")],
6389 );
6390
6391 let src = test_source();
6392 let errors = validate_type_specifications(&specs, None, "test", &src, None);
6393 assert!(errors.is_empty());
6394 }
6395
6396 #[test]
6397 fn validate_time_minimum_after_maximum() {
6398 let mut specs = TypeSpecification::time();
6399 specs = apply(
6400 specs,
6401 TypeConstraintCommand::Minimum,
6402 &[time_arg("23:00:00")],
6403 );
6404 specs = apply(
6405 specs,
6406 TypeConstraintCommand::Maximum,
6407 &[time_arg("10:00:00")],
6408 );
6409
6410 let src = test_source();
6411 let errors = validate_type_specifications(&specs, None, "test", &src, None);
6412 assert_eq!(errors.len(), 1);
6413 assert!(
6414 errors[0].to_string().contains("minimum")
6415 && errors[0].to_string().contains("is after maximum")
6416 );
6417 }
6418}