1use crate::engine::Context;
2use crate::parsing::ast::{
3 self as ast, CalendarUnit, CommandArg, Constraint, EffectiveDate, FillRhs, LemmaData,
4 LemmaRepository, LemmaRule, LemmaSpec, MetaValue, ParentType, PrimitiveKind,
5 TypeConstraintCommand, Value,
6};
7use crate::parsing::source::Source;
8use crate::planning::discovery;
9use crate::planning::semantics::{
10 self, calendar_decomposition, combine_decompositions, conversion_target_to_semantic,
11 duration_decomposition, number_with_unit_to_value_kind, parser_value_to_value_kind,
12 primitive_boolean, primitive_calendar, primitive_calendar_range, primitive_date,
13 primitive_date_range, primitive_number, primitive_number_range, primitive_text, primitive_time,
14 value_to_semantic, ArithmeticComputation, BaseQuantityVector, ComparisonComputation,
15 DataDefinition, DataPath, Expression, ExpressionKind, LemmaType, LiteralValue, PathSegment,
16 ReferenceTarget, RulePath, SemanticConversionTarget, TypeDefiningSpec, TypeExtends,
17 TypeSpecification, ValueKind,
18};
19use crate::Error;
20use ast::DataValue as ParsedDataValue;
21use indexmap::IndexMap;
22use std::cmp::Ordering;
23use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
24use std::fmt;
25use std::sync::Arc;
26
27type DataBindings = HashMap<Vec<String>, (BindingValue, Source)>;
36
37#[derive(Debug, Clone)]
46pub(crate) enum BindingValue {
47 Literal(ast::Value),
49 Reference {
51 target: ReferenceTarget,
52 constraints: Option<Vec<Constraint>>,
53 },
54}
55
56#[derive(Debug)]
57pub(crate) struct Graph {
58 main_spec: Arc<LemmaSpec>,
60 data: IndexMap<DataPath, DataDefinition>,
61 rules: BTreeMap<RulePath, RuleNode>,
62 execution_order: Vec<RulePath>,
63 reference_evaluation_order: Vec<DataPath>,
68}
69
70impl Graph {
71 pub(crate) fn data(&self) -> &IndexMap<DataPath, DataDefinition> {
72 &self.data
73 }
74
75 pub(crate) fn rules(&self) -> &BTreeMap<RulePath, RuleNode> {
76 &self.rules
77 }
78
79 pub(crate) fn rules_mut(&mut self) -> &mut BTreeMap<RulePath, RuleNode> {
80 &mut self.rules
81 }
82
83 pub(crate) fn execution_order(&self) -> &[RulePath] {
84 &self.execution_order
85 }
86
87 pub(crate) fn reference_evaluation_order(&self) -> &[DataPath] {
88 &self.reference_evaluation_order
89 }
90
91 pub(crate) fn main_spec(&self) -> &Arc<LemmaSpec> {
92 &self.main_spec
93 }
94
95 pub(crate) fn build_data(&self) -> IndexMap<DataPath, DataDefinition> {
98 struct PendingReference {
99 target: ReferenceTarget,
100 resolved_type: LemmaType,
101 local_constraints: Option<Vec<Constraint>>,
102 local_default: Option<ValueKind>,
103 }
104
105 let mut schema: HashMap<DataPath, LemmaType> = HashMap::new();
106 let mut declared_defaults: HashMap<DataPath, ValueKind> = HashMap::new();
107 let mut values: HashMap<DataPath, LiteralValue> = HashMap::new();
108 let mut spec_arcs: HashMap<DataPath, Arc<LemmaSpec>> = HashMap::new();
109 let mut references: HashMap<DataPath, PendingReference> = HashMap::new();
110
111 for (path, rfv) in self.data.iter() {
112 match rfv {
113 DataDefinition::Value { value, .. } => {
114 values.insert(path.clone(), value.clone());
115 schema.insert(path.clone(), value.lemma_type.clone());
116 }
117 DataDefinition::TypeDeclaration {
118 resolved_type,
119 declared_default,
120 ..
121 } => {
122 schema.insert(path.clone(), resolved_type.clone());
123 if let Some(dv) = declared_default {
124 declared_defaults.insert(path.clone(), dv.clone());
125 }
126 }
127 DataDefinition::Import { spec: spec_arc, .. } => {
128 spec_arcs.insert(path.clone(), Arc::clone(spec_arc));
129 }
130 DataDefinition::Reference {
131 target,
132 resolved_type,
133 local_constraints,
134 local_default,
135 ..
136 } => {
137 schema.insert(path.clone(), resolved_type.clone());
138 references.insert(
139 path.clone(),
140 PendingReference {
141 target: target.clone(),
142 resolved_type: resolved_type.clone(),
143 local_constraints: local_constraints.clone(),
144 local_default: local_default.clone(),
145 },
146 );
147 }
148 }
149 }
150
151 for (path, value) in values.iter_mut() {
152 let Some(schema_type) = schema.get(path).cloned() else {
153 continue;
154 };
155 match Self::coerce_literal_to_schema_type(value, &schema_type) {
156 Ok(coerced) => *value = coerced,
157 Err(msg) => unreachable!("Data {} incompatible: {}", path, msg),
158 }
159 }
160
161 let mut data = IndexMap::new();
162 for (path, rfv) in &self.data {
163 let source = rfv.source().clone();
164 if let Some(spec_arc) = spec_arcs.remove(path) {
165 data.insert(
166 path.clone(),
167 DataDefinition::Import {
168 spec: spec_arc,
169 source,
170 },
171 );
172 } else if let Some(pending) = references.remove(path) {
173 data.insert(
174 path.clone(),
175 DataDefinition::Reference {
176 target: pending.target,
177 resolved_type: pending.resolved_type,
178 local_constraints: pending.local_constraints,
179 local_default: pending.local_default,
180 source,
181 },
182 );
183 } else if let Some(value) = values.remove(path) {
184 data.insert(path.clone(), DataDefinition::Value { value, source });
185 } else {
186 let resolved_type = schema
187 .get(path)
188 .cloned()
189 .expect("non-spec-ref data has schema (value, reference, or type-only)");
190 let declared_default = declared_defaults.remove(path);
191 data.insert(
192 path.clone(),
193 DataDefinition::TypeDeclaration {
194 resolved_type,
195 declared_default,
196 source,
197 },
198 );
199 }
200 }
201 data
202 }
203
204 pub(crate) fn coerce_literal_to_schema_type(
205 lit: &LiteralValue,
206 schema_type: &LemmaType,
207 ) -> Result<LiteralValue, String> {
208 fn range_endpoint_schema_type(schema_type: &LemmaType) -> Option<LemmaType> {
209 match &schema_type.specifications {
210 TypeSpecification::NumberRange { .. } => {
211 Some(LemmaType::primitive(TypeSpecification::number()))
212 }
213 TypeSpecification::DateRange { .. } => {
214 Some(LemmaType::primitive(TypeSpecification::date()))
215 }
216 TypeSpecification::RatioRange { units, .. } => {
217 Some(LemmaType::primitive(TypeSpecification::Ratio {
218 minimum: None,
219 maximum: None,
220 decimals: None,
221 units: units.clone(),
222 help: String::new(),
223 }))
224 }
225 TypeSpecification::CalendarRange { .. } => {
226 Some(LemmaType::primitive(TypeSpecification::calendar()))
227 }
228 TypeSpecification::QuantityRange {
229 units,
230 decomposition,
231 canonical_unit,
232 ..
233 } => Some(LemmaType::primitive(TypeSpecification::Quantity {
234 minimum: None,
235 maximum: None,
236 decimals: None,
237 units: units.clone(),
238 traits: Vec::new(),
239 decomposition: decomposition.clone(),
240 canonical_unit: canonical_unit.clone(),
241 help: String::new(),
242 })),
243 _ => None,
244 }
245 }
246
247 if lit.lemma_type.specifications == schema_type.specifications {
248 let mut out = lit.clone();
249 out.lemma_type = schema_type.clone();
250 return Ok(out);
251 }
252 match (&schema_type.specifications, &lit.value) {
253 (TypeSpecification::Number { .. }, ValueKind::Number(_))
254 | (TypeSpecification::Text { .. }, ValueKind::Text(_))
255 | (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
256 | (TypeSpecification::Date { .. }, ValueKind::Date(_))
257 | (TypeSpecification::Time { .. }, ValueKind::Time(_))
258 | (TypeSpecification::Calendar { .. }, ValueKind::Calendar(_, _)) => {
259 let mut out = lit.clone();
260 out.lemma_type = schema_type.clone();
261 Ok(out)
262 }
263 (TypeSpecification::Quantity { units, .. }, ValueKind::Quantity(_, unit_name, _)) => {
264 if !units.iter().any(|u| u.name == *unit_name) {
265 return Err(format!(
266 "value {} cannot be used as type {}: unknown unit '{}'",
267 lit,
268 schema_type.name(),
269 unit_name
270 ));
271 }
272 let mut out = lit.clone();
273 out.lemma_type = schema_type.clone();
274 Ok(out)
275 }
276 (TypeSpecification::Ratio { units, .. }, ValueKind::Ratio(_, unit_name)) => {
277 if let Some(unit_name) = unit_name {
278 if !units.iter().any(|u| u.name == *unit_name) {
279 return Err(format!(
280 "value {} cannot be used as type {}: unknown unit '{}'",
281 lit,
282 schema_type.name(),
283 unit_name
284 ));
285 }
286 }
287 let mut out = lit.clone();
288 out.lemma_type = schema_type.clone();
289 Ok(out)
290 }
291 (
292 TypeSpecification::NumberRange { .. }
293 | TypeSpecification::DateRange { .. }
294 | TypeSpecification::RatioRange { .. }
295 | TypeSpecification::CalendarRange { .. }
296 | TypeSpecification::QuantityRange { .. },
297 ValueKind::Range(left, right),
298 ) => {
299 let endpoint_schema_type =
300 range_endpoint_schema_type(schema_type).unwrap_or_else(|| {
301 unreachable!("BUG: range_endpoint_schema_type missing range schema arm")
302 });
303 let coerced_left =
304 Self::coerce_literal_to_schema_type(left.as_ref(), &endpoint_schema_type)?;
305 let coerced_right =
306 Self::coerce_literal_to_schema_type(right.as_ref(), &endpoint_schema_type)?;
307 Ok(LiteralValue {
308 value: ValueKind::Range(Box::new(coerced_left), Box::new(coerced_right)),
309 lemma_type: schema_type.clone(),
310 })
311 }
312 (TypeSpecification::Ratio { .. }, ValueKind::Number(n)) => {
313 Ok(LiteralValue::ratio_with_type(*n, None, schema_type.clone()))
314 }
315 _ => Err(format!(
316 "value {} cannot be used as type {}",
317 lit,
318 schema_type.name()
319 )),
320 }
321 }
322
323 fn resolve_data_reference_types(&mut self) -> Result<(), Vec<Error>> {
335 let mut errors: Vec<Error> = Vec::new();
336 let mut updates: Vec<(DataPath, LemmaType, Option<ValueKind>)> = Vec::new();
337
338 for (reference_path, entry) in &self.data {
339 let DataDefinition::Reference {
340 target,
341 resolved_type: provisional,
342 local_constraints,
343 source,
344 ..
345 } = entry
346 else {
347 continue;
348 };
349
350 let target_data_path = match target {
351 ReferenceTarget::Data(path) => path,
352 ReferenceTarget::Rule(_) => continue,
353 };
354
355 let Some(target_entry) = self.data.get(target_data_path) else {
356 errors.push(reference_error(
357 &self.main_spec,
358 source,
359 format!(
360 "Data reference '{}' target '{}' does not exist",
361 reference_path, target_data_path
362 ),
363 ));
364 continue;
365 };
366
367 let Some(target_type) = target_entry.schema_type().cloned() else {
368 errors.push(reference_error(
369 &self.main_spec,
370 source,
371 format!(
372 "Data reference '{}' target '{}' is a spec reference and cannot carry a value",
373 reference_path, target_data_path
374 ),
375 ));
376 continue;
377 };
378
379 let lhs_declared_type: Option<&LemmaType> = if provisional.is_undetermined() {
380 None
381 } else {
382 Some(provisional)
383 };
384
385 if let Some(lhs) = lhs_declared_type {
386 if let Some(msg) = reference_kind_mismatch_message(
387 lhs,
388 &target_type,
389 reference_path,
390 target_data_path,
391 "target",
392 ) {
393 errors.push(reference_error(&self.main_spec, source, msg));
394 continue;
395 }
396 }
397
398 let mut merged = match lhs_declared_type {
403 Some(lhs) => lhs.clone(),
404 None => target_type.clone(),
405 };
406 let mut captured_default: Option<ValueKind> = None;
407 if let Some(constraints) = local_constraints {
408 let constraint_type_name = merged.name();
409 match apply_constraints_to_spec(
410 &self.main_spec,
411 &constraint_type_name,
412 merged.specifications.clone(),
413 constraints,
414 source,
415 &mut captured_default,
416 ) {
417 Ok(specs) => merged.specifications = specs,
418 Err(errs) => {
419 errors.extend(errs);
420 continue;
421 }
422 }
423 }
424
425 updates.push((reference_path.clone(), merged, captured_default));
426 }
427
428 for (path, new_type, new_default) in updates {
429 if let Some(DataDefinition::Reference {
430 resolved_type,
431 local_default,
432 ..
433 }) = self.data.get_mut(&path)
434 {
435 *resolved_type = new_type;
436 if new_default.is_some() {
437 *local_default = new_default;
438 }
439 } else {
440 unreachable!("BUG: reference path disappeared between collect and update phases");
441 }
442 }
443
444 if errors.is_empty() {
445 Ok(())
446 } else {
447 Err(errors)
448 }
449 }
450
451 fn resolve_rule_reference_types(
460 &mut self,
461 computed_rule_types: &HashMap<RulePath, LemmaType>,
462 ) -> Result<(), Vec<Error>> {
463 let mut errors: Vec<Error> = Vec::new();
464 let mut updates: Vec<(DataPath, LemmaType, Option<ValueKind>)> = Vec::new();
465
466 for (reference_path, entry) in &self.data {
467 let DataDefinition::Reference {
468 target,
469 resolved_type: provisional,
470 local_constraints,
471 source,
472 ..
473 } = entry
474 else {
475 continue;
476 };
477
478 let target_rule_path = match target {
479 ReferenceTarget::Rule(path) => path,
480 ReferenceTarget::Data(_) => continue,
481 };
482
483 let Some(target_type) = computed_rule_types.get(target_rule_path) else {
484 errors.push(reference_error(
485 &self.main_spec,
486 source,
487 format!(
488 "Data reference '{}' target rule '{}' does not exist",
489 reference_path, target_rule_path
490 ),
491 ));
492 continue;
493 };
494
495 if target_type.vetoed() || target_type.is_undetermined() {
500 let mut merged = target_type.clone();
501 let mut captured_default: Option<ValueKind> = None;
502 if let Some(constraints) = local_constraints {
503 let constraint_type_name = merged.name();
504 match apply_constraints_to_spec(
505 &self.main_spec,
506 &constraint_type_name,
507 merged.specifications.clone(),
508 constraints,
509 source,
510 &mut captured_default,
511 ) {
512 Ok(specs) => merged.specifications = specs,
513 Err(errs) => {
514 errors.extend(errs);
515 continue;
516 }
517 }
518 }
519 updates.push((reference_path.clone(), merged, captured_default));
520 continue;
521 }
522
523 let lhs_declared_type: Option<&LemmaType> = if provisional.is_undetermined() {
524 None
525 } else {
526 Some(provisional)
527 };
528
529 if let Some(lhs) = lhs_declared_type {
530 if let Some(msg) = reference_kind_mismatch_message(
531 lhs,
532 target_type,
533 reference_path,
534 target_rule_path,
535 "target rule",
536 ) {
537 errors.push(reference_error(&self.main_spec, source, msg));
538 continue;
539 }
540 }
541
542 let mut merged = match lhs_declared_type {
545 Some(lhs) => lhs.clone(),
546 None => target_type.clone(),
547 };
548 let mut captured_default: Option<ValueKind> = None;
549 if let Some(constraints) = local_constraints {
550 let constraint_type_name = merged.name();
551 match apply_constraints_to_spec(
552 &self.main_spec,
553 &constraint_type_name,
554 merged.specifications.clone(),
555 constraints,
556 source,
557 &mut captured_default,
558 ) {
559 Ok(specs) => merged.specifications = specs,
560 Err(errs) => {
561 errors.extend(errs);
562 continue;
563 }
564 }
565 }
566
567 updates.push((reference_path.clone(), merged, captured_default));
568 }
569
570 for (path, new_type, new_default) in updates {
571 if let Some(DataDefinition::Reference {
572 resolved_type,
573 local_default,
574 ..
575 }) = self.data.get_mut(&path)
576 {
577 *resolved_type = new_type;
578 if new_default.is_some() {
579 *local_default = new_default;
580 }
581 } else {
582 unreachable!(
583 "BUG: rule-target reference path disappeared between collect and update phases"
584 );
585 }
586 }
587
588 if errors.is_empty() {
589 Ok(())
590 } else {
591 Err(errors)
592 }
593 }
594
595 fn add_rule_reference_dependency_edges(&mut self) {
605 let reference_to_rule: HashMap<DataPath, RulePath> =
606 self.transitive_reference_to_rule_map();
607
608 if reference_to_rule.is_empty() {
609 return;
610 }
611
612 let mut updates: Vec<(RulePath, RulePath)> = Vec::new();
613 for (rule_path, rule_node) in &self.rules {
614 let mut found: BTreeSet<RulePath> = BTreeSet::new();
615 for (cond, result) in &rule_node.branches {
616 if let Some(c) = cond {
617 collect_rule_reference_dependencies(c, &reference_to_rule, &mut found);
618 }
619 collect_rule_reference_dependencies(result, &reference_to_rule, &mut found);
620 }
621 for target in found {
622 updates.push((rule_path.clone(), target));
623 }
624 }
625
626 for (rule_path, target) in updates {
627 if let Some(node) = self.rules.get_mut(&rule_path) {
628 node.depends_on_rules.insert(target);
629 }
630 }
631 }
632
633 fn transitive_reference_to_rule_map(&self) -> HashMap<DataPath, RulePath> {
640 let mut out: HashMap<DataPath, RulePath> = HashMap::new();
641 for (path, def) in &self.data {
642 if !matches!(def, DataDefinition::Reference { .. }) {
643 continue;
644 }
645 let mut visited: HashSet<DataPath> = HashSet::new();
646 let mut cursor: DataPath = path.clone();
647 loop {
648 if !visited.insert(cursor.clone()) {
649 break;
650 }
651 let Some(DataDefinition::Reference { target, .. }) = self.data.get(&cursor) else {
652 break;
653 };
654 match target {
655 ReferenceTarget::Data(next) => cursor = next.clone(),
656 ReferenceTarget::Rule(rule_path) => {
657 out.insert(path.clone(), rule_path.clone());
658 break;
659 }
660 }
661 }
662 }
663 out
664 }
665
666 fn compute_reference_evaluation_order(&self) -> Result<Vec<DataPath>, Vec<Error>> {
673 let reference_paths: Vec<DataPath> = self
674 .data
675 .iter()
676 .filter_map(|(p, d)| match d {
677 DataDefinition::Reference {
678 target: ReferenceTarget::Data(_),
679 ..
680 } => Some(p.clone()),
681 _ => None,
682 })
683 .collect();
684
685 if reference_paths.is_empty() {
686 return Ok(Vec::new());
687 }
688
689 let reference_set: BTreeSet<DataPath> = reference_paths.iter().cloned().collect();
690 let mut in_degree: BTreeMap<DataPath, usize> = BTreeMap::new();
691 let mut dependents: BTreeMap<DataPath, Vec<DataPath>> = BTreeMap::new();
692 for p in &reference_paths {
693 in_degree.insert(p.clone(), 0);
694 dependents.insert(p.clone(), Vec::new());
695 }
696
697 for p in &reference_paths {
698 let Some(DataDefinition::Reference { target, .. }) = self.data.get(p) else {
699 unreachable!("BUG: reference entry lost between collect and walk");
700 };
701 if let ReferenceTarget::Data(target_path) = target {
702 if reference_set.contains(target_path) {
703 *in_degree
704 .get_mut(p)
705 .expect("BUG: reference missing in_degree") += 1;
706 dependents
707 .get_mut(target_path)
708 .expect("BUG: reference missing dependents list")
709 .push(p.clone());
710 }
711 }
712 }
713
714 let mut queue: VecDeque<DataPath> = in_degree
715 .iter()
716 .filter(|(_, d)| **d == 0)
717 .map(|(p, _)| p.clone())
718 .collect();
719
720 let mut result: Vec<DataPath> = Vec::new();
721 while let Some(path) = queue.pop_front() {
722 result.push(path.clone());
723 if let Some(deps) = dependents.get(&path) {
724 for dependent in deps.clone() {
725 let degree = in_degree
726 .get_mut(&dependent)
727 .expect("BUG: reference dependent missing in_degree");
728 *degree -= 1;
729 if *degree == 0 {
730 queue.push_back(dependent);
731 }
732 }
733 }
734 }
735
736 if result.len() != reference_paths.len() {
737 let cycle_members: Vec<DataPath> = reference_paths
738 .iter()
739 .filter(|p| !result.contains(p))
740 .cloned()
741 .collect();
742 let cycle_display: String = cycle_members
743 .iter()
744 .map(|p| p.to_string())
745 .collect::<Vec<_>>()
746 .join(", ");
747 let errors: Vec<Error> = cycle_members
748 .iter()
749 .filter_map(|p| {
750 self.data.get(p).map(|entry| {
751 reference_error(
752 &self.main_spec,
753 entry.source(),
754 format!("Circular data reference ({})", cycle_display),
755 )
756 })
757 })
758 .collect();
759 return Err(errors);
760 }
761
762 Ok(result)
763 }
764
765 fn topological_sort(&self) -> Result<Vec<RulePath>, Vec<Error>> {
766 let mut in_degree: BTreeMap<RulePath, usize> = BTreeMap::new();
767 let mut dependents: BTreeMap<RulePath, Vec<RulePath>> = BTreeMap::new();
768 let mut queue = VecDeque::new();
769 let mut result = Vec::new();
770
771 for rule_path in self.rules.keys() {
772 in_degree.insert(rule_path.clone(), 0);
773 dependents.insert(rule_path.clone(), Vec::new());
774 }
775
776 for (rule_path, rule_node) in &self.rules {
777 for dependency in &rule_node.depends_on_rules {
778 if self.rules.contains_key(dependency) {
779 if let Some(degree) = in_degree.get_mut(rule_path) {
780 *degree += 1;
781 }
782 if let Some(deps) = dependents.get_mut(dependency) {
783 deps.push(rule_path.clone());
784 }
785 }
786 }
787 }
788
789 for (rule_path, degree) in &in_degree {
790 if *degree == 0 {
791 queue.push_back(rule_path.clone());
792 }
793 }
794
795 while let Some(rule_path) = queue.pop_front() {
796 result.push(rule_path.clone());
797
798 if let Some(dependent_rules) = dependents.get(&rule_path) {
799 for dependent in dependent_rules {
800 if let Some(degree) = in_degree.get_mut(dependent) {
801 *degree -= 1;
802 if *degree == 0 {
803 queue.push_back(dependent.clone());
804 }
805 }
806 }
807 }
808 }
809
810 if result.len() != self.rules.len() {
811 let missing: Vec<RulePath> = self
812 .rules
813 .keys()
814 .filter(|rule| !result.contains(rule))
815 .cloned()
816 .collect();
817 let cycle: Vec<Source> = missing
818 .iter()
819 .filter_map(|rule| self.rules.get(rule).map(|n| n.source.clone()))
820 .collect();
821
822 if cycle.is_empty() {
823 unreachable!(
824 "BUG: circular dependency detected but no sources could be collected ({} missing rules)",
825 missing.len()
826 );
827 }
828 let rules_involved: String = missing
829 .iter()
830 .map(|rp| rp.rule.as_str())
831 .collect::<Vec<_>>()
832 .join(", ");
833 let message = format!("Circular dependency (rules: {})", rules_involved);
834 let errors: Vec<Error> = cycle
835 .into_iter()
836 .map(|source| {
837 Error::validation_with_context(
838 message.clone(),
839 Some(source),
840 None::<String>,
841 Some(Arc::clone(&self.main_spec)),
842 None,
843 )
844 })
845 .collect();
846 return Err(errors);
847 }
848
849 Ok(result)
850 }
851}
852
853#[derive(Debug)]
854pub(crate) struct RuleNode {
855 pub branches: Vec<(Option<Expression>, Expression)>,
858 pub source: Source,
859
860 pub depends_on_rules: BTreeSet<RulePath>,
861
862 pub rule_type: LemmaType,
865
866 pub spec_arc: Arc<LemmaSpec>,
868}
869
870type ResolvedTypesMap = Vec<(Arc<LemmaRepository>, Arc<LemmaSpec>, ResolvedSpecTypes)>;
871
872struct GraphBuilder<'a> {
873 data: IndexMap<DataPath, DataDefinition>,
874 rules: BTreeMap<RulePath, RuleNode>,
875 context: &'a Context,
876 local_types: ResolvedTypesMap,
877 errors: Vec<Error>,
878 main_spec: Arc<LemmaSpec>,
879 main_repository: Arc<ast::LemmaRepository>,
880}
881
882struct RuleExpressionConversion<'a> {
883 spec: &'a Arc<LemmaSpec>,
884 data_map: &'a HashMap<String, &'a LemmaData>,
885 segments: &'a [PathSegment],
886 rule_names: &'a HashSet<&'a str>,
887 effective: &'a EffectiveDate,
888 depends_on_rules: &'a mut BTreeSet<RulePath>,
889}
890
891fn reference_error(main_spec: &Arc<LemmaSpec>, source: &Source, message: String) -> Error {
892 Error::validation_with_context(
893 message,
894 Some(source.clone()),
895 None::<String>,
896 Some(Arc::clone(main_spec)),
897 None,
898 )
899}
900
901fn reference_kind_mismatch_message<P: fmt::Display>(
918 lhs: &LemmaType,
919 target_type: &LemmaType,
920 reference_path: &DataPath,
921 target_path: &P,
922 target_kind_label: &str,
923) -> Option<String> {
924 if !lhs.has_same_base_type(target_type) {
925 return Some(format!(
926 "Data reference '{}' type mismatch: declared as '{}' but {} '{}' is '{}'",
927 reference_path,
928 lhs.name(),
929 target_kind_label,
930 target_path,
931 target_type.name(),
932 ));
933 }
934 if lhs.is_quantity() && !lhs.same_quantity_family(target_type) {
935 let lhs_family = lhs.quantity_family_name().expect(
936 "BUG: declared quantity data must carry a family name; \
937 anonymous quantity types only arise from runtime synthesis \
938 and never appear as a reference's LHS-declared type",
939 );
940 let target_family = target_type.quantity_family_name().expect(
941 "BUG: declared quantity data must carry a family name; \
942 anonymous quantity types only arise from runtime synthesis \
943 and never appear as a reference target's schema type",
944 );
945 return Some(format!(
946 "Data reference '{}' quantity family mismatch: declared as '{}' (family '{}') but {} '{}' is '{}' (family '{}')",
947 reference_path,
948 lhs.name(),
949 lhs_family,
950 target_kind_label,
951 target_path,
952 target_type.name(),
953 target_family,
954 ));
955 }
956 None
957}
958
959fn constraint_application_type_name(parent: &ParentType, data_name: &str) -> String {
961 match parent {
962 ParentType::Custom { name } => name.clone(),
963 ParentType::Qualified { inner, .. } => constraint_application_type_name(inner, data_name),
964 ParentType::Primitive { .. } => data_name.to_string(),
965 }
966}
967
968fn apply_constraints_to_spec(
973 spec: &Arc<LemmaSpec>,
974 type_name: &str,
975 mut specs: TypeSpecification,
976 constraints: &[Constraint],
977 source: &crate::parsing::source::Source,
978 declared_default: &mut Option<ValueKind>,
979) -> Result<TypeSpecification, Vec<Error>> {
980 let mut errors = Vec::new();
981 let mut apply_one = |specs: TypeSpecification,
982 command: TypeConstraintCommand,
983 args: &[CommandArg],
984 declared_default: &mut Option<ValueKind>|
985 -> TypeSpecification {
986 let specs_clone = specs.clone();
987 let mut default_before = declared_default.clone();
988 match specs.apply_constraint(type_name, command, args, &mut default_before) {
989 Ok(updated_specs) => {
990 *declared_default = default_before;
991 updated_specs
992 }
993 Err(e) => {
994 errors.push(Error::validation_with_context(
995 format!("Failed to apply constraint '{}': {}", command, e),
996 Some(source.clone()),
997 None::<String>,
998 Some(Arc::clone(spec)),
999 None,
1000 ));
1001 specs_clone
1002 }
1003 }
1004 };
1005
1006 let mut deferred: Vec<(TypeConstraintCommand, Vec<CommandArg>)> = Vec::new();
1007 for (command, args) in constraints {
1008 if matches!(
1009 command,
1010 TypeConstraintCommand::Unit | TypeConstraintCommand::Trait
1011 ) {
1012 specs = apply_one(specs, *command, args, declared_default);
1013 } else {
1014 deferred.push((*command, args.clone()));
1015 }
1016 }
1017 for (command, args) in deferred {
1018 specs = apply_one(specs, command, &args, declared_default);
1019 }
1020 if !errors.is_empty() {
1021 return Err(errors);
1022 }
1023 Ok(specs)
1024}
1025
1026impl Graph {
1027 pub(crate) fn build(
1029 context: &Context,
1030 repository: &Arc<LemmaRepository>,
1031 main_spec: &Arc<LemmaSpec>,
1032 dag: &[(Arc<LemmaRepository>, Arc<LemmaSpec>)],
1033 effective: &EffectiveDate,
1034 ) -> Result<(Graph, ResolvedTypesMap), Vec<Error>> {
1035 let mut type_resolver = TypeResolver::new(context);
1036
1037 let mut type_errors: Vec<Error> = Vec::new();
1038 for (repo, spec) in dag {
1039 type_errors.extend(type_resolver.register_all(repo, spec));
1040 }
1041
1042 let (data, rules, graph_errors, local_types) = {
1043 let mut builder = GraphBuilder {
1044 data: IndexMap::new(),
1045 rules: BTreeMap::new(),
1046 context,
1047 local_types: Vec::new(),
1048 errors: Vec::new(),
1049 main_spec: Arc::clone(main_spec),
1050 main_repository: Arc::clone(repository),
1051 };
1052
1053 builder.build_spec(
1054 main_spec,
1055 repository,
1056 Vec::new(),
1057 HashMap::new(),
1058 effective,
1059 &mut type_resolver,
1060 )?;
1061
1062 (
1063 builder.data,
1064 builder.rules,
1065 builder.errors,
1066 builder.local_types,
1067 )
1068 };
1069
1070 let mut graph = Graph {
1071 data,
1072 rules,
1073 execution_order: Vec::new(),
1074 reference_evaluation_order: Vec::new(),
1075 main_spec: Arc::clone(main_spec),
1076 };
1077
1078 let validation_errors = match graph.validate(&local_types) {
1079 Ok(()) => Vec::new(),
1080 Err(errors) => errors,
1081 };
1082
1083 let mut all_errors = type_errors;
1084 all_errors.extend(graph_errors);
1085 all_errors.extend(validation_errors);
1086
1087 if all_errors.is_empty() {
1088 Ok((graph, local_types))
1089 } else {
1090 Err(all_errors)
1091 }
1092 }
1093
1094 fn validate(&mut self, resolved_types: &ResolvedTypesMap) -> Result<(), Vec<Error>> {
1095 let mut errors = Vec::new();
1096
1097 if let Err(structural_errors) = check_all_rule_references_exist(self) {
1099 errors.extend(structural_errors);
1100 }
1101 if let Err(collision_errors) = check_data_and_rule_name_collisions(self) {
1102 errors.extend(collision_errors);
1103 }
1104
1105 if let Err(reference_errors) = self.resolve_data_reference_types() {
1109 errors.extend(reference_errors);
1110 }
1111
1112 let reference_order = match self.compute_reference_evaluation_order() {
1116 Ok(order) => order,
1117 Err(circular_errors) => {
1118 errors.extend(circular_errors);
1119 return Err(errors);
1120 }
1121 };
1122
1123 self.add_rule_reference_dependency_edges();
1129
1130 let execution_order = match self.topological_sort() {
1131 Ok(order) => order,
1132 Err(circular_errors) => {
1133 errors.extend(circular_errors);
1134 return Err(errors);
1135 }
1136 };
1137
1138 let inferred_types = infer_rule_types(self, &execution_order, resolved_types);
1146
1147 if let Err(rule_reference_errors) = self.resolve_rule_reference_types(&inferred_types) {
1152 errors.extend(rule_reference_errors);
1153 }
1154
1155 if let Err(type_errors) =
1157 check_rule_types(self, &execution_order, &inferred_types, resolved_types)
1158 {
1159 errors.extend(type_errors);
1160 }
1161
1162 if !errors.is_empty() {
1163 return Err(errors);
1164 }
1165
1166 apply_inferred_types(self, inferred_types);
1168 self.execution_order = execution_order;
1169 self.reference_evaluation_order = reference_order;
1170 Ok(())
1171 }
1172}
1173
1174fn uses_import_surface_syntax(alias: &str, target_spec: &str) -> String {
1175 if alias == target_spec {
1176 format!("uses {alias}")
1177 } else {
1178 format!("uses {alias}: {target_spec}")
1179 }
1180}
1181
1182fn is_uses_vs_data_clash(existing: &DataDefinition, incoming: &ParsedDataValue) -> bool {
1183 matches!(
1184 (existing, incoming),
1185 (
1186 DataDefinition::Import { .. },
1187 ParsedDataValue::Definition { .. }
1188 )
1189 ) || matches!(
1190 (existing, incoming),
1191 (
1192 DataDefinition::TypeDeclaration { .. } | DataDefinition::Value { .. },
1193 ParsedDataValue::Import(_)
1194 )
1195 )
1196}
1197
1198fn qualified_type_name_from_definition(incoming: &ParsedDataValue) -> Option<&str> {
1199 let ParsedDataValue::Definition {
1200 base: Some(ParentType::Qualified { inner, .. }),
1201 ..
1202 } = incoming
1203 else {
1204 return None;
1205 };
1206 match inner.as_ref() {
1207 ParentType::Custom { name } => Some(name.as_str()),
1208 _ => None,
1209 }
1210}
1211
1212fn uses_vs_data_duplicate_message(
1213 name: &str,
1214 existing: &DataDefinition,
1215 incoming: &ParsedDataValue,
1216) -> (String, Option<String>) {
1217 let (alias, target_spec) = match (existing, incoming) {
1218 (DataDefinition::Import { spec, .. }, ParsedDataValue::Definition { .. }) => {
1219 (name, spec.name.as_str())
1220 }
1221 (
1222 DataDefinition::TypeDeclaration { .. } | DataDefinition::Value { .. },
1223 ParsedDataValue::Import(spec_ref),
1224 ) => (name, spec_ref.name.as_str()),
1225 _ => unreachable!("uses_vs_data_duplicate_message requires a uses vs data clash"),
1226 };
1227 let uses_syntax = uses_import_surface_syntax(alias, target_spec);
1228 let import_alias = format!("{alias}_spec");
1229 let message = format!(
1230 "You used the name `{alias}` in both `{uses_syntax}` and `data {alias}`. A `uses` import and a `data` definition can't share the same name.",
1231 );
1232 let suggestion = match qualified_type_name_from_definition(incoming) {
1233 Some(type_name) => format!(
1234 "Try `uses {import_alias}: {target_spec}` and `data {alias}: {import_alias}.{type_name}`."
1235 ),
1236 _ => format!("Try `uses {import_alias}: {target_spec}` with a different name than `{alias}`."),
1237 };
1238 (message, Some(suggestion))
1239}
1240
1241impl<'a> GraphBuilder<'a> {
1242 fn engine_error(&self, message: impl Into<String>, source: &Source) -> Error {
1243 Error::validation_with_context(
1244 message.into(),
1245 Some(source.clone()),
1246 None::<String>,
1247 Some(Arc::clone(&self.main_spec)),
1248 None,
1249 )
1250 }
1251
1252 fn process_meta_fields(&mut self, spec: &LemmaSpec) {
1253 let mut seen = HashSet::new();
1254 for field in &spec.meta_fields {
1255 if field.key == "title" && !matches!(field.value, MetaValue::Literal(Value::Text(_))) {
1257 self.errors.push(self.engine_error(
1258 "Meta 'title' must be a text literal",
1259 &field.source_location,
1260 ));
1261 }
1262
1263 if !seen.insert(field.key.clone()) {
1264 self.errors.push(self.engine_error(
1265 format!("Duplicate meta key '{}'", field.key),
1266 &field.source_location,
1267 ));
1268 }
1269 }
1270 }
1271
1272 fn resolve_spec_ref(
1273 &self,
1274 spec_ref: &ast::SpecRef,
1275 effective: &EffectiveDate,
1276 consumer_spec: &Arc<LemmaSpec>,
1277 consumer_repository: &Arc<LemmaRepository>,
1278 ) -> Result<(Arc<LemmaRepository>, Arc<LemmaSpec>), Error> {
1279 discovery::resolve_spec_ref(
1280 self.context,
1281 spec_ref,
1282 consumer_repository,
1283 consumer_spec,
1284 effective,
1285 None,
1286 )
1287 }
1288
1289 fn resolve_data_binding(
1296 &mut self,
1297 data: &LemmaData,
1298 current_segment_names: &[String],
1299 parent_spec: &Arc<LemmaSpec>,
1300 effective: &EffectiveDate,
1301 ) -> Option<(Vec<String>, BindingValue, Source)> {
1302 let binding_path_display = format!("{}", data.reference);
1303
1304 let mut walk_spec = Arc::clone(parent_spec);
1305
1306 for segment in &data.reference.segments {
1307 let Some(seg_data) = walk_spec
1308 .data
1309 .iter()
1310 .find(|f| f.reference.segments.is_empty() && f.reference.name == *segment)
1311 else {
1312 self.errors.push(self.engine_error(
1313 format!(
1314 "Data binding path '{}': data '{}' not found in spec '{}'",
1315 binding_path_display, segment, walk_spec.name
1316 ),
1317 &data.source_location,
1318 ));
1319 return None;
1320 };
1321
1322 let spec_ref = match &seg_data.value {
1323 ParsedDataValue::Import(sr) => sr,
1324 _ => {
1325 self.errors.push(self.engine_error(
1326 format!(
1327 "Data binding path '{}': '{}' in spec '{}' is not a spec reference",
1328 binding_path_display, segment, walk_spec.name
1329 ),
1330 &data.source_location,
1331 ));
1332 return None;
1333 }
1334 };
1335
1336 let walk_repository = discovery::lookup_owning_repository(self.context, &walk_spec)
1337 .unwrap_or_else(|| Arc::clone(&self.main_repository));
1338 walk_spec =
1339 match self.resolve_spec_ref(spec_ref, effective, &walk_spec, &walk_repository) {
1340 Ok((_, arc)) => arc,
1341 Err(e) => {
1342 self.errors.push(e);
1343 return None;
1344 }
1345 };
1346 }
1347
1348 if !walk_spec
1349 .data
1350 .iter()
1351 .any(|d| d.reference.segments.is_empty() && d.reference.name == data.reference.name)
1352 {
1353 self.errors.push(self.engine_error(
1354 format!(
1355 "Data binding path '{}': data '{}' not found in spec '{}'",
1356 binding_path_display, data.reference.name, walk_spec.name
1357 ),
1358 &data.source_location,
1359 ));
1360 return None;
1361 }
1362
1363 let mut binding_key: Vec<String> = current_segment_names.to_vec();
1365 binding_key.extend(data.reference.segments.iter().cloned());
1366 binding_key.push(data.reference.name.clone());
1367
1368 let binding_value = match &data.value {
1369 ParsedDataValue::Fill(FillRhs::Literal(v)) => BindingValue::Literal(v.clone()),
1370 ParsedDataValue::Fill(FillRhs::Reference { target }) => {
1371 let resolved_target = self.resolve_reference_target_in_spec(
1372 target,
1373 &data.source_location,
1374 parent_spec,
1375 current_segment_names,
1376 effective,
1377 )?;
1378 BindingValue::Reference {
1379 target: resolved_target,
1380 constraints: None,
1381 }
1382 }
1383 ParsedDataValue::Definition { value: Some(v), .. }
1384 if data.value.is_definition_literal_only() =>
1385 {
1386 BindingValue::Literal(v.clone())
1387 }
1388 ParsedDataValue::Import(_) => {
1389 unreachable!(
1390 "BUG: build_data_bindings must reject Import bindings before calling resolve_data_binding"
1391 );
1392 }
1393 ParsedDataValue::Definition { .. } => {
1394 unreachable!(
1395 "BUG: build_data_bindings must reject non-literal Definition bindings before calling resolve_data_binding"
1396 );
1397 }
1398 };
1399
1400 Some((binding_key, binding_value, data.source_location.clone()))
1401 }
1402
1403 fn resolve_reference_target_in_spec(
1410 &mut self,
1411 reference: &ast::Reference,
1412 reference_source: &Source,
1413 containing_spec_arc: &Arc<LemmaSpec>,
1414 containing_segments_names: &[String],
1415 effective: &EffectiveDate,
1416 ) -> Option<ReferenceTarget> {
1417 let containing_data_map: HashMap<String, LemmaData> = containing_spec_arc
1418 .data
1419 .iter()
1420 .filter(|d| d.reference.is_local())
1421 .map(|d| (d.reference.name.clone(), d.clone()))
1422 .collect();
1423
1424 let containing_rule_names: HashSet<&str> = containing_spec_arc
1425 .rules
1426 .iter()
1427 .map(|r| r.name.as_str())
1428 .collect();
1429
1430 let containing_segments: Vec<PathSegment> = containing_segments_names
1431 .iter()
1432 .map(|name| PathSegment {
1433 data: name.clone(),
1434 spec: containing_spec_arc.name.clone(),
1435 })
1436 .collect();
1437
1438 if reference.segments.is_empty() {
1439 let is_data = containing_data_map.contains_key(&reference.name);
1440 let is_rule = containing_rule_names.contains(reference.name.as_str());
1441 if is_data && is_rule {
1442 self.errors.push(self.engine_error(
1443 format!(
1444 "Reference target '{}' is ambiguous: both a data and a rule in spec '{}'",
1445 reference.name, containing_spec_arc.name
1446 ),
1447 reference_source,
1448 ));
1449 return None;
1450 }
1451 if is_data {
1452 return Some(ReferenceTarget::Data(DataPath {
1453 segments: containing_segments,
1454 data: reference.name.clone(),
1455 }));
1456 }
1457 if is_rule {
1458 return Some(ReferenceTarget::Rule(RulePath {
1459 segments: containing_segments,
1460 rule: reference.name.clone(),
1461 }));
1462 }
1463 self.errors.push(self.engine_error(
1464 format!(
1465 "Reference target '{}' not found in spec '{}'",
1466 reference.name, containing_spec_arc.name
1467 ),
1468 reference_source,
1469 ));
1470 return None;
1471 }
1472
1473 let (resolved_segments, target_spec_arc) = self.resolve_path_segments(
1474 &reference.segments,
1475 reference_source,
1476 containing_data_map,
1477 containing_segments,
1478 Arc::clone(containing_spec_arc),
1479 effective,
1480 )?;
1481
1482 let target_data_names: HashSet<&str> = target_spec_arc
1483 .data
1484 .iter()
1485 .filter(|d| d.reference.is_local())
1486 .map(|d| d.reference.name.as_str())
1487 .collect();
1488 let target_rule_names: HashSet<&str> = target_spec_arc
1489 .rules
1490 .iter()
1491 .map(|r| r.name.as_str())
1492 .collect();
1493 let is_data = target_data_names.contains(reference.name.as_str());
1494 let is_rule = target_rule_names.contains(reference.name.as_str());
1495
1496 if is_data && is_rule {
1497 self.errors.push(self.engine_error(
1498 format!(
1499 "Reference target '{}' is ambiguous: both a data and a rule in spec '{}'",
1500 reference.name, target_spec_arc.name
1501 ),
1502 reference_source,
1503 ));
1504 return None;
1505 }
1506 if is_data {
1507 return Some(ReferenceTarget::Data(DataPath {
1508 segments: resolved_segments,
1509 data: reference.name.clone(),
1510 }));
1511 }
1512 if is_rule {
1513 return Some(ReferenceTarget::Rule(RulePath {
1514 segments: resolved_segments,
1515 rule: reference.name.clone(),
1516 }));
1517 }
1518
1519 self.errors.push(self.engine_error(
1520 format!(
1521 "Reference target '{}' not found in spec '{}'",
1522 reference.name, target_spec_arc.name
1523 ),
1524 reference_source,
1525 ));
1526 None
1527 }
1528
1529 fn build_data_bindings(
1535 &mut self,
1536 spec: &LemmaSpec,
1537 current_segment_names: &[String],
1538 spec_arc: &Arc<LemmaSpec>,
1539 effective: &EffectiveDate,
1540 ) -> Result<DataBindings, Vec<Error>> {
1541 let mut bindings: DataBindings = HashMap::new();
1542 let mut errors: Vec<Error> = Vec::new();
1543
1544 for data in &spec.data {
1545 let has_binding_lhs_segments = !data.reference.segments.is_empty();
1546 let is_local_fill = matches!(&data.value, ParsedDataValue::Fill(_));
1547 if !has_binding_lhs_segments && !is_local_fill {
1548 continue;
1549 }
1550
1551 let binding_path_display = format!("{}", data.reference);
1552
1553 if matches!(&data.value, ParsedDataValue::Import(_)) {
1555 errors.push(self.engine_error(
1556 format!(
1557 "Data binding '{}' cannot override a spec reference — only literal values can be bound to nested data",
1558 binding_path_display
1559 ),
1560 &data.source_location,
1561 ));
1562 continue;
1563 }
1564
1565 if has_binding_lhs_segments {
1567 if let ParsedDataValue::Definition { .. } = &data.value {
1568 if !data.value.is_definition_literal_only() {
1569 errors.push(self.engine_error(
1570 format!(
1571 "Data binding '{}' must provide a literal value, not a data definition",
1572 binding_path_display
1573 ),
1574 &data.source_location,
1575 ));
1576 continue;
1577 }
1578 }
1579 }
1580
1581 if let Some((binding_key, binding_value, source)) =
1582 self.resolve_data_binding(data, current_segment_names, spec_arc, effective)
1583 {
1584 if let Some((_, existing_source)) = bindings.get(&binding_key) {
1585 errors.push(self.engine_error(
1586 format!(
1587 "Duplicate data binding for '{}' (previously bound at {}:{})",
1588 binding_key.join("."),
1589 existing_source.source_type,
1590 existing_source.span.line
1591 ),
1592 &data.source_location,
1593 ));
1594 } else {
1595 bindings.insert(binding_key, (binding_value, source));
1596 }
1597 }
1598 }
1600
1601 if !errors.is_empty() {
1602 return Err(errors);
1603 }
1604
1605 Ok(bindings)
1606 }
1607
1608 fn add_data(
1614 &mut self,
1615 data: &LemmaData,
1616 current_segments: &[PathSegment],
1617 data_bindings: &DataBindings,
1618 current_spec_arc: &Arc<LemmaSpec>,
1619 used_binding_keys: &mut HashSet<Vec<String>>,
1620 effective: &EffectiveDate,
1621 ) {
1622 let data_path = DataPath {
1623 segments: current_segments.to_vec(),
1624 data: data.reference.name.clone(),
1625 };
1626
1627 if let Some(existing) = self.data.get(&data_path) {
1629 let (message, suggestion) = if is_uses_vs_data_clash(existing, &data.value) {
1630 uses_vs_data_duplicate_message(&data_path.data, existing, &data.value)
1631 } else {
1632 (
1633 format!(
1634 "The name '{}' is already used for data in this spec.",
1635 data_path.data
1636 ),
1637 None,
1638 )
1639 };
1640 self.errors.push(Error::validation_with_context(
1641 message,
1642 Some(data.source_location.clone()),
1643 suggestion,
1644 Some(Arc::clone(&self.main_spec)),
1645 None,
1646 ));
1647 return;
1648 }
1649
1650 let binding_key: Vec<String> = current_segments
1652 .iter()
1653 .map(|s| s.data.clone())
1654 .chain(std::iter::once(data.reference.name.clone()))
1655 .collect();
1656
1657 let binding_override: Option<(BindingValue, Source)> =
1664 data_bindings.get(&binding_key).and_then(|(v, s)| {
1665 if matches!(v, BindingValue::Reference { .. })
1666 && Self::has_local_fill_reference_for_name(
1667 current_spec_arc.as_ref(),
1668 &data.reference.name,
1669 )
1670 {
1671 return None;
1672 }
1673 used_binding_keys.insert(binding_key.clone());
1674 Some((v.clone(), s.clone()))
1675 });
1676
1677 let (original_schema_type, original_declared_default) = if matches!(
1678 &data.value,
1679 ParsedDataValue::Definition { .. }
1680 ) && data
1681 .value
1682 .definition_needs_type_resolution()
1683 {
1684 let resolved = self
1685 .local_types
1686 .iter()
1687 .find(|(_, s, _)| Arc::ptr_eq(s, current_spec_arc))
1688 .map(|(_, _, t)| t)
1689 .expect("BUG: no resolved types for spec during add_local_data");
1690 let lemma_type = resolved
1691 .resolved
1692 .get(&data.reference.name)
1693 .expect("BUG: type not in ResolvedSpecTypes.resolved. TypeResolver should have registered it")
1694 .clone();
1695 let declared = resolved
1696 .declared_defaults
1697 .get(&data.reference.name)
1698 .cloned();
1699 (Some(lemma_type), declared)
1700 } else {
1701 (None, None)
1702 };
1703
1704 if let Some((binding_value, binding_source)) = binding_override {
1705 self.add_data_from_binding(
1706 data_path,
1707 binding_value,
1708 binding_source,
1709 original_schema_type,
1710 current_spec_arc,
1711 );
1712 return;
1713 }
1714
1715 let effective_source = data.source_location.clone();
1716
1717 match &data.value {
1718 ParsedDataValue::Definition { .. } if data.value.is_definition_literal_only() => {
1719 let ParsedDataValue::Definition {
1720 value: Some(value), ..
1721 } = &data.value
1722 else {
1723 unreachable!("BUG: literal-only Definition must carry value");
1724 };
1725 self.insert_literal_data(
1726 data_path,
1727 value,
1728 original_schema_type,
1729 effective_source,
1730 current_spec_arc,
1731 );
1732 }
1733 ParsedDataValue::Definition { .. } => {
1734 let mut resolved_type = original_schema_type.unwrap_or_else(|| {
1735 unreachable!(
1736 "BUG: Definition without schema — TypeResolver should have registered it"
1737 )
1738 });
1739 let mut declared_default = original_declared_default;
1740
1741 let is_generic_quantity_range = matches!(
1742 &resolved_type.specifications,
1743 TypeSpecification::QuantityRange {
1744 units,
1745 decomposition,
1746 canonical_unit,
1747 ..
1748 } if units.0.is_empty() && decomposition.is_empty() && canonical_unit.is_empty()
1749 );
1750
1751 if is_generic_quantity_range {
1752 if let Some(ValueKind::Range(left, right)) = &declared_default {
1753 if let (
1754 ValueKind::Quantity(_, left_unit, _),
1755 ValueKind::Quantity(_, right_unit, _),
1756 ) = (&left.value, &right.value)
1757 {
1758 let resolved = self
1759 .local_types
1760 .iter()
1761 .find(|(_, s, _)| Arc::ptr_eq(s, current_spec_arc))
1762 .map(|(_, _, t)| t)
1763 .expect("BUG: no resolved types for spec during add_local_data");
1764
1765 let left_quantity_type = resolved.unit_index.get(left_unit);
1766 let right_quantity_type = resolved.unit_index.get(right_unit);
1767
1768 match (left_quantity_type, right_quantity_type) {
1769 (Some(left_quantity_type), Some(right_quantity_type))
1770 if left_quantity_type
1771 .same_quantity_family(right_quantity_type) =>
1772 {
1773 let specialized_range_type =
1774 infer_range_type_from_endpoint_types(
1775 left_quantity_type,
1776 right_quantity_type,
1777 );
1778 let coerced_left = Graph::coerce_literal_to_schema_type(
1779 left.as_ref(),
1780 left_quantity_type,
1781 )
1782 .unwrap_or_else(|message| {
1783 unreachable!(
1784 "BUG: coercing quantity range default left endpoint failed: {}",
1785 message
1786 )
1787 });
1788 let coerced_right = Graph::coerce_literal_to_schema_type(
1789 right.as_ref(),
1790 right_quantity_type,
1791 )
1792 .unwrap_or_else(|message| {
1793 unreachable!(
1794 "BUG: coercing quantity range default right endpoint failed: {}",
1795 message
1796 )
1797 });
1798 let specialized_default = Graph::coerce_literal_to_schema_type(
1799 &LiteralValue {
1800 value: ValueKind::Range(
1801 Box::new(coerced_left),
1802 Box::new(coerced_right),
1803 ),
1804 lemma_type: specialized_range_type.clone(),
1805 },
1806 &specialized_range_type,
1807 )
1808 .unwrap_or_else(|message| {
1809 unreachable!(
1810 "BUG: specializing generic quantity range default failed: {}",
1811 message
1812 )
1813 });
1814 resolved_type = specialized_range_type;
1815 declared_default = Some(specialized_default.value);
1816 }
1817 _ => {
1818 self.errors.push(self.engine_error(
1819 format!(
1820 "Generic quantity range default must use units from one concrete local quantity family, got '{}' and '{}'",
1821 left_unit, right_unit
1822 ),
1823 &effective_source,
1824 ));
1825 return;
1826 }
1827 }
1828 }
1829 }
1830 }
1831
1832 self.data.insert(
1833 data_path,
1834 DataDefinition::TypeDeclaration {
1835 resolved_type,
1836 declared_default,
1837 source: effective_source,
1838 },
1839 );
1840 }
1841 ParsedDataValue::Import(spec_ref) => {
1842 let consumer_repository =
1843 discovery::lookup_owning_repository(self.context, current_spec_arc)
1844 .unwrap_or_else(|| Arc::clone(&self.main_repository));
1845 let effective_spec_arc = match self.resolve_spec_ref(
1846 spec_ref,
1847 effective,
1848 current_spec_arc,
1849 &consumer_repository,
1850 ) {
1851 Ok((_, arc)) => arc,
1852 Err(e) => {
1853 self.errors.push(e);
1854 return;
1855 }
1856 };
1857
1858 self.data.insert(
1859 data_path,
1860 DataDefinition::Import {
1861 spec: Arc::clone(&effective_spec_arc),
1862 source: effective_source,
1863 },
1864 );
1865 }
1866 ParsedDataValue::Fill(_) => {
1867 self.errors.push(self.engine_error(
1868 "Internal planning error: a fill row reached add_data; fill rows must apply only through data_bindings"
1869 .to_string(),
1870 &effective_source,
1871 ));
1872 }
1873 }
1874 }
1875
1876 fn insert_literal_data(
1880 &mut self,
1881 data_path: DataPath,
1882 value: &ast::Value,
1883 declared_schema_type: Option<LemmaType>,
1884 effective_source: Source,
1885 current_spec_arc: &Arc<LemmaSpec>,
1886 ) {
1887 let semantic_value = if let Some(ref schema) = declared_schema_type {
1888 match parser_value_to_value_kind(value, &schema.specifications) {
1889 Ok(s) => s,
1890 Err(e) => {
1891 self.errors.push(self.engine_error(e, &effective_source));
1892 return;
1893 }
1894 }
1895 } else {
1896 match value {
1897 Value::NumberWithUnit(magnitude, unit) => {
1898 let Some(lt) = self
1899 .local_types
1900 .iter()
1901 .find(|(_, s, _)| Arc::ptr_eq(s, current_spec_arc))
1902 .map(|(_, _, t)| t)
1903 .and_then(|dt| dt.unit_index.get(unit))
1904 else {
1905 self.errors.push(self.engine_error(
1906 format!("Unit '{}' is not in scope for this spec", unit),
1907 &effective_source,
1908 ));
1909 return;
1910 };
1911 match number_with_unit_to_value_kind(*magnitude, unit, lt) {
1912 Ok(s) => s,
1913 Err(e) => {
1914 self.errors.push(self.engine_error(e, &effective_source));
1915 return;
1916 }
1917 }
1918 }
1919 _ => match value_to_semantic(value) {
1920 Ok(s) => s,
1921 Err(e) => {
1922 self.errors.push(self.engine_error(e, &effective_source));
1923 return;
1924 }
1925 },
1926 }
1927 };
1928 let inferred_type = match value {
1929 Value::Text(_) => primitive_text().clone(),
1930 Value::Number(_) => primitive_number().clone(),
1931 Value::NumberWithUnit(_, unit) => {
1932 match self
1933 .local_types
1934 .iter()
1935 .find(|(_, s, _)| Arc::ptr_eq(s, current_spec_arc))
1936 .map(|(_, _, t)| t)
1937 .and_then(|dt| dt.unit_index.get(unit))
1938 {
1939 Some(lt) => lt.clone(),
1940 None => {
1941 self.errors.push(self.engine_error(
1942 format!("Unit '{}' is not in scope for this spec", unit),
1943 &effective_source,
1944 ));
1945 return;
1946 }
1947 }
1948 }
1949 Value::Boolean(_) => primitive_boolean().clone(),
1950 Value::Date(_) => primitive_date().clone(),
1951 Value::Time(_) => primitive_time().clone(),
1952 Value::Calendar(_, _) => primitive_calendar().clone(),
1953 Value::Range(_, _) => match &semantic_value {
1954 ValueKind::Range(left, right) => {
1955 LiteralValue::range(left.as_ref().clone(), right.as_ref().clone()).lemma_type
1956 }
1957 _ => unreachable!(
1958 "BUG: semantic range literal conversion returned non-range value kind"
1959 ),
1960 },
1961 };
1962 let schema_type = declared_schema_type.unwrap_or(inferred_type);
1963 let literal_value = LiteralValue {
1964 value: semantic_value,
1965 lemma_type: schema_type,
1966 };
1967 self.data.insert(
1968 data_path,
1969 DataDefinition::Value {
1970 value: literal_value,
1971 source: effective_source,
1972 },
1973 );
1974 }
1975
1976 fn add_data_from_binding(
1979 &mut self,
1980 data_path: DataPath,
1981 binding_value: BindingValue,
1982 binding_source: Source,
1983 declared_schema_type: Option<LemmaType>,
1984 current_spec_arc: &Arc<LemmaSpec>,
1985 ) {
1986 match binding_value {
1987 BindingValue::Literal(value) => {
1988 self.insert_literal_data(
1989 data_path,
1990 &value,
1991 declared_schema_type,
1992 binding_source,
1993 current_spec_arc,
1994 );
1995 }
1996 BindingValue::Reference {
1997 target,
1998 constraints,
1999 } => {
2000 let provisional_type =
2001 declared_schema_type.unwrap_or_else(LemmaType::undetermined_type);
2002 self.data.insert(
2003 data_path,
2004 DataDefinition::Reference {
2005 target,
2006 resolved_type: provisional_type,
2007 local_constraints: constraints,
2008 local_default: None,
2009 source: binding_source,
2010 },
2011 );
2012 }
2013 }
2014 }
2015
2016 fn resolve_path_segments(
2018 &mut self,
2019 segments: &[String],
2020 reference_source: &Source,
2021 mut current_data_map: HashMap<String, LemmaData>,
2022 mut path_segments: Vec<PathSegment>,
2023 mut spec_context: Arc<LemmaSpec>,
2024 effective: &EffectiveDate,
2025 ) -> Option<(Vec<PathSegment>, Arc<LemmaSpec>)> {
2026 let mut last_arc: Option<Arc<LemmaSpec>> = None;
2027
2028 for segment in segments.iter() {
2029 let data_ref =
2030 match current_data_map.get(segment) {
2031 Some(f) => f,
2032 None => {
2033 self.errors.push(self.engine_error(
2034 format!("Data '{}' not found", segment),
2035 reference_source,
2036 ));
2037 return None;
2038 }
2039 };
2040
2041 if let ParsedDataValue::Import(original_spec_ref) = &data_ref.value {
2042 let context_repository =
2043 discovery::lookup_owning_repository(self.context, &spec_context)
2044 .unwrap_or_else(|| Arc::clone(&self.main_repository));
2045 let arc = match self.resolve_spec_ref(
2046 original_spec_ref,
2047 effective,
2048 &spec_context,
2049 &context_repository,
2050 ) {
2051 Ok((_, a)) => a,
2052 Err(e) => {
2053 self.errors.push(e);
2054 return None;
2055 }
2056 };
2057 spec_context = Arc::clone(&arc);
2058
2059 path_segments.push(PathSegment {
2060 data: segment.clone(),
2061 spec: arc.name.clone(),
2062 });
2063 current_data_map = arc
2064 .data
2065 .iter()
2066 .map(|f| (f.reference.name.clone(), f.clone()))
2067 .collect();
2068 last_arc = Some(arc);
2069 } else {
2070 self.errors.push(self.engine_error(
2071 format!("Data '{}' is not a spec reference", segment),
2072 reference_source,
2073 ));
2074 return None;
2075 }
2076 }
2077
2078 let final_arc = last_arc.unwrap_or_else(|| {
2079 unreachable!(
2080 "BUG: resolve_path_segments called with empty segments should not reach here"
2081 )
2082 });
2083 Some((path_segments, final_arc))
2084 }
2085
2086 fn has_local_fill_reference_for_name(spec: &LemmaSpec, name: &str) -> bool {
2087 spec.data.iter().any(|d| {
2088 d.reference.segments.is_empty()
2089 && d.reference.name == name
2090 && matches!(&d.value, ParsedDataValue::Fill(FillRhs::Reference { .. }))
2091 })
2092 }
2093
2094 fn materialize_local_fill_rows(
2097 &mut self,
2098 spec: &LemmaSpec,
2099 current_segments: &[PathSegment],
2100 effective_bindings: &DataBindings,
2101 spec_arc: &Arc<LemmaSpec>,
2102 used_binding_keys: &mut HashSet<Vec<String>>,
2103 ) {
2104 let current_segment_names: Vec<String> =
2105 current_segments.iter().map(|s| s.data.clone()).collect();
2106
2107 for data in &spec.data {
2108 if !data.reference.segments.is_empty() {
2109 continue;
2110 }
2111 if !matches!(&data.value, ParsedDataValue::Fill(_)) {
2112 continue;
2113 }
2114
2115 let data_path = DataPath {
2116 segments: current_segments.to_vec(),
2117 data: data.reference.name.clone(),
2118 };
2119
2120 let binding_key: Vec<String> = current_segment_names
2121 .iter()
2122 .cloned()
2123 .chain(std::iter::once(data.reference.name.clone()))
2124 .collect();
2125
2126 let Some((binding_value, binding_source)) = effective_bindings.get(&binding_key) else {
2127 self.errors.push(self.engine_error(
2128 format!(
2129 "Internal planning error: fill '{}' has no resolved binding",
2130 data.reference.name
2131 ),
2132 &data.source_location,
2133 ));
2134 continue;
2135 };
2136
2137 used_binding_keys.insert(binding_key);
2138
2139 if let Some(DataDefinition::TypeDeclaration {
2140 resolved_type,
2141 declared_default,
2142 ..
2143 }) = self.data.get(&data_path)
2144 {
2145 let BindingValue::Reference {
2146 target,
2147 constraints,
2148 } = binding_value
2149 else {
2150 continue;
2151 };
2152 if constraints.is_some() {
2153 self.errors.push(self.engine_error(
2154 format!(
2155 "Constraint chains (`-> ...`) on `fill` are not allowed; use `data {}: … -> …` for constraints",
2156 data.reference.name
2157 ),
2158 &data.source_location,
2159 ));
2160 continue;
2161 }
2162 let resolved_type = resolved_type.clone();
2163 let declared_default = declared_default.clone();
2164 self.data.insert(
2165 data_path.clone(),
2166 DataDefinition::Reference {
2167 target: target.clone(),
2168 resolved_type,
2169 local_constraints: None,
2170 local_default: declared_default,
2171 source: binding_source.clone(),
2172 },
2173 );
2174 continue;
2175 }
2176
2177 if self.data.contains_key(&data_path) {
2178 continue;
2179 }
2180
2181 self.add_data_from_binding(
2182 data_path,
2183 binding_value.clone(),
2184 binding_source.clone(),
2185 None,
2186 spec_arc,
2187 );
2188 }
2189 }
2190
2191 fn build_spec(
2192 &mut self,
2193 spec_arc: &Arc<LemmaSpec>,
2194 spec_repository: &Arc<LemmaRepository>,
2195 current_segments: Vec<PathSegment>,
2196 data_bindings: DataBindings,
2197 effective: &EffectiveDate,
2198 type_resolver: &mut TypeResolver<'a>,
2199 ) -> Result<(), Vec<Error>> {
2200 let spec = spec_arc.as_ref();
2201
2202 if current_segments.is_empty() {
2203 self.process_meta_fields(spec);
2204 }
2205
2206 let current_segment_names: Vec<String> =
2207 current_segments.iter().map(|s| s.data.clone()).collect();
2208
2209 let this_spec_bindings =
2211 match self.build_data_bindings(spec, ¤t_segment_names, spec_arc, effective) {
2212 Ok(bindings) => bindings,
2213 Err(errors) => {
2214 self.errors.extend(errors);
2215 HashMap::new()
2216 }
2217 };
2218
2219 let data_map: HashMap<String, &LemmaData> = spec
2221 .data
2222 .iter()
2223 .map(|data| (data.reference.name.clone(), data))
2224 .collect();
2225
2226 if !self
2227 .local_types
2228 .iter()
2229 .any(|(_, s, _)| Arc::ptr_eq(s, spec_arc))
2230 {
2231 if !type_resolver.is_registered(spec_arc) {
2235 return Ok(());
2236 }
2237 match type_resolver.resolve_and_validate(spec_arc, effective) {
2238 Ok(resolved_types) => {
2239 self.local_types.push((
2240 Arc::clone(spec_repository),
2241 Arc::clone(spec_arc),
2242 resolved_types,
2243 ));
2244 }
2245 Err(es) => {
2246 self.errors.extend(es);
2247 return Ok(());
2248 }
2249 }
2250 }
2251
2252 for data in &spec.data {
2253 if let ParsedDataValue::Definition {
2254 base: Some(ParentType::Qualified { spec_alias, .. }),
2255 ..
2256 } = &data.value
2257 {
2258 let from_ref = ast::SpecRef::same_repository(spec_alias.clone());
2259 match self.resolve_spec_ref(&from_ref, effective, spec_arc, spec_repository) {
2260 Ok((source_repo, source_arc)) => {
2261 if !self
2262 .local_types
2263 .iter()
2264 .any(|(_, s, _)| Arc::ptr_eq(s, &source_arc))
2265 {
2266 match type_resolver.resolve_and_validate(&source_arc, effective) {
2267 Ok(resolved_types) => {
2268 self.local_types.push((
2269 source_repo,
2270 source_arc,
2271 resolved_types,
2272 ));
2273 }
2274 Err(es) => self.errors.extend(es),
2275 }
2276 }
2277 }
2278 Err(e) => self.errors.push(e),
2279 }
2280 }
2281 }
2282
2283 let mut effective_bindings = data_bindings.clone();
2284 effective_bindings.extend(this_spec_bindings.clone());
2285
2286 let mut used_binding_keys: HashSet<Vec<String>> = HashSet::new();
2288 for data in &spec.data {
2289 if !data.reference.segments.is_empty() {
2290 continue; }
2292 if matches!(&data.value, ParsedDataValue::Fill(_)) {
2293 continue; }
2295 if matches!(&data.value, ParsedDataValue::Import(_)) {
2296 continue;
2297 }
2298 self.add_data(
2299 data,
2300 ¤t_segments,
2301 &effective_bindings,
2302 spec_arc,
2303 &mut used_binding_keys,
2304 effective,
2305 );
2306 }
2307
2308 self.materialize_local_fill_rows(
2309 spec,
2310 ¤t_segments,
2311 &effective_bindings,
2312 spec_arc,
2313 &mut used_binding_keys,
2314 );
2315
2316 for data in &spec.data {
2317 if !data.reference.segments.is_empty() {
2318 continue;
2319 }
2320 if let ParsedDataValue::Import(spec_ref) = &data.value {
2321 let nested_effective = spec_ref.at(effective);
2322 let (nested_repo, nested_arc) =
2323 match self.resolve_spec_ref(spec_ref, effective, spec_arc, spec_repository) {
2324 Ok(pair) => pair,
2325 Err(e) => {
2326 self.errors.push(e);
2327 continue;
2328 }
2329 };
2330 self.add_data(
2331 data,
2332 ¤t_segments,
2333 &effective_bindings,
2334 spec_arc,
2335 &mut used_binding_keys,
2336 effective,
2337 );
2338 let mut nested_segments = current_segments.clone();
2339 nested_segments.push(PathSegment {
2340 data: data.reference.name.clone(),
2341 spec: nested_arc.name.clone(),
2342 });
2343
2344 let nested_segment_names: Vec<String> =
2345 nested_segments.iter().map(|s| s.data.clone()).collect();
2346 let mut combined_bindings = effective_bindings.clone();
2347 for (key, value_and_source) in &data_bindings {
2348 if key.len() > nested_segment_names.len()
2349 && key[..nested_segment_names.len()] == nested_segment_names[..]
2350 && !combined_bindings.contains_key(key)
2351 {
2352 combined_bindings.insert(key.clone(), value_and_source.clone());
2353 }
2354 }
2355
2356 if let Err(errs) = self.build_spec(
2357 &nested_arc,
2358 &nested_repo,
2359 nested_segments,
2360 combined_bindings,
2361 &nested_effective,
2362 type_resolver,
2363 ) {
2364 self.errors.extend(errs);
2365 }
2366 }
2367 }
2368
2369 let expected_key_len = current_segments.len() + 1;
2371 for data in &spec.data {
2372 if data.reference.segments.is_empty() {
2373 continue;
2374 }
2375 let mut binding_key: Vec<String> = current_segment_names.clone();
2376 binding_key.extend(data.reference.segments.iter().cloned());
2377 binding_key.push(data.reference.name.clone());
2378 if binding_key.len() != expected_key_len {
2379 continue;
2380 }
2381 if used_binding_keys.contains(&binding_key) {
2382 continue;
2383 }
2384 let Some((_, binding_source)) = effective_bindings.get(&binding_key) else {
2385 continue;
2386 };
2387 self.errors.push(self.engine_error(
2388 format!(
2389 "No declared data matches fill or binding for '{}'",
2390 binding_key.join(".")
2391 ),
2392 binding_source,
2393 ));
2394 }
2395
2396 let rule_names: HashSet<&str> = spec.rules.iter().map(|r| r.name.as_str()).collect();
2397 for rule in &spec.rules {
2398 self.add_rule(
2399 rule,
2400 spec_arc,
2401 &data_map,
2402 ¤t_segments,
2403 &rule_names,
2404 effective,
2405 );
2406 }
2407
2408 Ok(())
2409 }
2410
2411 fn add_rule(
2412 &mut self,
2413 rule: &LemmaRule,
2414 current_spec_arc: &Arc<LemmaSpec>,
2415 data_map: &HashMap<String, &LemmaData>,
2416 current_segments: &[PathSegment],
2417 rule_names: &HashSet<&str>,
2418 effective: &EffectiveDate,
2419 ) {
2420 let rule_path = RulePath {
2421 segments: current_segments.to_vec(),
2422 rule: rule.name.clone(),
2423 };
2424
2425 if self.rules.contains_key(&rule_path) {
2426 let rule_source = &rule.source_location;
2427 self.errors.push(
2428 self.engine_error(format!("Duplicate rule '{}'", rule_path.rule), rule_source),
2429 );
2430 return;
2431 }
2432
2433 let mut branches = Vec::new();
2434 let mut depends_on_rules = BTreeSet::new();
2435 let mut convert_ctx = RuleExpressionConversion {
2436 spec: current_spec_arc,
2437 data_map,
2438 segments: current_segments,
2439 rule_names,
2440 effective,
2441 depends_on_rules: &mut depends_on_rules,
2442 };
2443
2444 let converted_expression = match self
2445 .convert_expression_and_extract_dependencies(&rule.expression, &mut convert_ctx)
2446 {
2447 Some(expr) => expr,
2448 None => return,
2449 };
2450 branches.push((None, converted_expression));
2451
2452 for unless_clause in &rule.unless_clauses {
2453 let converted_condition = match self.convert_expression_and_extract_dependencies(
2454 &unless_clause.condition,
2455 &mut convert_ctx,
2456 ) {
2457 Some(expr) => expr,
2458 None => return,
2459 };
2460 let converted_result = match self.convert_expression_and_extract_dependencies(
2461 &unless_clause.result,
2462 &mut convert_ctx,
2463 ) {
2464 Some(expr) => expr,
2465 None => return,
2466 };
2467 branches.push((Some(converted_condition), converted_result));
2468 }
2469
2470 let rule_node = RuleNode {
2471 branches,
2472 source: rule.source_location.clone(),
2473 depends_on_rules,
2474 rule_type: LemmaType::veto_type(),
2475 spec_arc: Arc::clone(current_spec_arc),
2476 };
2477
2478 self.rules.insert(rule_path, rule_node);
2479 }
2480
2481 fn convert_binary_operands(
2483 &mut self,
2484 left: &ast::Expression,
2485 right: &ast::Expression,
2486 ctx: &mut RuleExpressionConversion<'_>,
2487 ) -> Option<(Expression, Expression)> {
2488 let converted_left = self.convert_expression_and_extract_dependencies(left, ctx)?;
2489 let converted_right = self.convert_expression_and_extract_dependencies(right, ctx)?;
2490 Some((converted_left, converted_right))
2491 }
2492
2493 fn convert_expression_and_extract_dependencies(
2495 &mut self,
2496 expr: &ast::Expression,
2497 ctx: &mut RuleExpressionConversion<'_>,
2498 ) -> Option<Expression> {
2499 let expr_src = expr
2500 .source_location
2501 .as_ref()
2502 .expect("BUG: AST expression missing source location");
2503 match &expr.kind {
2504 ast::ExpressionKind::Reference(r) => {
2505 let expr_source = expr_src;
2506 let (segments, target_arc_opt) = if r.segments.is_empty() {
2507 (ctx.segments.to_vec(), None)
2508 } else {
2509 let data_map_owned: HashMap<String, LemmaData> = ctx
2510 .data_map
2511 .iter()
2512 .map(|(k, v)| (k.clone(), (*v).clone()))
2513 .collect();
2514 let (segs, arc) = self.resolve_path_segments(
2515 &r.segments,
2516 expr_source,
2517 data_map_owned,
2518 ctx.segments.to_vec(),
2519 Arc::clone(ctx.spec),
2520 ctx.effective,
2521 )?;
2522 (segs, Some(arc))
2523 };
2524
2525 let (is_data, is_rule, target_spec_name_opt) = match &target_arc_opt {
2526 None => {
2527 let is_data = ctx.data_map.contains_key(&r.name);
2528 let is_rule = ctx.rule_names.contains(r.name.as_str());
2529 (is_data, is_rule, None)
2530 }
2531 Some(target_arc) => {
2532 let target_spec = target_arc.as_ref();
2533 let target_data_names: HashSet<&str> = target_spec
2534 .data
2535 .iter()
2536 .filter(|f| f.reference.is_local())
2537 .map(|f| f.reference.name.as_str())
2538 .collect();
2539 let target_rule_names: HashSet<&str> =
2540 target_spec.rules.iter().map(|r| r.name.as_str()).collect();
2541 let is_data = target_data_names.contains(r.name.as_str());
2542 let is_rule = target_rule_names.contains(r.name.as_str());
2543 (is_data, is_rule, Some(target_spec.name.as_str()))
2544 }
2545 };
2546
2547 if is_data && is_rule {
2548 self.errors.push(self.engine_error(
2549 format!("'{}' is both a data and a rule", r.name),
2550 expr_source,
2551 ));
2552 return None;
2553 }
2554 if is_data {
2555 let data_path = DataPath {
2556 segments,
2557 data: r.name.clone(),
2558 };
2559 return Some(Expression {
2560 kind: ExpressionKind::DataPath(data_path),
2561 source_location: expr.source_location.clone(),
2562 });
2563 }
2564 if is_rule {
2565 let rule_path = RulePath {
2566 segments,
2567 rule: r.name.clone(),
2568 };
2569 ctx.depends_on_rules.insert(rule_path.clone());
2570 return Some(Expression {
2571 kind: ExpressionKind::RulePath(rule_path),
2572 source_location: expr.source_location.clone(),
2573 });
2574 }
2575 let msg = match target_spec_name_opt {
2576 Some(s) => format!("Reference '{}' not found in spec '{}'", r.name, s),
2577 None => format!("Reference '{}' not found", r.name),
2578 };
2579 self.errors.push(self.engine_error(msg, expr_source));
2580 None
2581 }
2582
2583 ast::ExpressionKind::LogicalAnd(left, right) => {
2584 let (l, r) = self.convert_binary_operands(left, right, ctx)?;
2585 Some(Expression {
2586 kind: ExpressionKind::LogicalAnd(Arc::new(l), Arc::new(r)),
2587 source_location: expr.source_location.clone(),
2588 })
2589 }
2590
2591 ast::ExpressionKind::Arithmetic(left, op, right) => {
2592 let (l, r) = self.convert_binary_operands(left, right, ctx)?;
2593 Some(Expression {
2594 kind: ExpressionKind::Arithmetic(Arc::new(l), op.clone(), Arc::new(r)),
2595 source_location: expr.source_location.clone(),
2596 })
2597 }
2598
2599 ast::ExpressionKind::Comparison(left, op, right) => {
2600 let (l, r) = self.convert_binary_operands(left, right, ctx)?;
2601 Some(Expression {
2602 kind: ExpressionKind::Comparison(Arc::new(l), op.clone(), Arc::new(r)),
2603 source_location: expr.source_location.clone(),
2604 })
2605 }
2606
2607 ast::ExpressionKind::UnitConversion(value, target) => {
2608 let converted_value =
2609 self.convert_expression_and_extract_dependencies(value, ctx)?;
2610
2611 let resolved_spec_types = self
2612 .local_types
2613 .iter()
2614 .find(|(_, s, _)| Arc::ptr_eq(s, ctx.spec))
2615 .map(|(_, _, t)| t);
2616 let unit_index = resolved_spec_types.map(|dt| &dt.unit_index);
2617 let semantic_target = match conversion_target_to_semantic(target, unit_index) {
2618 Ok(t) => t,
2619 Err(msg) => {
2620 let full_msg = unit_index
2623 .map(|idx| {
2624 let valid: Vec<&str> = idx.keys().map(String::as_str).collect();
2625 format!("{} Valid units: {}", msg, valid.join(", "))
2626 })
2627 .unwrap_or(msg);
2628 self.errors.push(Error::validation_with_context(
2629 full_msg,
2630 expr.source_location.clone(),
2631 None::<String>,
2632 Some(Arc::clone(&self.main_spec)),
2633 None,
2634 ));
2635 return None;
2636 }
2637 };
2638
2639 Some(Expression {
2640 kind: ExpressionKind::UnitConversion(
2641 Arc::new(converted_value),
2642 semantic_target,
2643 ),
2644 source_location: expr.source_location.clone(),
2645 })
2646 }
2647
2648 ast::ExpressionKind::LogicalNegation(operand, neg_type) => {
2649 let converted_operand =
2650 self.convert_expression_and_extract_dependencies(operand, ctx)?;
2651 Some(Expression {
2652 kind: ExpressionKind::LogicalNegation(
2653 Arc::new(converted_operand),
2654 neg_type.clone(),
2655 ),
2656 source_location: expr.source_location.clone(),
2657 })
2658 }
2659
2660 ast::ExpressionKind::MathematicalComputation(op, operand) => {
2661 let converted_operand =
2662 self.convert_expression_and_extract_dependencies(operand, ctx)?;
2663 Some(Expression {
2664 kind: ExpressionKind::MathematicalComputation(
2665 op.clone(),
2666 Arc::new(converted_operand),
2667 ),
2668 source_location: expr.source_location.clone(),
2669 })
2670 }
2671
2672 ast::ExpressionKind::Literal(value) => {
2673 let semantic_value = match value {
2674 Value::NumberWithUnit(magnitude, unit) => {
2675 let Some(lt) = self
2676 .local_types
2677 .iter()
2678 .find(|(_, s, _)| Arc::ptr_eq(s, ctx.spec))
2679 .map(|(_, _, t)| t)
2680 .and_then(|dt| dt.unit_index.get(unit))
2681 else {
2682 self.errors.push(self.engine_error(
2683 format!("Unit '{}' is not in scope for this spec", unit),
2684 expr_src,
2685 ));
2686 return None;
2687 };
2688 match number_with_unit_to_value_kind(*magnitude, unit, lt) {
2689 Ok(v) => v,
2690 Err(e) => {
2691 self.errors.push(self.engine_error(e, expr_src));
2692 return None;
2693 }
2694 }
2695 }
2696 _ => match value_to_semantic(value) {
2697 Ok(v) => v,
2698 Err(e) => {
2699 self.errors.push(self.engine_error(e, expr_src));
2700 return None;
2701 }
2702 },
2703 };
2704 let lemma_type = match value {
2705 Value::Text(_) => primitive_text().clone(),
2706 Value::Number(_) => primitive_number().clone(),
2707 Value::NumberWithUnit(_, unit) => {
2708 match self
2709 .local_types
2710 .iter()
2711 .find(|(_, s, _)| Arc::ptr_eq(s, ctx.spec))
2712 .map(|(_, _, t)| t)
2713 .and_then(|dt| dt.unit_index.get(unit))
2714 {
2715 Some(lt) => lt.clone(),
2716 None => {
2717 self.errors.push(self.engine_error(
2718 format!("Unit '{}' is not in scope for this spec", unit),
2719 expr_src,
2720 ));
2721 return None;
2722 }
2723 }
2724 }
2725 Value::Boolean(_) => primitive_boolean().clone(),
2726 Value::Date(_) => primitive_date().clone(),
2727 Value::Time(_) => primitive_time().clone(),
2728 Value::Calendar(_, _) => primitive_calendar().clone(),
2729 Value::Range(_, _) => match &semantic_value {
2730 ValueKind::Range(left, right) => {
2731 LiteralValue::range(left.as_ref().clone(), right.as_ref().clone())
2732 .lemma_type
2733 }
2734 _ => unreachable!(
2735 "BUG: semantic range literal conversion returned non-range value kind"
2736 ),
2737 },
2738 };
2739 let literal_value = LiteralValue {
2740 value: semantic_value,
2741 lemma_type,
2742 };
2743 Some(Expression {
2744 kind: ExpressionKind::Literal(Box::new(literal_value)),
2745 source_location: expr.source_location.clone(),
2746 })
2747 }
2748
2749 ast::ExpressionKind::Veto(veto_expression) => Some(Expression {
2750 kind: ExpressionKind::Veto(veto_expression.clone()),
2751 source_location: expr.source_location.clone(),
2752 }),
2753
2754 ast::ExpressionKind::ResultIsVeto(operand) => {
2755 let converted = self.convert_expression_and_extract_dependencies(operand, ctx)?;
2756 Some(Expression {
2757 kind: ExpressionKind::ResultIsVeto(Arc::new(converted)),
2758 source_location: expr.source_location.clone(),
2759 })
2760 }
2761
2762 ast::ExpressionKind::Now => Some(Expression {
2763 kind: ExpressionKind::Now,
2764 source_location: expr.source_location.clone(),
2765 }),
2766
2767 ast::ExpressionKind::DateRelative(kind, date_expr) => {
2768 let converted_date =
2769 self.convert_expression_and_extract_dependencies(date_expr, ctx)?;
2770 Some(Expression {
2771 kind: ExpressionKind::DateRelative(*kind, Arc::new(converted_date)),
2772 source_location: expr.source_location.clone(),
2773 })
2774 }
2775
2776 ast::ExpressionKind::DateCalendar(kind, unit, date_expr) => {
2777 let converted_date =
2778 self.convert_expression_and_extract_dependencies(date_expr, ctx)?;
2779 Some(Expression {
2780 kind: ExpressionKind::DateCalendar(*kind, *unit, Arc::new(converted_date)),
2781 source_location: expr.source_location.clone(),
2782 })
2783 }
2784
2785 ast::ExpressionKind::RangeLiteral(left, right) => {
2786 let (l, r) = self.convert_binary_operands(left, right, ctx)?;
2787 Some(Expression {
2788 kind: ExpressionKind::RangeLiteral(Arc::new(l), Arc::new(r)),
2789 source_location: expr.source_location.clone(),
2790 })
2791 }
2792
2793 ast::ExpressionKind::PastFutureRange(kind, offset_expr) => {
2794 let converted_offset =
2795 self.convert_expression_and_extract_dependencies(offset_expr, ctx)?;
2796 Some(Expression {
2797 kind: ExpressionKind::PastFutureRange(*kind, Arc::new(converted_offset)),
2798 source_location: expr.source_location.clone(),
2799 })
2800 }
2801
2802 ast::ExpressionKind::RangeContainment(value, range) => {
2803 let (converted_value, converted_range) =
2804 self.convert_binary_operands(value, range, ctx)?;
2805 Some(Expression {
2806 kind: ExpressionKind::RangeContainment(
2807 Arc::new(converted_value),
2808 Arc::new(converted_range),
2809 ),
2810 source_location: expr.source_location.clone(),
2811 })
2812 }
2813 }
2814 }
2815}
2816
2817fn find_types_by_spec<'b>(
2820 types: &'b ResolvedTypesMap,
2821 spec_arc: &Arc<LemmaSpec>,
2822) -> Option<&'b ResolvedSpecTypes> {
2823 types
2824 .iter()
2825 .find(|(_, s, _)| Arc::ptr_eq(s, spec_arc))
2826 .map(|(_, _, t)| t)
2827}
2828
2829fn find_duration_type_in_spec(
2830 resolved_types: &ResolvedTypesMap,
2831 spec_arc: &Arc<LemmaSpec>,
2832) -> Option<LemmaType> {
2833 let spec_types = find_types_by_spec(resolved_types, spec_arc)?;
2834 if let Some(named) = spec_types
2835 .resolved
2836 .values()
2837 .find(|lemma_type| lemma_type.is_duration_like_quantity())
2838 {
2839 return Some(named.clone());
2840 }
2841 if let Some(from_units) = spec_types
2842 .unit_index
2843 .values()
2844 .find(|lemma_type| lemma_type.is_duration_like_quantity())
2845 {
2846 return Some(from_units.clone());
2847 }
2848 for data in &spec_arc.data {
2849 let ParsedDataValue::Import(spec_ref) = &data.value else {
2850 continue;
2851 };
2852 let (_, _, imported_types) = resolved_types
2853 .iter()
2854 .find(|(_, s, _)| s.name == spec_ref.name)?;
2855 if let Some(duration_type) = imported_types
2856 .resolved
2857 .get("duration")
2858 .filter(|t| t.is_duration_like_quantity())
2859 {
2860 return Some(duration_type.clone());
2861 }
2862 }
2863 None
2864}
2865
2866fn compute_arithmetic_result_type(
2867 left_type: LemmaType,
2868 op: &ArithmeticComputation,
2869 right_type: LemmaType,
2870) -> LemmaType {
2871 compute_arithmetic_result_type_recursive(left_type, op, right_type, false)
2872}
2873
2874fn compute_arithmetic_result_type_recursive(
2875 left_type: LemmaType,
2876 op: &ArithmeticComputation,
2877 right_type: LemmaType,
2878 swapped: bool,
2879) -> LemmaType {
2880 match (&left_type.specifications, &right_type.specifications) {
2881 (TypeSpecification::Veto { .. }, _) | (_, TypeSpecification::Veto { .. }) => {
2882 LemmaType::veto_type()
2883 }
2884 (TypeSpecification::Undetermined, _) => LemmaType::undetermined_type(),
2885
2886 (TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => {
2887 LemmaType::undetermined_type()
2888 }
2889 (TypeSpecification::Date { .. }, TypeSpecification::Time { .. }) => {
2890 LemmaType::anonymous_for_decomposition(duration_decomposition())
2891 }
2892 (TypeSpecification::Time { .. }, TypeSpecification::Time { .. }) => {
2893 LemmaType::anonymous_for_decomposition(duration_decomposition())
2894 }
2895
2896 _ if left_type == right_type
2899 && !matches!(
2900 &left_type.specifications,
2901 TypeSpecification::Quantity { .. }
2902 | TypeSpecification::QuantityRange { .. }
2903 | TypeSpecification::NumberRange { .. }
2904 | TypeSpecification::DateRange { .. }
2905 | TypeSpecification::Calendar { .. }
2906 | TypeSpecification::RatioRange { .. }
2907 ) =>
2908 {
2909 left_type
2910 }
2911
2912 (TypeSpecification::Date { .. }, TypeSpecification::Calendar { .. }) => left_type,
2913 (TypeSpecification::Date { .. }, TypeSpecification::Quantity { .. })
2914 if right_type.is_duration_like_quantity() =>
2915 {
2916 left_type
2917 }
2918 (TypeSpecification::Time { .. }, TypeSpecification::Quantity { .. })
2919 if right_type.is_duration_like_quantity() =>
2920 {
2921 left_type
2922 }
2923
2924 (TypeSpecification::Quantity { .. }, TypeSpecification::Ratio { .. }) => left_type,
2925 (TypeSpecification::Quantity { .. }, TypeSpecification::Number { .. }) => left_type,
2926 (
2927 TypeSpecification::Quantity {
2928 decomposition: l_decomp,
2929 ..
2930 },
2931 TypeSpecification::Calendar { .. },
2932 ) => match op {
2933 ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
2934 LemmaType::undetermined_type()
2935 }
2936 ArithmeticComputation::Multiply | ArithmeticComputation::Divide => {
2937 let cal_decomp = calendar_decomposition();
2938 let combined = combine_decompositions(
2939 l_decomp,
2940 &cal_decomp,
2941 matches!(op, ArithmeticComputation::Multiply),
2942 );
2943 if combined.is_empty() {
2944 primitive_number().clone()
2945 } else {
2946 LemmaType::anonymous_for_decomposition(combined)
2947 }
2948 }
2949 _ => primitive_number().clone(),
2950 },
2951 (
2952 TypeSpecification::Quantity {
2953 decomposition: l_decomp,
2954 ..
2955 },
2956 TypeSpecification::Quantity {
2957 decomposition: r_decomp,
2958 ..
2959 },
2960 ) => match op {
2961 ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
2962 if left_type.compatible_with_anonymous_quantity(&right_type)
2963 || right_type.compatible_with_anonymous_quantity(&left_type)
2964 {
2965 let left_decomp = left_type.quantity_type_decomposition();
2966 let right_decomp = right_type.quantity_type_decomposition();
2967 if !left_decomp.is_empty() && left_decomp == right_decomp {
2968 if *left_decomp == duration_decomposition() {
2969 LemmaType::anonymous_for_decomposition(duration_decomposition())
2970 } else {
2971 LemmaType::anonymous_for_decomposition(left_decomp.clone())
2972 }
2973 } else if left_type.is_duration_like_quantity()
2974 && right_type.is_duration_like_quantity()
2975 {
2976 LemmaType::anonymous_for_decomposition(duration_decomposition())
2977 } else {
2978 left_type
2979 }
2980 } else {
2981 left_type
2982 }
2983 }
2984 ArithmeticComputation::Multiply | ArithmeticComputation::Divide => {
2985 let combined = combine_decompositions(
2986 l_decomp,
2987 r_decomp,
2988 matches!(op, ArithmeticComputation::Multiply),
2989 );
2990 if combined.is_empty() {
2991 primitive_number().clone()
2992 } else {
2993 LemmaType::anonymous_for_decomposition(combined)
2994 }
2995 }
2996 _ => primitive_number().clone(),
2997 },
2998
2999 (
3000 TypeSpecification::Number { .. },
3001 TypeSpecification::Quantity {
3002 decomposition: r_decomp,
3003 ..
3004 },
3005 ) => {
3006 match op {
3007 ArithmeticComputation::Multiply => right_type,
3008 ArithmeticComputation::Divide => {
3009 if right_type.is_anonymous_quantity() && !r_decomp.is_empty() {
3013 let negated: BaseQuantityVector =
3014 r_decomp.iter().map(|(k, &e)| (k.clone(), -e)).collect();
3015 LemmaType::anonymous_for_decomposition(negated)
3016 } else {
3017 primitive_number().clone()
3018 }
3019 }
3020 _ => primitive_number().clone(),
3021 }
3022 }
3023
3024 (
3025 TypeSpecification::Calendar { .. },
3026 TypeSpecification::Quantity {
3027 decomposition: r_decomp,
3028 ..
3029 },
3030 ) => match op {
3031 ArithmeticComputation::Multiply | ArithmeticComputation::Divide => {
3032 let cal_decomp = calendar_decomposition();
3033 let combined = combine_decompositions(
3034 &cal_decomp,
3035 r_decomp,
3036 matches!(op, ArithmeticComputation::Multiply),
3037 );
3038 if combined.is_empty() {
3039 primitive_number().clone()
3040 } else {
3041 LemmaType::anonymous_for_decomposition(combined)
3042 }
3043 }
3044 _ => primitive_number().clone(),
3045 },
3046 (TypeSpecification::Calendar { .. }, TypeSpecification::Number { .. }) => left_type,
3047 (TypeSpecification::Calendar { .. }, TypeSpecification::Ratio { .. }) => left_type,
3048 (TypeSpecification::Calendar { .. }, TypeSpecification::Calendar { .. }) => match op {
3049 ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
3050 primitive_calendar().clone()
3051 }
3052 _ => primitive_number().clone(),
3053 },
3054
3055 (TypeSpecification::Number { .. }, TypeSpecification::Calendar { .. }) => match op {
3056 ArithmeticComputation::Multiply => right_type,
3057 _ => primitive_number().clone(),
3058 },
3059
3060 (TypeSpecification::Number { .. }, TypeSpecification::Ratio { .. }) => {
3061 primitive_number().clone()
3062 }
3063 (TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => {
3064 primitive_number().clone()
3065 }
3066
3067 (TypeSpecification::Ratio { .. }, TypeSpecification::Ratio { .. }) => left_type,
3068 (TypeSpecification::DateRange { .. }, TypeSpecification::DateRange { .. }) => match op {
3069 ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
3070 range_span_type(&left_type)
3071 }
3072 _ => LemmaType::undetermined_type(),
3073 },
3074 (TypeSpecification::NumberRange { .. }, TypeSpecification::NumberRange { .. }) => {
3075 match op {
3076 ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
3077 range_span_type(&left_type)
3078 }
3079 _ => LemmaType::undetermined_type(),
3080 }
3081 }
3082 (TypeSpecification::QuantityRange { .. }, TypeSpecification::QuantityRange { .. }) => {
3083 match op {
3084 ArithmeticComputation::Add | ArithmeticComputation::Subtract
3085 if range_matches_range_quantity(&left_type, &right_type) =>
3086 {
3087 range_span_type(&left_type)
3088 }
3089 _ => LemmaType::undetermined_type(),
3090 }
3091 }
3092 (TypeSpecification::RatioRange { .. }, TypeSpecification::RatioRange { .. }) => match op {
3093 ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
3094 range_span_type(&left_type)
3095 }
3096 _ => LemmaType::undetermined_type(),
3097 },
3098 (TypeSpecification::CalendarRange { .. }, TypeSpecification::CalendarRange { .. }) => {
3099 match op {
3100 ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
3101 range_span_type(&left_type)
3102 }
3103 _ => LemmaType::undetermined_type(),
3104 }
3105 }
3106 (TypeSpecification::DateRange { .. }, TypeSpecification::CalendarRange { .. })
3107 | (TypeSpecification::CalendarRange { .. }, TypeSpecification::DateRange { .. })
3108 | (TypeSpecification::Date { .. }, TypeSpecification::CalendarRange { .. })
3109 | (TypeSpecification::CalendarRange { .. }, TypeSpecification::Date { .. }) => {
3110 LemmaType::undetermined_type()
3111 }
3112 (TypeSpecification::DateRange { .. }, TypeSpecification::Calendar { .. }) => match op {
3113 ArithmeticComputation::Add | ArithmeticComputation::Subtract => left_type,
3114 _ => LemmaType::undetermined_type(),
3115 },
3116 (TypeSpecification::Calendar { .. }, TypeSpecification::DateRange { .. }) => match op {
3117 ArithmeticComputation::Add | ArithmeticComputation::Subtract => right_type,
3118 _ => LemmaType::undetermined_type(),
3119 },
3120 (TypeSpecification::CalendarRange { .. }, TypeSpecification::Calendar { .. }) => match op {
3121 ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
3122 range_quantity_type_for_operand(&left_type, &right_type)
3123 }
3124 _ => LemmaType::undetermined_type(),
3125 },
3126 (TypeSpecification::Calendar { .. }, TypeSpecification::CalendarRange { .. }) => match op {
3127 ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
3128 range_quantity_type_for_operand(&right_type, &left_type)
3129 }
3130 _ => LemmaType::undetermined_type(),
3131 },
3132 (TypeSpecification::NumberRange { .. }, TypeSpecification::Number { .. })
3133 | (TypeSpecification::RatioRange { .. }, TypeSpecification::Ratio { .. }) => match op {
3134 ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
3135 range_quantity_type_for_operand(&left_type, &right_type)
3136 }
3137 _ => LemmaType::undetermined_type(),
3138 },
3139 (TypeSpecification::QuantityRange { .. }, TypeSpecification::Quantity { .. }) => match op {
3140 ArithmeticComputation::Add | ArithmeticComputation::Subtract
3141 if range_matches_quantity_type(&left_type, &right_type) =>
3142 {
3143 range_quantity_type_for_operand(&left_type, &right_type)
3144 }
3145 _ => LemmaType::undetermined_type(),
3146 },
3147 (TypeSpecification::Number { .. }, TypeSpecification::NumberRange { .. }) => match op {
3148 ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
3149 range_quantity_type_for_operand(&right_type, &left_type)
3150 }
3151 _ => LemmaType::undetermined_type(),
3152 },
3153 (TypeSpecification::Quantity { .. }, TypeSpecification::QuantityRange { .. }) => match op {
3154 ArithmeticComputation::Add | ArithmeticComputation::Subtract
3155 if range_matches_quantity_type(&right_type, &left_type) =>
3156 {
3157 range_quantity_type_for_operand(&right_type, &left_type)
3158 }
3159 _ => LemmaType::undetermined_type(),
3160 },
3161 (TypeSpecification::Ratio { .. }, TypeSpecification::RatioRange { .. }) => match op {
3162 ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
3163 range_quantity_type_for_operand(&right_type, &left_type)
3164 }
3165 _ => LemmaType::undetermined_type(),
3166 },
3167 (TypeSpecification::DateRange { .. }, TypeSpecification::Quantity { .. })
3168 if right_type.is_duration_like_quantity() =>
3169 {
3170 match op {
3171 ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
3172 range_quantity_type_for_operand(&left_type, &right_type)
3173 }
3174 _ => LemmaType::undetermined_type(),
3175 }
3176 }
3177 (TypeSpecification::Quantity { .. }, TypeSpecification::DateRange { .. })
3178 if left_type.is_duration_like_quantity() =>
3179 {
3180 match op {
3181 ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
3182 range_quantity_type_for_operand(&right_type, &left_type)
3183 }
3184 _ => LemmaType::undetermined_type(),
3185 }
3186 }
3187 (TypeSpecification::DateRange { .. }, TypeSpecification::Number { .. }) => match op {
3188 ArithmeticComputation::Multiply => range_span_type(&left_type),
3189 _ => LemmaType::undetermined_type(),
3190 },
3191 (TypeSpecification::Number { .. }, TypeSpecification::DateRange { .. }) => match op {
3192 ArithmeticComputation::Multiply => range_span_type(&right_type),
3193 _ => LemmaType::undetermined_type(),
3194 },
3195 (
3196 TypeSpecification::Quantity { decomposition, .. },
3197 TypeSpecification::DateRange { .. },
3198 ) => match (op, date_range_projection_axis(&left_type)) {
3199 (ArithmeticComputation::Multiply, Ok(DateRangeProjectionAxis::Duration)) => {
3200 let combined =
3201 combine_decompositions(decomposition, &duration_decomposition(), true);
3202 if combined.is_empty() {
3203 primitive_number().clone()
3204 } else {
3205 LemmaType::anonymous_for_decomposition(combined)
3206 }
3207 }
3208 (ArithmeticComputation::Multiply, Ok(DateRangeProjectionAxis::Calendar)) => {
3209 let combined =
3210 combine_decompositions(decomposition, &calendar_decomposition(), true);
3211 if combined.is_empty() {
3212 primitive_number().clone()
3213 } else {
3214 LemmaType::anonymous_for_decomposition(combined)
3215 }
3216 }
3217 _ => LemmaType::undetermined_type(),
3218 },
3219 (TypeSpecification::DateRange { .. }, TypeSpecification::Quantity { .. }) => {
3220 compute_arithmetic_result_type_recursive(right_type, op, left_type, true)
3221 }
3222
3223 _ => {
3224 if swapped {
3225 LemmaType::undetermined_type()
3226 } else {
3227 compute_arithmetic_result_type_recursive(right_type, op, left_type, true)
3228 }
3229 }
3230 }
3231}
3232
3233fn infer_range_type_from_endpoint_types(
3234 left_type: &LemmaType,
3235 right_type: &LemmaType,
3236) -> LemmaType {
3237 match (&left_type.specifications, &right_type.specifications) {
3238 (TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => {
3239 primitive_date_range().clone()
3240 }
3241 (TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => {
3242 primitive_number_range().clone()
3243 }
3244 (
3245 TypeSpecification::Quantity {
3246 units,
3247 decomposition,
3248 canonical_unit,
3249 ..
3250 },
3251 TypeSpecification::Quantity { .. },
3252 ) if left_type.same_quantity_family(right_type) => {
3253 let mut spec = TypeSpecification::quantity_range();
3254 if let TypeSpecification::QuantityRange {
3255 units: range_units,
3256 decomposition: range_decomposition,
3257 canonical_unit: range_canonical_unit,
3258 ..
3259 } = &mut spec
3260 {
3261 *range_units = units.clone();
3262 *range_decomposition = decomposition.clone();
3263 *range_canonical_unit = canonical_unit.clone();
3264 }
3265 LemmaType::primitive(spec)
3266 }
3267 (TypeSpecification::Ratio { units, .. }, TypeSpecification::Ratio { .. }) => {
3268 let mut spec = TypeSpecification::ratio_range();
3269 if let TypeSpecification::RatioRange {
3270 units: range_units, ..
3271 } = &mut spec
3272 {
3273 *range_units = units.clone();
3274 }
3275 LemmaType::primitive(spec)
3276 }
3277 (TypeSpecification::Calendar { .. }, TypeSpecification::Calendar { .. }) => {
3278 primitive_calendar_range().clone()
3279 }
3280 _ => LemmaType::undetermined_type(),
3281 }
3282}
3283
3284fn range_span_type(range_type: &LemmaType) -> LemmaType {
3285 match &range_type.specifications {
3286 TypeSpecification::DateRange { .. } => {
3287 LemmaType::anonymous_for_decomposition(duration_decomposition())
3288 }
3289 TypeSpecification::NumberRange { .. } => primitive_number().clone(),
3290 TypeSpecification::QuantityRange {
3291 units,
3292 canonical_unit,
3293 ..
3294 } => LemmaType::primitive(TypeSpecification::Quantity {
3295 minimum: None,
3296 maximum: None,
3297 decimals: None,
3298 units: units.clone(),
3299 traits: Vec::new(),
3300 decomposition: BaseQuantityVector::new(),
3303 canonical_unit: canonical_unit.clone(),
3304 help: String::new(),
3305 }),
3306 TypeSpecification::RatioRange { units, .. } => {
3307 LemmaType::primitive(TypeSpecification::Ratio {
3308 minimum: None,
3309 maximum: None,
3310 decimals: None,
3311 units: units.clone(),
3312 help: String::new(),
3313 })
3314 }
3315 TypeSpecification::CalendarRange { .. } => primitive_calendar().clone(),
3316 _ => LemmaType::undetermined_type(),
3317 }
3318}
3319
3320fn range_quantity_type_for_operand(range_type: &LemmaType, other_type: &LemmaType) -> LemmaType {
3321 let _ = other_type;
3322 if range_type.is_range() {
3323 range_type.clone()
3324 } else {
3325 range_span_type(range_type)
3326 }
3327}
3328
3329fn range_matches_quantity_type(range_type: &LemmaType, measure_type: &LemmaType) -> bool {
3330 match &range_type.specifications {
3331 TypeSpecification::DateRange { .. } => {
3332 measure_type.is_duration_like() || measure_type.is_calendar()
3333 }
3334 TypeSpecification::NumberRange { .. } => measure_type.is_number(),
3335 TypeSpecification::QuantityRange { .. } => {
3336 measure_type.is_quantity() && quantity_range_matches_quantity(range_type, measure_type)
3337 }
3338 TypeSpecification::RatioRange { .. } => measure_type.is_ratio(),
3339 TypeSpecification::CalendarRange { .. } => measure_type.is_calendar(),
3340 _ => false,
3341 }
3342}
3343
3344fn range_matches_range_quantity(left_range: &LemmaType, right_range: &LemmaType) -> bool {
3345 let right_measure_type = range_span_type(right_range);
3346 !right_measure_type.is_undetermined()
3347 && range_matches_quantity_type(left_range, &right_measure_type)
3348}
3349
3350#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3351enum DateRangeProjectionAxis {
3352 Duration,
3353 Calendar,
3354}
3355
3356fn date_range_projection_axis(
3357 quantity_type: &LemmaType,
3358) -> Result<DateRangeProjectionAxis, String> {
3359 let quantity_decomposition = match &quantity_type.specifications {
3360 TypeSpecification::Quantity { decomposition, .. } => decomposition,
3361 _ => {
3362 return Err(format!(
3363 "Cannot project date range through non-quantity type {}.",
3364 quantity_type.name()
3365 ));
3366 }
3367 };
3368
3369 let has_duration_axis = quantity_decomposition
3370 .get(semantics::DURATION_DIMENSION)
3371 .is_some_and(|exponent| *exponent != 0);
3372 let has_calendar_axis = quantity_decomposition
3373 .get(semantics::CALENDAR_DIMENSION)
3374 .is_some_and(|exponent| *exponent != 0);
3375
3376 match (has_duration_axis, has_calendar_axis) {
3377 (true, false) => Ok(DateRangeProjectionAxis::Duration),
3378 (false, true) => Ok(DateRangeProjectionAxis::Calendar),
3379 (false, false) => Err(format!(
3380 "Cannot multiply {} by a date range because {} has no duration or calendar dimension.",
3381 quantity_type.name(),
3382 quantity_type.name()
3383 )),
3384 (true, true) => Err(format!(
3385 "Cannot multiply {} by a date range because {} has both duration and calendar dimensions.",
3386 quantity_type.name(),
3387 quantity_type.name()
3388 )),
3389 }
3390}
3391
3392fn quantity_range_matches_quantity(range_type: &LemmaType, quantity_type: &LemmaType) -> bool {
3393 match (&range_type.specifications, &quantity_type.specifications) {
3394 (
3395 TypeSpecification::QuantityRange {
3396 units: range_units,
3397 decomposition: range_decomposition,
3398 canonical_unit: range_canonical_unit,
3399 ..
3400 },
3401 TypeSpecification::Quantity {
3402 units: quantity_units,
3403 decomposition: quantity_decomposition,
3404 canonical_unit: quantity_canonical_unit,
3405 ..
3406 },
3407 ) => {
3408 if range_units.0.is_empty()
3409 && range_decomposition.is_empty()
3410 && range_canonical_unit.is_empty()
3411 {
3412 true
3413 } else if quantity_decomposition.is_empty() {
3414 range_units == quantity_units && range_canonical_unit == quantity_canonical_unit
3415 } else {
3416 range_units == quantity_units
3417 && range_decomposition == quantity_decomposition
3418 && range_canonical_unit == quantity_canonical_unit
3419 }
3420 }
3421 _ => false,
3422 }
3423}
3424
3425fn infer_expression_type(
3432 expression: &Expression,
3433 graph: &Graph,
3434 computed_rule_types: &HashMap<RulePath, LemmaType>,
3435 resolved_types: &ResolvedTypesMap,
3436 spec_arc: &Arc<LemmaSpec>,
3437) -> LemmaType {
3438 match &expression.kind {
3439 ExpressionKind::Literal(literal_value) => literal_value.as_ref().get_type().clone(),
3440
3441 ExpressionKind::DataPath(data_path) => {
3442 infer_data_type(data_path, graph, computed_rule_types)
3443 }
3444
3445 ExpressionKind::RulePath(rule_path) => computed_rule_types
3446 .get(rule_path)
3447 .cloned()
3448 .unwrap_or_else(LemmaType::undetermined_type),
3449
3450 ExpressionKind::LogicalAnd(left, right) => {
3451 let left_type =
3452 infer_expression_type(left, graph, computed_rule_types, resolved_types, spec_arc);
3453 let right_type =
3454 infer_expression_type(right, graph, computed_rule_types, resolved_types, spec_arc);
3455 if left_type.vetoed() || right_type.vetoed() {
3456 return LemmaType::veto_type();
3457 }
3458 if left_type.is_undetermined() || right_type.is_undetermined() {
3459 return LemmaType::undetermined_type();
3460 }
3461 if !left_type.is_boolean() {
3462 return LemmaType::undetermined_type();
3463 }
3464 if right_type.is_boolean() {
3465 primitive_boolean().clone()
3466 } else {
3467 right_type
3468 }
3469 }
3470
3471 ExpressionKind::LogicalOr(left, right) => {
3472 let left_type =
3473 infer_expression_type(left, graph, computed_rule_types, resolved_types, spec_arc);
3474 let right_type =
3475 infer_expression_type(right, graph, computed_rule_types, resolved_types, spec_arc);
3476 if left_type.vetoed() || right_type.vetoed() {
3477 return LemmaType::veto_type();
3478 }
3479 if left_type.is_undetermined() || right_type.is_undetermined() {
3480 return LemmaType::undetermined_type();
3481 }
3482 if left_type.is_boolean() && right_type.is_boolean() {
3483 return primitive_boolean().clone();
3484 }
3485 if left_type == right_type {
3486 return left_type;
3487 }
3488 LemmaType::undetermined_type()
3489 }
3490
3491 ExpressionKind::LogicalNegation(operand, _) => {
3492 let operand_type = infer_expression_type(
3493 operand,
3494 graph,
3495 computed_rule_types,
3496 resolved_types,
3497 spec_arc,
3498 );
3499 if operand_type.vetoed() {
3500 return LemmaType::veto_type();
3501 }
3502 if operand_type.is_undetermined() {
3503 return LemmaType::undetermined_type();
3504 }
3505 primitive_boolean().clone()
3506 }
3507
3508 ExpressionKind::Comparison(left, _op, right) => {
3509 let left_type =
3510 infer_expression_type(left, graph, computed_rule_types, resolved_types, spec_arc);
3511 let right_type =
3512 infer_expression_type(right, graph, computed_rule_types, resolved_types, spec_arc);
3513 if left_type.vetoed() || right_type.vetoed() {
3514 return LemmaType::veto_type();
3515 }
3516 if left_type.is_undetermined() || right_type.is_undetermined() {
3517 return LemmaType::undetermined_type();
3518 }
3519 primitive_boolean().clone()
3520 }
3521
3522 ExpressionKind::Arithmetic(left, operator, right) => {
3523 let left_type =
3524 infer_expression_type(left, graph, computed_rule_types, resolved_types, spec_arc);
3525 let right_type =
3526 infer_expression_type(right, graph, computed_rule_types, resolved_types, spec_arc);
3527 let mut result =
3528 compute_arithmetic_result_type(left_type.clone(), operator, right_type.clone());
3529 if *operator == ArithmeticComputation::Subtract
3530 && left_type.is_time()
3531 && right_type.is_time()
3532 && result.is_anonymous_quantity()
3533 && *result.quantity_type_decomposition() == duration_decomposition()
3534 {
3535 if let Some(duration_type) = find_duration_type_in_spec(resolved_types, spec_arc) {
3536 result = duration_type;
3537 }
3538 }
3539 result
3540 }
3541
3542 ExpressionKind::UnitConversion(source_expression, target) => {
3543 let source_type = infer_expression_type(
3544 source_expression,
3545 graph,
3546 computed_rule_types,
3547 resolved_types,
3548 spec_arc,
3549 );
3550 if source_type.vetoed() {
3551 return LemmaType::veto_type();
3552 }
3553 if source_type.is_undetermined() {
3554 return LemmaType::undetermined_type();
3555 }
3556 if source_type.is_range() {
3557 let span_type = range_span_type(&source_type);
3558 return match target {
3559 SemanticConversionTarget::Number => primitive_number().clone(),
3560 SemanticConversionTarget::Calendar(_) => primitive_calendar().clone(),
3561 SemanticConversionTarget::QuantityUnit(unit_name) => {
3562 find_types_by_spec(resolved_types, spec_arc)
3563 .and_then(|dt| dt.unit_index.get(unit_name))
3564 .cloned()
3565 .unwrap_or(span_type)
3566 }
3567 SemanticConversionTarget::RatioUnit(unit_name) => {
3568 find_types_by_spec(resolved_types, spec_arc)
3569 .and_then(|dt| dt.unit_index.get(unit_name))
3570 .cloned()
3571 .unwrap_or(span_type)
3572 }
3573 };
3574 }
3575 match target {
3576 SemanticConversionTarget::Number => primitive_number().clone(),
3577 SemanticConversionTarget::Calendar(_) => primitive_calendar().clone(),
3578 SemanticConversionTarget::QuantityUnit(unit_name) => {
3579 if source_type.is_number()
3580 || source_type.is_duration_like()
3581 || source_type.is_date_range()
3582 || source_type.is_anonymous_quantity()
3583 {
3584 find_types_by_spec(resolved_types, spec_arc)
3585 .and_then(|dt| dt.unit_index.get(unit_name))
3586 .cloned()
3587 .unwrap_or_else(LemmaType::undetermined_type)
3588 } else {
3589 source_type
3590 }
3591 }
3592 SemanticConversionTarget::RatioUnit(unit_name) => {
3593 if source_type.is_number() {
3594 find_types_by_spec(resolved_types, spec_arc)
3595 .and_then(|dt| dt.unit_index.get(unit_name))
3596 .cloned()
3597 .unwrap_or_else(LemmaType::undetermined_type)
3598 } else {
3599 source_type
3600 }
3601 }
3602 }
3603 }
3604
3605 ExpressionKind::MathematicalComputation(_, operand) => {
3606 let operand_type = infer_expression_type(
3607 operand,
3608 graph,
3609 computed_rule_types,
3610 resolved_types,
3611 spec_arc,
3612 );
3613 if operand_type.vetoed() {
3614 return LemmaType::veto_type();
3615 }
3616 if operand_type.is_undetermined() {
3617 return LemmaType::undetermined_type();
3618 }
3619 primitive_number().clone()
3620 }
3621
3622 ExpressionKind::Veto(_) => LemmaType::veto_type(),
3623
3624 ExpressionKind::ResultIsVeto(operand) => {
3625 let _ = infer_expression_type(
3626 operand,
3627 graph,
3628 computed_rule_types,
3629 resolved_types,
3630 spec_arc,
3631 );
3632 primitive_boolean().clone()
3633 }
3634
3635 ExpressionKind::Now => primitive_date().clone(),
3636
3637 ExpressionKind::DateRelative(..)
3638 | ExpressionKind::DateCalendar(..)
3639 | ExpressionKind::RangeContainment(..) => primitive_boolean().clone(),
3640
3641 ExpressionKind::RangeLiteral(left, right) => {
3642 let left_type =
3643 infer_expression_type(left, graph, computed_rule_types, resolved_types, spec_arc);
3644 let right_type =
3645 infer_expression_type(right, graph, computed_rule_types, resolved_types, spec_arc);
3646 if left_type.vetoed() || right_type.vetoed() {
3647 return LemmaType::veto_type();
3648 }
3649 if left_type.is_undetermined() || right_type.is_undetermined() {
3650 return LemmaType::undetermined_type();
3651 }
3652 infer_range_type_from_endpoint_types(&left_type, &right_type)
3653 }
3654
3655 ExpressionKind::PastFutureRange(..) => primitive_date_range().clone(),
3656 }
3657}
3658
3659fn infer_data_type(
3668 data_path: &DataPath,
3669 graph: &Graph,
3670 computed_rule_types: &HashMap<RulePath, LemmaType>,
3671) -> LemmaType {
3672 let entry = match graph.data().get(data_path) {
3673 Some(e) => e,
3674 None => return LemmaType::undetermined_type(),
3675 };
3676 match entry {
3677 DataDefinition::Value { value, .. } => value.lemma_type.clone(),
3678 DataDefinition::TypeDeclaration { resolved_type, .. } => resolved_type.clone(),
3679 DataDefinition::Reference {
3680 target: ReferenceTarget::Rule(target_rule),
3681 resolved_type,
3682 ..
3683 } => {
3684 if !resolved_type.is_undetermined() {
3685 resolved_type.clone()
3686 } else {
3687 computed_rule_types
3688 .get(target_rule)
3689 .cloned()
3690 .unwrap_or_else(LemmaType::undetermined_type)
3691 }
3692 }
3693 DataDefinition::Reference { resolved_type, .. } => resolved_type.clone(),
3694 DataDefinition::Import { .. } => LemmaType::undetermined_type(),
3695 }
3696}
3697
3698fn collect_rule_reference_dependencies(
3705 expression: &Expression,
3706 reference_to_rule: &HashMap<DataPath, RulePath>,
3707 out: &mut BTreeSet<RulePath>,
3708) {
3709 let mut paths: HashSet<DataPath> = HashSet::new();
3710 expression.kind.collect_data_paths(&mut paths);
3711 for path in paths {
3712 if let Some(target_rule) = reference_to_rule.get(&path) {
3713 out.insert(target_rule.clone());
3714 }
3715 }
3716}
3717
3718fn engine_error_at_graph(graph: &Graph, source: &Source, message: impl Into<String>) -> Error {
3723 Error::validation_with_context(
3724 message.into(),
3725 Some(source.clone()),
3726 None::<String>,
3727 Some(Arc::clone(&graph.main_spec)),
3728 None,
3729 )
3730}
3731
3732fn check_logical_operands(
3733 graph: &Graph,
3734 left_type: &LemmaType,
3735 right_type: &LemmaType,
3736 source: &Source,
3737) -> Result<(), Vec<Error>> {
3738 if left_type.vetoed() || right_type.vetoed() {
3739 return Ok(());
3740 }
3741 let mut errors = Vec::new();
3742 if !left_type.is_boolean() {
3743 errors.push(engine_error_at_graph(
3744 graph,
3745 source,
3746 format!(
3747 "Logical operation requires boolean operands, got {:?} for left operand",
3748 left_type
3749 ),
3750 ));
3751 }
3752 if !right_type.is_boolean() {
3753 errors.push(engine_error_at_graph(
3754 graph,
3755 source,
3756 format!(
3757 "Logical operation requires boolean operands, got {:?} for right operand",
3758 right_type
3759 ),
3760 ));
3761 }
3762 if errors.is_empty() {
3763 Ok(())
3764 } else {
3765 Err(errors)
3766 }
3767}
3768
3769fn check_logical_or_operands(
3770 graph: &Graph,
3771 left_type: &LemmaType,
3772 right_type: &LemmaType,
3773 source: &Source,
3774) -> Result<(), Vec<Error>> {
3775 if left_type.vetoed() || right_type.vetoed() {
3776 return Ok(());
3777 }
3778 if left_type.is_undetermined() || right_type.is_undetermined() {
3779 return Ok(());
3780 }
3781 if left_type.is_boolean() && right_type.is_boolean() {
3782 return check_logical_operands(graph, left_type, right_type, source);
3783 }
3784 if left_type == right_type {
3785 return Ok(());
3786 }
3787 Err(vec![engine_error_at_graph(
3788 graph,
3789 source,
3790 format!(
3791 "Logical OR requires matching types (unless-chain / De Morgan), got {:?} and {:?}",
3792 left_type, right_type
3793 ),
3794 )])
3795}
3796
3797fn check_logical_and_operands(
3798 graph: &Graph,
3799 left_type: &LemmaType,
3800 right_type: &LemmaType,
3801 source: &Source,
3802) -> Result<(), Vec<Error>> {
3803 if left_type.vetoed() || right_type.vetoed() {
3804 return Ok(());
3805 }
3806 if !left_type.is_boolean() {
3807 return Err(vec![engine_error_at_graph(
3808 graph,
3809 source,
3810 format!(
3811 "Logical AND requires boolean left operand, got {:?}",
3812 left_type
3813 ),
3814 )]);
3815 }
3816 if right_type.is_boolean() {
3817 return Ok(());
3818 }
3819 Ok(())
3820}
3821
3822fn check_logical_operand(
3823 graph: &Graph,
3824 operand_type: &LemmaType,
3825 source: &Source,
3826) -> Result<(), Vec<Error>> {
3827 if operand_type.vetoed() {
3828 return Ok(());
3829 }
3830 if !operand_type.is_boolean() {
3831 Err(vec![engine_error_at_graph(
3832 graph,
3833 source,
3834 format!(
3835 "Logical negation requires boolean operand, got {:?}",
3836 operand_type
3837 ),
3838 )])
3839 } else {
3840 Ok(())
3841 }
3842}
3843
3844fn check_comparison_types(
3845 graph: &Graph,
3846 left_type: &LemmaType,
3847 op: &ComparisonComputation,
3848 right_type: &LemmaType,
3849 source: &Source,
3850) -> Result<(), Vec<Error>> {
3851 if left_type.vetoed() || right_type.vetoed() {
3852 return Ok(());
3853 }
3854 let is_equality_only = matches!(op, ComparisonComputation::Is | ComparisonComputation::IsNot);
3855
3856 if left_type.is_range() {
3857 if range_matches_quantity_type(left_type, right_type) {
3858 return Ok(());
3859 }
3860 return Err(vec![engine_error_at_graph(
3861 graph,
3862 source,
3863 format!("Cannot compare {:?} with {:?}", left_type, right_type),
3864 )]);
3865 }
3866
3867 if left_type.is_boolean() && right_type.is_boolean() {
3868 if !is_equality_only {
3869 return Err(vec![engine_error_at_graph(
3870 graph,
3871 source,
3872 format!("Can only use 'is' and 'is not' with booleans (got {})", op),
3873 )]);
3874 }
3875 return Ok(());
3876 }
3877
3878 if left_type.is_text() && right_type.is_text() {
3879 if !is_equality_only {
3880 return Err(vec![engine_error_at_graph(
3881 graph,
3882 source,
3883 format!("Can only use 'is' and 'is not' with text (got {})", op),
3884 )]);
3885 }
3886 return Ok(());
3887 }
3888
3889 if left_type.is_number() && right_type.is_number() {
3890 return Ok(());
3891 }
3892
3893 if left_type.is_ratio() && right_type.is_ratio() {
3894 return Ok(());
3895 }
3896
3897 if left_type.is_date() && right_type.is_date() {
3898 return Ok(());
3899 }
3900
3901 if left_type.is_time() && right_type.is_time() {
3902 return Ok(());
3903 }
3904
3905 if left_type.is_quantity() && right_type.is_quantity() {
3906 if !left_type.same_quantity_family(right_type)
3907 && !left_type.compatible_with_anonymous_quantity(right_type)
3908 {
3909 return Err(vec![engine_error_at_graph(
3910 graph,
3911 source,
3912 format!(
3913 "Cannot compare unrelated quantity types: {} and {}",
3914 left_type.name(),
3915 right_type.name()
3916 ),
3917 )]);
3918 }
3919 return Ok(());
3920 }
3921
3922 if left_type.is_duration_like() && right_type.is_duration_like() {
3923 return Ok(());
3924 }
3925 if left_type.is_calendar() && right_type.is_calendar() {
3926 return Ok(());
3927 }
3928 if left_type.is_calendar() && right_type.is_number() {
3929 return Ok(());
3930 }
3931 if left_type.is_number() && right_type.is_calendar() {
3932 return Ok(());
3933 }
3934
3935 Err(vec![engine_error_at_graph(
3936 graph,
3937 source,
3938 format!("Cannot compare {:?} with {:?}", left_type, right_type),
3939 )])
3940}
3941
3942fn arithmetic_literal_zero_divisor_planning_errors(
3944 graph: &Graph,
3945 right: &Expression,
3946 operator: &ArithmeticComputation,
3947 source: &Source,
3948) -> Result<(), Vec<Error>> {
3949 if !matches!(
3950 operator,
3951 ArithmeticComputation::Divide | ArithmeticComputation::Modulo
3952 ) {
3953 return Ok(());
3954 }
3955
3956 if let ExpressionKind::Literal(literal) = &right.kind {
3957 if let ValueKind::Number(number) = &literal.value {
3958 if crate::computation::rational::rational_is_zero(number) {
3959 return Err(vec![engine_error_at_graph(
3960 graph,
3961 source,
3962 format!("Cannot apply '{}' with a zero divisor literal.", operator),
3963 )]);
3964 }
3965 }
3966 }
3967
3968 Ok(())
3969}
3970
3971fn arithmetic_power_exponent_planning_errors(
3972 graph: &Graph,
3973 _left: &Expression,
3974 right: &Expression,
3975 left_type: &LemmaType,
3976 _right_type: &LemmaType,
3977 operator: &ArithmeticComputation,
3978 source: &Source,
3979) -> Result<(), Vec<Error>> {
3980 if *operator != ArithmeticComputation::Power {
3981 return Ok(());
3982 }
3983 if left_type.is_quantity() || left_type.is_duration_like() {
3986 let is_integer_literal = if let ExpressionKind::Literal(lit) = &right.kind {
3987 if let crate::planning::semantics::ValueKind::Number(n) = &lit.value {
3988 *n.denom() == 1
3989 } else {
3990 false
3991 }
3992 } else {
3993 false
3994 };
3995 if !is_integer_literal {
3996 return Err(vec![engine_error_at_graph(
3997 graph,
3998 source,
3999 "Cannot raise a quantity value to a fractional or variable exponent. Use a positive integer literal.".to_string(),
4000 )]);
4001 }
4002 }
4003 Ok(())
4004}
4005
4006fn arithmetic_plan_time_exactness_planning_errors(
4009 graph: &Graph,
4010 left: &Expression,
4011 right: &Expression,
4012 left_type: &LemmaType,
4013 right_type: &LemmaType,
4014 operator: &ArithmeticComputation,
4015 source: &Source,
4016) -> Result<(), Vec<Error>> {
4017 if left_type.vetoed() || right_type.vetoed() {
4018 return Ok(());
4019 }
4020 if left_type.is_undetermined() || right_type.is_undetermined() {
4021 return Ok(());
4022 }
4023
4024 let mut errors = Vec::new();
4025 let collect = |result: Result<(), Vec<Error>>, errors: &mut Vec<Error>| {
4026 if let Err(mut errs) = result {
4027 errors.append(&mut errs);
4028 }
4029 };
4030
4031 collect(
4032 arithmetic_literal_zero_divisor_planning_errors(graph, right, operator, source),
4033 &mut errors,
4034 );
4035 collect(
4036 arithmetic_power_exponent_planning_errors(
4037 graph, left, right, left_type, right_type, operator, source,
4038 ),
4039 &mut errors,
4040 );
4041
4042 if errors.is_empty() {
4043 Ok(())
4044 } else {
4045 Err(errors)
4046 }
4047}
4048
4049fn check_arithmetic_types(
4050 graph: &Graph,
4051 left_type: &LemmaType,
4052 right_type: &LemmaType,
4053 operator: &ArithmeticComputation,
4054 source: &Source,
4055) -> Result<(), Vec<Error>> {
4056 if left_type.vetoed() || right_type.vetoed() {
4057 return Ok(());
4058 }
4059
4060 if left_type.is_date() && right_type.is_date() && *operator == ArithmeticComputation::Subtract {
4061 return Err(vec![engine_error_at_graph(
4062 graph,
4063 source,
4064 "Cannot subtract dates. Use dateA...dateB to create a date range.".to_string(),
4065 )]);
4066 }
4067
4068 if left_type.is_range() || right_type.is_range() {
4069 let range_measure_allowed = matches!(
4070 operator,
4071 ArithmeticComputation::Add | ArithmeticComputation::Subtract
4072 ) && ((left_type.is_range()
4073 && right_type.is_range()
4074 && range_matches_range_quantity(left_type, right_type))
4075 || (left_type.is_range()
4076 && !right_type.is_range()
4077 && range_matches_quantity_type(left_type, right_type))
4078 || (right_type.is_range()
4079 && !left_type.is_range()
4080 && range_matches_quantity_type(right_type, left_type)));
4081 let range_scalar_multiply_allowed = *operator == ArithmeticComputation::Multiply
4082 && ((left_type.is_range() && right_type.is_number())
4083 || (right_type.is_range() && left_type.is_number()));
4084
4085 let quantity_date_range_allowed = if *operator == ArithmeticComputation::Multiply {
4086 if left_type.is_quantity() && right_type.is_date_range() {
4087 match date_range_projection_axis(left_type) {
4088 Ok(_) => true,
4089 Err(message) => {
4090 return Err(vec![engine_error_at_graph(graph, source, message)]);
4091 }
4092 }
4093 } else if left_type.is_date_range() && right_type.is_quantity() {
4094 match date_range_projection_axis(right_type) {
4095 Ok(_) => true,
4096 Err(message) => {
4097 return Err(vec![engine_error_at_graph(graph, source, message)]);
4098 }
4099 }
4100 } else {
4101 false
4102 }
4103 } else {
4104 false
4105 };
4106
4107 if range_measure_allowed || range_scalar_multiply_allowed || quantity_date_range_allowed {
4108 return Ok(());
4109 }
4110
4111 if (left_type.is_date_range()
4112 && right_type.is_quantity()
4113 && !right_type.is_duration_like_quantity())
4114 || (right_type.is_date_range()
4115 && left_type.is_quantity()
4116 && !left_type.is_duration_like_quantity())
4117 {
4118 return Err(vec![engine_error_at_graph(
4119 graph,
4120 source,
4121 format!(
4122 "Cannot apply '{}' to a date range and an unrelated quantity.",
4123 operator
4124 ),
4125 )]);
4126 }
4127
4128 return Err(vec![engine_error_at_graph(
4129 graph,
4130 source,
4131 format!(
4132 "Cannot apply '{}' to {} and {}.",
4133 operator,
4134 left_type.name(),
4135 right_type.name()
4136 ),
4137 )]);
4138 }
4139
4140 if left_type.is_date() || left_type.is_time() || right_type.is_date() || right_type.is_time() {
4142 let left_is_duration_like = left_type.is_duration_like();
4143 let right_is_duration_like = right_type.is_duration_like();
4144 let valid = matches!(
4145 (
4146 left_type.is_date(),
4147 left_type.is_time(),
4148 right_type.is_date(),
4149 right_type.is_time(),
4150 left_is_duration_like,
4151 right_is_duration_like,
4152 left_type.is_calendar(),
4153 right_type.is_calendar(),
4154 operator
4155 ),
4156 (
4157 true,
4158 _,
4159 _,
4160 true,
4161 _,
4162 _,
4163 _,
4164 _,
4165 ArithmeticComputation::Subtract
4166 ) | (
4167 _,
4168 true,
4169 _,
4170 true,
4171 _,
4172 _,
4173 _,
4174 _,
4175 ArithmeticComputation::Subtract
4176 ) | (
4177 true,
4178 _,
4179 _,
4180 _,
4181 _,
4182 true,
4183 _,
4184 _,
4185 ArithmeticComputation::Add | ArithmeticComputation::Subtract
4186 ) | (
4187 true,
4188 _,
4189 _,
4190 _,
4191 _,
4192 _,
4193 _,
4194 true,
4195 ArithmeticComputation::Add | ArithmeticComputation::Subtract
4196 ) | (_, _, true, _, true, _, _, _, ArithmeticComputation::Add)
4197 | (_, _, true, _, _, _, true, _, ArithmeticComputation::Add)
4198 | (
4199 _,
4200 true,
4201 _,
4202 _,
4203 _,
4204 true,
4205 _,
4206 _,
4207 ArithmeticComputation::Add | ArithmeticComputation::Subtract
4208 )
4209 | (_, _, _, true, true, _, _, _, ArithmeticComputation::Add)
4210 );
4211 if !valid {
4212 return Err(vec![engine_error_at_graph(
4213 graph,
4214 source,
4215 format!(
4216 "Cannot apply '{}' to {} and {}.",
4217 operator,
4218 left_type.name(),
4219 right_type.name()
4220 ),
4221 )]);
4222 }
4223 return Ok(());
4224 }
4225
4226 if left_type.is_quantity() && right_type.is_quantity() {
4232 return match operator {
4233 ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
4234 if left_type.same_quantity_family(right_type)
4235 || left_type.compatible_with_anonymous_quantity(right_type)
4236 {
4237 Ok(())
4238 } else {
4239 Err(vec![engine_error_at_graph(
4240 graph,
4241 source,
4242 format!(
4243 "Cannot {} unrelated quantity types: {} and {}.",
4244 if matches!(operator, ArithmeticComputation::Add) {
4245 "add"
4246 } else {
4247 "subtract"
4248 },
4249 left_type.name(),
4250 right_type.name()
4251 ),
4252 )])
4253 }
4254 }
4255 ArithmeticComputation::Multiply => {
4256 if left_type.same_quantity_family(right_type) {
4257 Err(vec![engine_error_at_graph(
4258 graph,
4259 source,
4260 format!(
4261 "Cannot multiply two '{}' quantity values of the same type. \
4262 Convert operands first: 'value as number'.",
4263 left_type.name()
4264 ),
4265 )])
4266 } else {
4267 Ok(())
4269 }
4270 }
4271 ArithmeticComputation::Divide => {
4272 Ok(())
4274 }
4275 ArithmeticComputation::Modulo | ArithmeticComputation::Power => {
4276 Err(vec![engine_error_at_graph(
4277 graph,
4278 source,
4279 format!(
4280 "Cannot apply '{}' to two quantity values ({} and {}).",
4281 operator,
4282 left_type.name(),
4283 right_type.name()
4284 ),
4285 )])
4286 }
4287 };
4288 }
4289
4290 if left_type.is_duration_like() && right_type.is_duration_like() {
4292 return match operator {
4293 ArithmeticComputation::Add | ArithmeticComputation::Subtract => Ok(()),
4294 ArithmeticComputation::Divide => Ok(()),
4295 _ => Err(vec![engine_error_at_graph(
4296 graph,
4297 source,
4298 "Cannot multiply two duration values. Convert operands first: 'value as number'."
4299 .to_string(),
4300 )]),
4301 };
4302 }
4303
4304 if left_type.is_calendar() && right_type.is_calendar() {
4305 return match operator {
4306 ArithmeticComputation::Add | ArithmeticComputation::Subtract => Ok(()),
4307 ArithmeticComputation::Divide => Ok(()),
4308 _ => Err(vec![engine_error_at_graph(
4309 graph,
4310 source,
4311 "Cannot multiply two calendar values. Convert operands first: 'value as number'."
4312 .to_string(),
4313 )]),
4314 };
4315 }
4316
4317 if (left_type.is_duration_like() && right_type.is_calendar())
4318 || (left_type.is_calendar() && right_type.is_duration_like())
4319 {
4320 return Err(vec![engine_error_at_graph(
4321 graph,
4322 source,
4323 format!(
4324 "Cannot apply '{}' to {} and {}. Duration and calendar are unrelated types.",
4325 operator,
4326 left_type.name(),
4327 right_type.name()
4328 ),
4329 )]);
4330 }
4331
4332 let left_valid = left_type.is_quantity()
4334 || left_type.is_number()
4335 || left_type.is_duration_like()
4336 || left_type.is_calendar()
4337 || left_type.is_ratio();
4338 let right_valid = right_type.is_quantity()
4339 || right_type.is_number()
4340 || right_type.is_duration_like()
4341 || right_type.is_calendar()
4342 || right_type.is_ratio();
4343
4344 if !left_valid || !right_valid {
4345 return Err(vec![engine_error_at_graph(
4346 graph,
4347 source,
4348 format!(
4349 "Cannot apply '{}' to {} and {}.",
4350 operator,
4351 left_type.name(),
4352 right_type.name()
4353 ),
4354 )]);
4355 }
4356
4357 if left_type.has_same_base_type(right_type) {
4359 return Ok(());
4360 }
4361
4362 let pair = |a: fn(&LemmaType) -> bool, b: fn(&LemmaType) -> bool| {
4363 (a(left_type) && b(right_type)) || (b(left_type) && a(right_type))
4364 };
4365
4366 let allowed = match operator {
4367 ArithmeticComputation::Multiply => {
4368 pair(LemmaType::is_quantity, LemmaType::is_number)
4369 || pair(LemmaType::is_quantity, LemmaType::is_ratio)
4370 || pair(LemmaType::is_quantity, LemmaType::is_duration_like_quantity)
4371 || pair(LemmaType::is_quantity, LemmaType::is_calendar)
4372 || pair(LemmaType::is_duration_like_quantity, LemmaType::is_number)
4373 || pair(LemmaType::is_duration_like_quantity, LemmaType::is_ratio)
4374 || pair(LemmaType::is_calendar, LemmaType::is_number)
4375 || pair(LemmaType::is_calendar, LemmaType::is_ratio)
4376 || pair(LemmaType::is_number, LemmaType::is_ratio)
4377 }
4378 ArithmeticComputation::Divide => {
4379 pair(LemmaType::is_quantity, LemmaType::is_number)
4380 || pair(LemmaType::is_quantity, LemmaType::is_ratio)
4381 || pair(LemmaType::is_quantity, LemmaType::is_duration_like_quantity)
4382 || pair(LemmaType::is_quantity, LemmaType::is_calendar)
4383 || (left_type.is_duration_like() && right_type.is_number())
4384 || (left_type.is_duration_like() && right_type.is_ratio())
4385 || (left_type.is_calendar() && right_type.is_number())
4386 || (left_type.is_calendar() && right_type.is_ratio())
4387 || (left_type.is_number() && right_type.is_duration_like())
4388 || (left_type.is_number() && right_type.is_calendar())
4389 || pair(LemmaType::is_number, LemmaType::is_ratio)
4390 }
4391 ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
4392 pair(LemmaType::is_quantity, LemmaType::is_number)
4393 || pair(LemmaType::is_quantity, LemmaType::is_ratio)
4394 || pair(LemmaType::is_duration_like_quantity, LemmaType::is_number)
4395 || pair(LemmaType::is_duration_like_quantity, LemmaType::is_ratio)
4396 || pair(LemmaType::is_calendar, LemmaType::is_number)
4397 || pair(LemmaType::is_calendar, LemmaType::is_ratio)
4398 || pair(LemmaType::is_number, LemmaType::is_ratio)
4399 }
4400 ArithmeticComputation::Power => {
4401 let left_ok = left_type.is_number()
4405 || left_type.is_quantity()
4406 || left_type.is_ratio()
4407 || left_type.is_duration_like();
4408 let right_ok = if left_type.is_quantity() || left_type.is_duration_like() {
4409 right_type.is_number()
4410 } else {
4411 right_type.is_number() || right_type.is_ratio()
4412 };
4413 left_ok && right_ok
4414 }
4415 ArithmeticComputation::Modulo => {
4416 if left_type.is_quantity() && right_type.is_ratio() {
4419 return Err(vec![engine_error_at_graph(
4420 graph,
4421 source,
4422 format!(
4423 "Cannot apply modulo to {} with a ratio. Use a number divisor.",
4424 left_type.name()
4425 ),
4426 )]);
4427 }
4428 right_type.is_number() || right_type.is_ratio()
4429 }
4430 };
4431
4432 if !allowed {
4433 return Err(vec![engine_error_at_graph(
4434 graph,
4435 source,
4436 format!(
4437 "Cannot apply '{}' to {} and {}.",
4438 operator,
4439 left_type.name(),
4440 right_type.name(),
4441 ),
4442 )]);
4443 }
4444
4445 Ok(())
4446}
4447
4448fn check_range_span_unit_conversion(
4449 graph: &Graph,
4450 source_type: &LemmaType,
4451 target: &SemanticConversionTarget,
4452 resolved_types: &ResolvedTypesMap,
4453 source: &Source,
4454 spec_arc: &Arc<LemmaSpec>,
4455) -> Result<(), Vec<Error>> {
4456 match target {
4457 SemanticConversionTarget::Number => {
4458 if source_type.is_number_range() {
4459 Ok(())
4460 } else {
4461 Err(vec![engine_error_at_graph(
4462 graph,
4463 source,
4464 format!("Cannot convert {} to number.", source_type.name()),
4465 )])
4466 }
4467 }
4468 SemanticConversionTarget::QuantityUnit(unit_name) => {
4469 if source_type.is_calendar_range() {
4470 return Err(vec![engine_error_at_graph(
4471 graph,
4472 source,
4473 format!(
4474 "Cannot convert {} to quantity unit '{}'.",
4475 source_type.name(),
4476 unit_name
4477 ),
4478 )]);
4479 }
4480 let target_type = find_types_by_spec(resolved_types, spec_arc)
4481 .and_then(|dt| dt.unit_index.get(unit_name))
4482 .cloned();
4483 let target_type = match target_type {
4484 Some(lemma_type) => lemma_type,
4485 None => {
4486 return Err(vec![engine_error_at_graph(
4487 graph,
4488 source,
4489 format!(
4490 "Unknown unit '{}': no quantity type in spec '{}' owns this unit.",
4491 unit_name, spec_arc.name
4492 ),
4493 )]);
4494 }
4495 };
4496 if !target_type.is_duration_like_quantity() {
4497 return Err(vec![engine_error_at_graph(
4498 graph,
4499 source,
4500 format!(
4501 "Cannot convert {} to quantity unit '{}'.",
4502 source_type.name(),
4503 unit_name
4504 ),
4505 )]);
4506 }
4507 if source_type.is_number_range() {
4508 return Ok(());
4509 }
4510 if source_type.is_quantity_range() {
4511 if let TypeSpecification::QuantityRange {
4512 decomposition,
4513 units,
4514 ..
4515 } = &source_type.specifications
4516 {
4517 if *decomposition == duration_decomposition() {
4518 return Ok(());
4519 }
4520 let all_duration_endpoints = !units.0.is_empty()
4521 && units.0.iter().all(|unit| {
4522 find_types_by_spec(resolved_types, spec_arc)
4523 .and_then(|dt| dt.unit_index.get(&unit.name))
4524 .is_some_and(|owner| owner.is_duration_like_quantity())
4525 });
4526 if all_duration_endpoints {
4527 return Ok(());
4528 }
4529 }
4530 return Err(vec![engine_error_at_graph(
4531 graph,
4532 source,
4533 format!(
4534 "Cannot convert {} to quantity unit '{}'.",
4535 source_type.name(),
4536 unit_name
4537 ),
4538 )]);
4539 }
4540 Err(vec![engine_error_at_graph(
4541 graph,
4542 source,
4543 format!(
4544 "Cannot convert {} to quantity unit '{}'.",
4545 source_type.name(),
4546 unit_name
4547 ),
4548 )])
4549 }
4550 SemanticConversionTarget::RatioUnit(unit_name) => {
4551 if !source_type.is_ratio_range() {
4552 return Err(vec![engine_error_at_graph(
4553 graph,
4554 source,
4555 format!(
4556 "Cannot convert {} to ratio unit '{}'.",
4557 source_type.name(),
4558 unit_name
4559 ),
4560 )]);
4561 }
4562 let valid: Vec<&str> = match &source_type.specifications {
4563 TypeSpecification::RatioRange { units, .. } => {
4564 units.iter().map(|u| u.name.as_str()).collect()
4565 }
4566 _ => unreachable!("BUG: is_ratio_range without RatioRange spec"),
4567 };
4568 if valid.iter().any(|name| *name == unit_name) {
4569 Ok(())
4570 } else {
4571 Err(vec![engine_error_at_graph(
4572 graph,
4573 source,
4574 format!(
4575 "Unknown unit '{}' for type {}. Valid units: {}",
4576 unit_name,
4577 source_type.name(),
4578 valid.join(", ")
4579 ),
4580 )])
4581 }
4582 }
4583 SemanticConversionTarget::Calendar(_) => Err(vec![engine_error_at_graph(
4584 graph,
4585 source,
4586 format!("Cannot convert {} to calendar.", source_type.name()),
4587 )]),
4588 }
4589}
4590
4591fn check_unit_conversion_types(
4592 graph: &Graph,
4593 source_type: &LemmaType,
4594 target: &SemanticConversionTarget,
4595 resolved_types: &ResolvedTypesMap,
4596 source: &Source,
4597 spec_arc: &Arc<LemmaSpec>,
4598) -> Result<(), Vec<Error>> {
4599 if source_type.vetoed() {
4600 return Ok(());
4601 }
4602 match target {
4603 SemanticConversionTarget::Number => {
4604 if source_type.is_date_range() || source_type.is_number_range() {
4605 return Ok(());
4606 }
4607 if source_type.is_quantity_range()
4608 || source_type.is_ratio_range()
4609 || source_type.is_calendar_range()
4610 {
4611 return check_range_span_unit_conversion(
4612 graph,
4613 source_type,
4614 target,
4615 resolved_types,
4616 source,
4617 spec_arc,
4618 );
4619 }
4620 if source_type.is_anonymous_quantity() {
4625 let decomp = source_type.quantity_type_decomposition();
4626 if !decomp.is_empty() {
4627 return Err(vec![engine_error_at_graph(
4628 graph,
4629 source,
4630 format!(
4631 "Cannot use 'as number' to strip an anonymous intermediate with unresolved \
4632 dimensions {:?}. Cast to a named quantity typedef first (e.g., 'as <unit>'), \
4633 or ensure all dimensions cancel before converting to number.",
4634 decomp
4635 ),
4636 )]);
4637 }
4638 }
4639 if source_type.is_quantity()
4640 || source_type.is_number()
4641 || source_type.is_duration_like()
4642 || source_type.is_calendar()
4643 || source_type.is_ratio()
4644 {
4645 Ok(())
4646 } else {
4647 Err(vec![engine_error_at_graph(
4648 graph,
4649 source,
4650 format!("Cannot convert {} to number.", source_type.name()),
4651 )])
4652 }
4653 }
4654 SemanticConversionTarget::QuantityUnit(unit_name) => {
4655 if source_type.is_date_range() {
4656 let target_type = find_types_by_spec(resolved_types, spec_arc)
4657 .and_then(|dt| dt.unit_index.get(unit_name))
4658 .cloned();
4659
4660 let target_type = match target_type {
4661 Some(lemma_type) => lemma_type,
4662 None => {
4663 return Err(vec![engine_error_at_graph(
4664 graph,
4665 source,
4666 format!(
4667 "Unknown unit '{}': no quantity type in spec '{}' owns this unit.",
4668 unit_name, spec_arc.name
4669 ),
4670 )]);
4671 }
4672 };
4673
4674 if !target_type.is_duration_like_quantity() {
4675 return Err(vec![engine_error_at_graph(
4676 graph,
4677 source,
4678 format!(
4679 "Cannot convert date range to quantity unit '{}'.",
4680 unit_name
4681 ),
4682 )]);
4683 }
4684
4685 return Ok(());
4686 }
4687
4688 if source_type.is_number_range()
4689 || source_type.is_quantity_range()
4690 || source_type.is_calendar_range()
4691 {
4692 return check_range_span_unit_conversion(
4693 graph,
4694 source_type,
4695 target,
4696 resolved_types,
4697 source,
4698 spec_arc,
4699 );
4700 }
4701 if source_type.is_number() {
4703 return if find_types_by_spec(resolved_types, spec_arc)
4704 .and_then(|dt| dt.unit_index.get(unit_name))
4705 .is_some()
4706 {
4707 Ok(())
4708 } else {
4709 Err(vec![engine_error_at_graph(
4710 graph,
4711 source,
4712 format!("Unknown unit '{}' in spec '{}'.", unit_name, spec_arc.name),
4713 )])
4714 };
4715 }
4716
4717 if source_type.is_anonymous_quantity() {
4720 let target_type = find_types_by_spec(resolved_types, spec_arc)
4721 .and_then(|dt| dt.unit_index.get(unit_name))
4722 .cloned();
4723
4724 let target_type = match target_type {
4725 Some(lemma_type) => lemma_type,
4726 None => {
4727 return Err(vec![engine_error_at_graph(
4728 graph,
4729 source,
4730 format!(
4731 "Unknown unit '{}': no quantity type in spec '{}' owns this unit.",
4732 unit_name, spec_arc.name
4733 ),
4734 )]);
4735 }
4736 };
4737
4738 let source_decomp = source_type.quantity_type_decomposition();
4739 let target_quantity_family = target_type
4740 .quantity_family_name()
4741 .map(str::to_string)
4742 .unwrap_or_else(|| target_type.name().to_string());
4743
4744 let target_decomp = match &target_type.specifications {
4745 TypeSpecification::Quantity { decomposition, .. } => decomposition.clone(),
4746 _ => {
4747 return Err(vec![engine_error_at_graph(
4748 graph,
4749 source,
4750 format!("Unit '{}' does not belong to a quantity type.", unit_name),
4751 )]);
4752 }
4753 };
4754
4755 if *source_decomp != target_decomp {
4756 return Err(vec![engine_error_at_graph(
4757 graph,
4758 source,
4759 format!(
4760 "Cannot cast to '{}' (quantity '{}'): source dimensions {:?} do not \
4761 match target dimensions {:?}. The intermediate result has a different \
4762 physical quantity than the target type.",
4763 unit_name, target_quantity_family, source_decomp, target_decomp
4764 ),
4765 )]);
4766 }
4767
4768 return Ok(());
4769 }
4770
4771 match source_type.validate_quantity_result_unit(unit_name) {
4772 Ok(()) => Ok(()),
4773 Err(message) => Err(vec![engine_error_at_graph(graph, source, message)]),
4774 }
4775 }
4776 SemanticConversionTarget::RatioUnit(unit_name) => {
4777 if source_type.is_ratio_range() {
4778 return check_range_span_unit_conversion(
4779 graph,
4780 source_type,
4781 target,
4782 resolved_types,
4783 source,
4784 spec_arc,
4785 );
4786 }
4787 let unit_check: Option<(bool, Vec<&str>)> = match &source_type.specifications {
4788 TypeSpecification::Ratio { units, .. } => {
4789 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
4790 let found = units.iter().any(|u| u.name == *unit_name);
4791 Some((found, valid))
4792 }
4793 _ => None,
4794 };
4795
4796 match unit_check {
4797 Some((true, _)) => Ok(()),
4798 Some((false, valid)) => Err(vec![engine_error_at_graph(
4799 graph,
4800 source,
4801 format!(
4802 "Unknown unit '{}' for type {}. Valid units: {}",
4803 unit_name,
4804 source_type.name(),
4805 valid.join(", ")
4806 ),
4807 )]),
4808 None if source_type.is_number() => {
4809 if find_types_by_spec(resolved_types, spec_arc)
4810 .and_then(|dt| dt.unit_index.get(unit_name))
4811 .is_none()
4812 {
4813 Err(vec![engine_error_at_graph(
4814 graph,
4815 source,
4816 format!("Unknown unit '{}' in spec '{}'.", unit_name, spec_arc.name),
4817 )])
4818 } else {
4819 Ok(())
4820 }
4821 }
4822 None => Err(vec![engine_error_at_graph(
4823 graph,
4824 source,
4825 format!(
4826 "Cannot convert {} to ratio unit '{}'.",
4827 source_type.name(),
4828 unit_name
4829 ),
4830 )]),
4831 }
4832 }
4833 SemanticConversionTarget::Calendar(_) => {
4834 if !source_type.is_calendar()
4835 && !source_type.is_number()
4836 && !source_type.is_date_range()
4837 {
4838 Err(vec![engine_error_at_graph(
4839 graph,
4840 source,
4841 format!("Cannot convert {} to calendar.", source_type.name()),
4842 )])
4843 } else {
4844 Ok(())
4845 }
4846 }
4847 }
4848}
4849
4850fn check_mathematical_operand(
4851 graph: &Graph,
4852 operand_type: &LemmaType,
4853 source: &Source,
4854) -> Result<(), Vec<Error>> {
4855 if operand_type.vetoed() {
4856 return Ok(());
4857 }
4858 if !operand_type.is_number() {
4859 Err(vec![engine_error_at_graph(
4860 graph,
4861 source,
4862 format!(
4863 "Mathematical function requires number operand, got {:?}",
4864 operand_type
4865 ),
4866 )])
4867 } else {
4868 Ok(())
4869 }
4870}
4871
4872fn check_all_rule_references_exist(graph: &Graph) -> Result<(), Vec<Error>> {
4874 let mut errors = Vec::new();
4875 let existing_rules: HashSet<&RulePath> = graph.rules().keys().collect();
4876 for (rule_path, rule_node) in graph.rules() {
4877 for dependency in &rule_node.depends_on_rules {
4878 if !existing_rules.contains(dependency) {
4879 errors.push(engine_error_at_graph(
4880 graph,
4881 &rule_node.source,
4882 format!(
4883 "Rule '{}' references non-existent rule '{}'",
4884 rule_path.rule, dependency.rule
4885 ),
4886 ));
4887 }
4888 }
4889 }
4890 if errors.is_empty() {
4891 Ok(())
4892 } else {
4893 Err(errors)
4894 }
4895}
4896
4897fn check_data_and_rule_name_collisions(graph: &Graph) -> Result<(), Vec<Error>> {
4899 let mut errors = Vec::new();
4900 for rule_path in graph.rules().keys() {
4901 let data_path = DataPath::new(rule_path.segments.clone(), rule_path.rule.clone());
4902 if graph.data().contains_key(&data_path) {
4903 let rule_node = graph.rules().get(rule_path).unwrap_or_else(|| {
4904 unreachable!(
4905 "BUG: rule '{}' missing from graph while validating name collisions",
4906 rule_path.rule
4907 )
4908 });
4909 errors.push(engine_error_at_graph(
4910 graph,
4911 &rule_node.source,
4912 format!(
4913 "Name collision: '{}' is defined as both a data and a rule",
4914 data_path
4915 ),
4916 ));
4917 }
4918 }
4919 if errors.is_empty() {
4920 Ok(())
4921 } else {
4922 Err(errors)
4923 }
4924}
4925
4926fn check_data_reference(
4928 data_path: &DataPath,
4929 graph: &Graph,
4930 data_source: &Source,
4931) -> Result<(), Vec<Error>> {
4932 let entry = match graph.data().get(data_path) {
4933 Some(e) => e,
4934 None => {
4935 return Err(vec![engine_error_at_graph(
4936 graph,
4937 data_source,
4938 format!("Unknown data reference '{}'", data_path),
4939 )]);
4940 }
4941 };
4942 match entry {
4943 DataDefinition::Value { .. }
4944 | DataDefinition::TypeDeclaration { .. }
4945 | DataDefinition::Reference { .. } => Ok(()),
4946 DataDefinition::Import { .. } => Err(vec![engine_error_at_graph(
4947 graph,
4948 entry.source(),
4949 format!(
4950 "Cannot compute type for spec reference data '{}'",
4951 data_path
4952 ),
4953 )]),
4954 }
4955}
4956
4957fn check_expression(
4961 expression: &Expression,
4962 graph: &Graph,
4963 inferred_types: &HashMap<RulePath, LemmaType>,
4964 resolved_types: &ResolvedTypesMap,
4965 spec_arc: &Arc<LemmaSpec>,
4966) -> Result<(), Vec<Error>> {
4967 let mut errors = Vec::new();
4968
4969 let collect = |result: Result<(), Vec<Error>>, errors: &mut Vec<Error>| {
4970 if let Err(errs) = result {
4971 errors.extend(errs);
4972 }
4973 };
4974
4975 match &expression.kind {
4976 ExpressionKind::Literal(_) => {}
4977
4978 ExpressionKind::DataPath(data_path) => {
4979 let data_source = expression
4980 .source_location
4981 .as_ref()
4982 .expect("BUG: expression missing source in check_expression");
4983 collect(
4984 check_data_reference(data_path, graph, data_source),
4985 &mut errors,
4986 );
4987 }
4988
4989 ExpressionKind::RulePath(_) => {}
4990
4991 ExpressionKind::LogicalAnd(left, right) => {
4992 collect(
4993 check_expression(left, graph, inferred_types, resolved_types, spec_arc),
4994 &mut errors,
4995 );
4996 collect(
4997 check_expression(right, graph, inferred_types, resolved_types, spec_arc),
4998 &mut errors,
4999 );
5000
5001 let left_type =
5002 infer_expression_type(left, graph, inferred_types, resolved_types, spec_arc);
5003 let right_type =
5004 infer_expression_type(right, graph, inferred_types, resolved_types, spec_arc);
5005 let expr_source = expression
5006 .source_location
5007 .as_ref()
5008 .expect("BUG: expression missing source in check_expression");
5009 collect(
5010 check_logical_and_operands(graph, &left_type, &right_type, expr_source),
5011 &mut errors,
5012 );
5013 }
5014
5015 ExpressionKind::LogicalOr(left, right) => {
5016 collect(
5017 check_expression(left, graph, inferred_types, resolved_types, spec_arc),
5018 &mut errors,
5019 );
5020 collect(
5021 check_expression(right, graph, inferred_types, resolved_types, spec_arc),
5022 &mut errors,
5023 );
5024
5025 let left_type =
5026 infer_expression_type(left, graph, inferred_types, resolved_types, spec_arc);
5027 let right_type =
5028 infer_expression_type(right, graph, inferred_types, resolved_types, spec_arc);
5029 let expr_source = expression
5030 .source_location
5031 .as_ref()
5032 .expect("BUG: expression missing source in check_expression");
5033 collect(
5034 check_logical_or_operands(graph, &left_type, &right_type, expr_source),
5035 &mut errors,
5036 );
5037 }
5038
5039 ExpressionKind::LogicalNegation(operand, _) => {
5040 collect(
5041 check_expression(operand, graph, inferred_types, resolved_types, spec_arc),
5042 &mut errors,
5043 );
5044
5045 let operand_type =
5046 infer_expression_type(operand, graph, inferred_types, resolved_types, spec_arc);
5047 let expr_source = expression
5048 .source_location
5049 .as_ref()
5050 .expect("BUG: expression missing source in check_expression");
5051 collect(
5052 check_logical_operand(graph, &operand_type, expr_source),
5053 &mut errors,
5054 );
5055 }
5056
5057 ExpressionKind::Comparison(left, op, right) => {
5058 collect(
5059 check_expression(left, graph, inferred_types, resolved_types, spec_arc),
5060 &mut errors,
5061 );
5062 collect(
5063 check_expression(right, graph, inferred_types, resolved_types, spec_arc),
5064 &mut errors,
5065 );
5066
5067 let left_type =
5068 infer_expression_type(left, graph, inferred_types, resolved_types, spec_arc);
5069 let right_type =
5070 infer_expression_type(right, graph, inferred_types, resolved_types, spec_arc);
5071 let expr_source = expression
5072 .source_location
5073 .as_ref()
5074 .expect("BUG: expression missing source in check_expression");
5075 collect(
5076 check_comparison_types(graph, &left_type, op, &right_type, expr_source),
5077 &mut errors,
5078 );
5079 }
5080
5081 ExpressionKind::Arithmetic(left, operator, right) => {
5082 collect(
5083 check_expression(left, graph, inferred_types, resolved_types, spec_arc),
5084 &mut errors,
5085 );
5086 collect(
5087 check_expression(right, graph, inferred_types, resolved_types, spec_arc),
5088 &mut errors,
5089 );
5090
5091 let left_type =
5092 infer_expression_type(left, graph, inferred_types, resolved_types, spec_arc);
5093 let right_type =
5094 infer_expression_type(right, graph, inferred_types, resolved_types, spec_arc);
5095 let expr_source = expression
5096 .source_location
5097 .as_ref()
5098 .expect("BUG: expression missing source in check_expression");
5099 collect(
5100 check_arithmetic_types(graph, &left_type, &right_type, operator, expr_source),
5101 &mut errors,
5102 );
5103 collect(
5104 arithmetic_plan_time_exactness_planning_errors(
5105 graph,
5106 left,
5107 right,
5108 &left_type,
5109 &right_type,
5110 operator,
5111 expr_source,
5112 ),
5113 &mut errors,
5114 );
5115 }
5116
5117 ExpressionKind::UnitConversion(source_expression, target) => {
5118 collect(
5119 check_expression(
5120 source_expression,
5121 graph,
5122 inferred_types,
5123 resolved_types,
5124 spec_arc,
5125 ),
5126 &mut errors,
5127 );
5128
5129 let source_type = infer_expression_type(
5130 source_expression,
5131 graph,
5132 inferred_types,
5133 resolved_types,
5134 spec_arc,
5135 );
5136 let expr_source = expression
5137 .source_location
5138 .as_ref()
5139 .expect("BUG: expression missing source in check_expression");
5140 collect(
5141 check_unit_conversion_types(
5142 graph,
5143 &source_type,
5144 target,
5145 resolved_types,
5146 expr_source,
5147 spec_arc,
5148 ),
5149 &mut errors,
5150 );
5151
5152 }
5155
5156 ExpressionKind::MathematicalComputation(_, operand) => {
5157 collect(
5158 check_expression(operand, graph, inferred_types, resolved_types, spec_arc),
5159 &mut errors,
5160 );
5161
5162 let operand_type =
5163 infer_expression_type(operand, graph, inferred_types, resolved_types, spec_arc);
5164 let expr_source = expression
5165 .source_location
5166 .as_ref()
5167 .expect("BUG: expression missing source in check_expression");
5168 collect(
5169 check_mathematical_operand(graph, &operand_type, expr_source),
5170 &mut errors,
5171 );
5172 }
5173
5174 ExpressionKind::Veto(_) => {}
5175
5176 ExpressionKind::ResultIsVeto(operand) => {
5177 collect(
5178 check_expression(operand, graph, inferred_types, resolved_types, spec_arc),
5179 &mut errors,
5180 );
5181 }
5182
5183 ExpressionKind::Now => {}
5184
5185 ExpressionKind::DateRelative(_, date_expr) => {
5186 collect(
5187 check_expression(date_expr, graph, inferred_types, resolved_types, spec_arc),
5188 &mut errors,
5189 );
5190
5191 let date_type =
5192 infer_expression_type(date_expr, graph, inferred_types, resolved_types, spec_arc);
5193 if !date_type.is_date() {
5194 let expr_source = expression
5195 .source_location
5196 .as_ref()
5197 .expect("BUG: expression missing source in check_expression");
5198 errors.push(engine_error_at_graph(
5199 graph,
5200 expr_source,
5201 format!(
5202 "Date sugar 'in past/future' requires a date expression, got type '{}'",
5203 date_type
5204 ),
5205 ));
5206 }
5207 }
5208
5209 ExpressionKind::DateCalendar(_, _, date_expr) => {
5210 collect(
5211 check_expression(date_expr, graph, inferred_types, resolved_types, spec_arc),
5212 &mut errors,
5213 );
5214
5215 let date_type =
5216 infer_expression_type(date_expr, graph, inferred_types, resolved_types, spec_arc);
5217 if !date_type.is_date() {
5218 let expr_source = expression
5219 .source_location
5220 .as_ref()
5221 .expect("BUG: expression missing source in check_expression");
5222 errors.push(engine_error_at_graph(
5223 graph,
5224 expr_source,
5225 format!(
5226 "Calendar sugar requires a date expression, got type '{}'",
5227 date_type
5228 ),
5229 ));
5230 }
5231 }
5232
5233 ExpressionKind::RangeLiteral(left, right) => {
5234 collect(
5235 check_expression(left, graph, inferred_types, resolved_types, spec_arc),
5236 &mut errors,
5237 );
5238 collect(
5239 check_expression(right, graph, inferred_types, resolved_types, spec_arc),
5240 &mut errors,
5241 );
5242
5243 let left_type =
5244 infer_expression_type(left, graph, inferred_types, resolved_types, spec_arc);
5245 let right_type =
5246 infer_expression_type(right, graph, inferred_types, resolved_types, spec_arc);
5247 let expr_source = expression
5248 .source_location
5249 .as_ref()
5250 .expect("BUG: expression missing source in check_expression");
5251
5252 let inferred_range_type = infer_range_type_from_endpoint_types(&left_type, &right_type);
5253 if inferred_range_type.is_undetermined() {
5254 errors.push(engine_error_at_graph(
5255 graph,
5256 expr_source,
5257 format!(
5258 "Cannot create a range from {} and {}.",
5259 left_type.name(),
5260 right_type.name()
5261 ),
5262 ));
5263 }
5264 }
5265
5266 ExpressionKind::PastFutureRange(_, offset_expr) => {
5267 collect(
5268 check_expression(offset_expr, graph, inferred_types, resolved_types, spec_arc),
5269 &mut errors,
5270 );
5271
5272 let offset_type =
5273 infer_expression_type(offset_expr, graph, inferred_types, resolved_types, spec_arc);
5274 if !offset_type.is_duration_like() && !offset_type.is_calendar() {
5275 let expr_source = expression
5276 .source_location
5277 .as_ref()
5278 .expect("BUG: expression missing source in check_expression");
5279 errors.push(engine_error_at_graph(
5280 graph,
5281 expr_source,
5282 format!(
5283 "Past/future range requires a duration or calendar expression, got type '{}'",
5284 offset_type.name()
5285 ),
5286 ));
5287 }
5288 }
5289
5290 ExpressionKind::RangeContainment(value, range) => {
5291 collect(
5292 check_expression(value, graph, inferred_types, resolved_types, spec_arc),
5293 &mut errors,
5294 );
5295 collect(
5296 check_expression(range, graph, inferred_types, resolved_types, spec_arc),
5297 &mut errors,
5298 );
5299
5300 let value_type =
5301 infer_expression_type(value, graph, inferred_types, resolved_types, spec_arc);
5302 let range_type =
5303 infer_expression_type(range, graph, inferred_types, resolved_types, spec_arc);
5304 let expr_source = expression
5305 .source_location
5306 .as_ref()
5307 .expect("BUG: expression missing source in check_expression");
5308
5309 if !range_type.is_range() {
5310 errors.push(engine_error_at_graph(
5311 graph,
5312 expr_source,
5313 format!(
5314 "Right side of 'in' must be a range, got type '{}'",
5315 range_type.name()
5316 ),
5317 ));
5318 } else {
5319 let compatible = (range_type.is_date_range() && value_type.is_date())
5320 || (range_type.is_number_range() && value_type.is_number())
5321 || (range_type.is_quantity_range()
5322 && value_type.is_quantity()
5323 && quantity_range_matches_quantity(&range_type, &value_type))
5324 || (range_type.is_ratio_range() && value_type.is_ratio())
5325 || (range_type.is_calendar_range() && value_type.is_calendar());
5326 if !compatible {
5327 errors.push(engine_error_at_graph(
5328 graph,
5329 expr_source,
5330 format!(
5331 "Cannot test whether {} is in {}.",
5332 value_type.name(),
5333 range_type.name()
5334 ),
5335 ));
5336 }
5337 }
5338 }
5339 }
5340
5341 if errors.is_empty() {
5342 Ok(())
5343 } else {
5344 Err(errors)
5345 }
5346}
5347
5348fn check_rule_types(
5354 graph: &Graph,
5355 execution_order: &[RulePath],
5356 inferred_types: &HashMap<RulePath, LemmaType>,
5357 resolved_types: &ResolvedTypesMap,
5358) -> Result<(), Vec<Error>> {
5359 let mut errors = Vec::new();
5360
5361 let collect = |result: Result<(), Vec<Error>>, errors: &mut Vec<Error>| {
5362 if let Err(errs) = result {
5363 errors.extend(errs);
5364 }
5365 };
5366
5367 for rule_path in execution_order {
5368 let rule_node = match graph.rules().get(rule_path) {
5369 Some(node) => node,
5370 None => continue,
5371 };
5372 let branches = &rule_node.branches;
5373 let spec_arc = &rule_node.spec_arc;
5374
5375 if branches.is_empty() {
5376 continue;
5377 }
5378
5379 let (_, default_result) = &branches[0];
5380 collect(
5381 check_expression(
5382 default_result,
5383 graph,
5384 inferred_types,
5385 resolved_types,
5386 spec_arc,
5387 ),
5388 &mut errors,
5389 );
5390 let default_type = infer_expression_type(
5391 default_result,
5392 graph,
5393 inferred_types,
5394 resolved_types,
5395 spec_arc,
5396 );
5397
5398 if default_type.is_anonymous_quantity() {
5400 let decomp = default_type.quantity_type_decomposition();
5401 if !decomp.is_empty() {
5402 let default_source = default_result
5403 .source_location
5404 .as_ref()
5405 .expect("BUG: default branch result expression has no source location");
5406 errors.push(engine_error_at_graph(
5407 graph,
5408 default_source,
5409 format!(
5410 "Rule '{}' in spec '{}' returns an anonymous intermediate with unresolved \
5411 dimensions {:?}. Cast the result with 'as <unit>' (e.g., 'as mps') \
5412 or ensure all dimensions cancel.",
5413 rule_path.rule, spec_arc.name, decomp
5414 ),
5415 ));
5416 }
5417 }
5418
5419 let mut non_veto_type: Option<LemmaType> = None;
5420 if !default_type.vetoed() && !default_type.is_undetermined() {
5421 non_veto_type = Some(default_type.clone());
5422 }
5423
5424 for (branch_index, (condition, result)) in branches.iter().enumerate().skip(1) {
5425 if let Some(condition_expression) = condition {
5426 collect(
5427 check_expression(
5428 condition_expression,
5429 graph,
5430 inferred_types,
5431 resolved_types,
5432 spec_arc,
5433 ),
5434 &mut errors,
5435 );
5436 let condition_type = infer_expression_type(
5437 condition_expression,
5438 graph,
5439 inferred_types,
5440 resolved_types,
5441 spec_arc,
5442 );
5443 if !condition_type.is_boolean() && !condition_type.is_undetermined() {
5444 let condition_source = condition_expression
5445 .source_location
5446 .as_ref()
5447 .expect("BUG: condition expression missing source in check_rule_types");
5448 errors.push(engine_error_at_graph(
5449 graph,
5450 condition_source,
5451 format!(
5452 "Unless clause condition in rule '{}' must be boolean, got {:?}",
5453 rule_path.rule, condition_type
5454 ),
5455 ));
5456 }
5457 }
5458
5459 collect(
5460 check_expression(result, graph, inferred_types, resolved_types, spec_arc),
5461 &mut errors,
5462 );
5463 let result_type =
5464 infer_expression_type(result, graph, inferred_types, resolved_types, spec_arc);
5465
5466 if result_type.is_anonymous_quantity() {
5468 let decomp = result_type.quantity_type_decomposition();
5469 if !decomp.is_empty() {
5470 let branch_source = result
5471 .source_location
5472 .as_ref()
5473 .expect("BUG: unless branch result expression has no source location");
5474 errors.push(engine_error_at_graph(
5475 graph,
5476 branch_source,
5477 format!(
5478 "Unless clause {} in rule '{}' (spec '{}') returns an anonymous \
5479 intermediate with unresolved dimensions {:?}. Cast the result with \
5480 'as <unit>' or ensure all dimensions cancel.",
5481 branch_index, rule_path.rule, spec_arc.name, decomp
5482 ),
5483 ));
5484 }
5485 }
5486
5487 if !result_type.vetoed() && !result_type.is_undetermined() {
5488 if non_veto_type.is_none() {
5489 non_veto_type = Some(result_type.clone());
5490 } else if let Some(ref existing_type) = non_veto_type {
5491 if !existing_type.has_same_base_type(&result_type) {
5492 let Some(rule_node) = graph.rules().get(rule_path) else {
5493 unreachable!(
5494 "BUG: rule type validation referenced missing rule '{}'",
5495 rule_path.rule
5496 );
5497 };
5498 let rule_source = &rule_node.source;
5499 let default_expr = &branches[0].1;
5500
5501 let mut location_parts = vec![format!(
5502 "{}:{}:{}",
5503 rule_source.source_type, rule_source.span.line, rule_source.span.col
5504 )];
5505
5506 if let Some(loc) = &default_expr.source_location {
5507 location_parts.push(format!(
5508 "default branch at {}:{}:{}",
5509 loc.source_type, loc.span.line, loc.span.col
5510 ));
5511 }
5512 if let Some(loc) = &result.source_location {
5513 location_parts.push(format!(
5514 "unless clause {} at {}:{}:{}",
5515 branch_index, loc.source_type, loc.span.line, loc.span.col
5516 ));
5517 }
5518
5519 errors.push(Error::validation_with_context(
5520 format!("Type mismatch in rule '{}' in spec '{}' ({}): default branch returns {}, but unless clause {} returns {}. All branches must return the same primitive type.",
5521 rule_path.rule,
5522 spec_arc.name,
5523 location_parts.join(", "),
5524 existing_type.name(),
5525 branch_index,
5526 result_type.name()),
5527 Some(rule_source.clone()),
5528 None::<String>,
5529 Some(Arc::clone(&graph.main_spec)),
5530 None,
5531 ));
5532 }
5533 }
5534 }
5535 }
5536 }
5537
5538 if errors.is_empty() {
5539 Ok(())
5540 } else {
5541 Err(errors)
5542 }
5543}
5544
5545fn apply_inferred_types(graph: &mut Graph, inferred_types: HashMap<RulePath, LemmaType>) {
5553 for (rule_path, rule_type) in inferred_types {
5554 if let Some(rule_node) = graph.rules_mut().get_mut(&rule_path) {
5555 rule_node.rule_type = rule_type;
5556 }
5557 }
5558}
5559
5560fn infer_rule_types(
5564 graph: &Graph,
5565 execution_order: &[RulePath],
5566 resolved_types: &ResolvedTypesMap,
5567) -> HashMap<RulePath, LemmaType> {
5568 let mut computed_types: HashMap<RulePath, LemmaType> = HashMap::new();
5569
5570 for rule_path in execution_order {
5571 let rule_node = match graph.rules().get(rule_path) {
5572 Some(node) => node,
5573 None => continue,
5574 };
5575 let branches = &rule_node.branches;
5576 let spec_arc = &rule_node.spec_arc;
5577
5578 if branches.is_empty() {
5579 continue;
5580 }
5581
5582 let (_, default_result) = &branches[0];
5583 let default_type = infer_expression_type(
5584 default_result,
5585 graph,
5586 &computed_types,
5587 resolved_types,
5588 spec_arc,
5589 );
5590
5591 let mut non_veto_type: Option<LemmaType> = None;
5592 if !default_type.vetoed() && !default_type.is_undetermined() {
5593 non_veto_type = Some(default_type.clone());
5594 }
5595
5596 for (_branch_index, (_condition, result)) in branches.iter().enumerate().skip(1) {
5597 let result_type =
5598 infer_expression_type(result, graph, &computed_types, resolved_types, spec_arc);
5599 if !result_type.vetoed() && !result_type.is_undetermined() && non_veto_type.is_none() {
5600 non_veto_type = Some(result_type.clone());
5601 }
5602 }
5603
5604 let rule_type = non_veto_type.unwrap_or_else(LemmaType::veto_type);
5605 computed_types.insert(rule_path.clone(), rule_type);
5606 }
5607
5608 computed_types
5609}
5610
5611type UnitDecompLookup = HashMap<
5612 String,
5613 (
5614 String,
5615 BaseQuantityVector,
5616 crate::computation::rational::RationalInteger,
5617 ),
5618>;
5619
5620fn declared_quantity_decomposition(type_name: &str, lemma_type: &LemmaType) -> BaseQuantityVector {
5621 match &lemma_type.specifications {
5622 TypeSpecification::Quantity { traits, .. }
5623 if traits.contains(&semantics::QuantityTrait::Duration) =>
5624 {
5625 duration_decomposition()
5626 }
5627 _ => {
5628 let dimension_key = lemma_type
5629 .quantity_family_name()
5630 .unwrap_or(type_name)
5631 .to_string();
5632 [(dimension_key, 1i32)].into_iter().collect()
5633 }
5634 }
5635}
5636
5637fn sync_unit_index_from_resolved(
5638 resolved: &HashMap<String, LemmaType>,
5639 unit_index: &mut HashMap<String, LemmaType>,
5640) {
5641 let unit_index_updates: Vec<(String, LemmaType)> = unit_index
5642 .iter()
5643 .filter_map(|(unit_name, pre_decomp_type)| {
5644 let type_name = pre_decomp_type
5645 .name
5646 .as_deref()
5647 .or_else(|| pre_decomp_type.quantity_family_name())?;
5648 resolved
5649 .get(type_name)
5650 .or_else(|| {
5651 pre_decomp_type
5652 .quantity_family_name()
5653 .and_then(|family| resolved.get(family))
5654 })
5655 .map(|post_decomp_type| (unit_name.clone(), post_decomp_type.clone()))
5656 })
5657 .collect();
5658 for (unit_name, post_decomp_type) in unit_index_updates {
5659 unit_index.insert(unit_name, post_decomp_type);
5660 }
5661}
5662
5663fn repair_empty_simple_quantity_decomposition_in_unit_index(
5667 unit_index: &mut HashMap<String, LemmaType>,
5668) {
5669 for (_unit_key, lemma_type) in unit_index.iter_mut() {
5670 let base_decomp = {
5671 let TypeSpecification::Quantity {
5672 units,
5673 decomposition,
5674 ..
5675 } = &lemma_type.specifications
5676 else {
5677 continue;
5678 };
5679 if !decomposition.is_empty() {
5680 continue;
5681 }
5682 if units.is_empty() || units.iter().any(|u| !u.derived_quantity_factors.is_empty()) {
5683 continue;
5684 }
5685 let Some(ref type_name) = lemma_type.name else {
5686 continue;
5687 };
5688 if type_name.is_empty() {
5689 continue;
5690 }
5691 let candidate = declared_quantity_decomposition(type_name.as_str(), lemma_type);
5692 if candidate.is_empty() {
5693 continue;
5694 }
5695 Some(candidate)
5696 };
5697 let Some(base_decomp) = base_decomp else {
5698 continue;
5699 };
5700 let TypeSpecification::Quantity {
5701 units,
5702 decomposition,
5703 canonical_unit,
5704 ..
5705 } = &mut lemma_type.specifications
5706 else {
5707 continue;
5708 };
5709 let mut canonical = String::new();
5710 for unit in units.0.iter_mut() {
5711 unit.decomposition = base_decomp.clone();
5712 if unit.is_canonical_factor() && canonical.is_empty() {
5713 canonical = unit.name.clone();
5714 }
5715 }
5716 *decomposition = base_decomp;
5717 *canonical_unit = canonical;
5718 }
5719}
5720
5721fn owning_quantity_type_name_for_unit(
5722 unit_name: &str,
5723 lookup: &UnitDecompLookup,
5724 unit_index: &HashMap<String, LemmaType>,
5725) -> Option<String> {
5726 if let Some((owning_quantity_name, _, _)) = lookup.get(unit_name) {
5727 return Some(owning_quantity_name.clone());
5728 }
5729 unit_index.get(unit_name).and_then(|lemma_type| {
5730 lemma_type
5731 .name
5732 .clone()
5733 .or_else(|| lemma_type.quantity_family_name().map(str::to_string))
5734 })
5735}
5736
5737fn sort_derived_quantity_types_for_resolution(
5739 spec_name: &str,
5740 derived_quantity_type_names: Vec<String>,
5741 resolved: &HashMap<String, LemmaType>,
5742 lookup: &UnitDecompLookup,
5743 unit_index: &HashMap<String, LemmaType>,
5744 source_for: &dyn Fn(&str) -> Option<Source>,
5745) -> Result<Vec<String>, Error> {
5746 let derived_quantity_type_count = derived_quantity_type_names.len();
5747 if derived_quantity_type_count == 0 {
5748 return Ok(derived_quantity_type_names);
5749 }
5750
5751 let type_index: HashMap<&str, usize> = derived_quantity_type_names
5752 .iter()
5753 .enumerate()
5754 .map(|(index, name)| (name.as_str(), index))
5755 .collect();
5756
5757 let mut dependency_sets: Vec<HashSet<usize>> =
5758 vec![HashSet::new(); derived_quantity_type_count];
5759
5760 for (dependent_index, type_name) in derived_quantity_type_names.iter().enumerate() {
5761 let TypeSpecification::Quantity { units, .. } = &resolved[type_name].specifications else {
5762 continue;
5763 };
5764 for unit in units.iter() {
5765 for (factor_unit_name, _) in &unit.derived_quantity_factors {
5766 let Some(owning_quantity_name) =
5767 owning_quantity_type_name_for_unit(factor_unit_name, lookup, unit_index)
5768 else {
5769 continue;
5770 };
5771 let Some(dependency_index) = type_index.get(owning_quantity_name.as_str()).copied()
5772 else {
5773 continue;
5774 };
5775 if dependency_index == dependent_index {
5776 continue;
5777 }
5778 dependency_sets[dependent_index].insert(dependency_index);
5779 }
5780 }
5781 }
5782
5783 let mut in_degree = vec![0usize; derived_quantity_type_count];
5784 let mut dependents: Vec<Vec<usize>> = vec![Vec::new(); derived_quantity_type_count];
5785
5786 for (dependent_index, dependencies) in dependency_sets.iter().enumerate() {
5787 for &dependency_index in dependencies {
5788 in_degree[dependent_index] += 1;
5789 dependents[dependency_index].push(dependent_index);
5790 }
5791 }
5792
5793 let mut queue: VecDeque<usize> = (0..derived_quantity_type_count)
5794 .filter(|&index| in_degree[index] == 0)
5795 .collect();
5796 let mut sorted_indices: Vec<usize> = Vec::with_capacity(derived_quantity_type_count);
5797
5798 while let Some(index) = queue.pop_front() {
5799 sorted_indices.push(index);
5800 for &dependent_index in &dependents[index] {
5801 in_degree[dependent_index] -= 1;
5802 if in_degree[dependent_index] == 0 {
5803 queue.push_back(dependent_index);
5804 }
5805 }
5806 }
5807
5808 if sorted_indices.len() != derived_quantity_type_count {
5809 let mut cycle_type_names: Vec<String> = (0..derived_quantity_type_count)
5810 .filter(|&index| in_degree[index] > 0)
5811 .map(|index| derived_quantity_type_names[index].clone())
5812 .collect();
5813 cycle_type_names.sort();
5814 return Err(Error::validation(
5815 format!(
5816 "In spec '{}': circular compound quantity type dependency among: {}",
5817 spec_name,
5818 cycle_type_names.join(", ")
5819 ),
5820 source_for(&cycle_type_names[0]),
5821 None::<String>,
5822 ));
5823 }
5824
5825 Ok(sorted_indices
5826 .into_iter()
5827 .map(|index| derived_quantity_type_names[index].clone())
5828 .collect())
5829}
5830
5831fn resolve_quantity_decompositions(
5832 spec_name: &str,
5833 resolved: &mut HashMap<String, LemmaType>,
5834 unit_index: &mut HashMap<String, LemmaType>,
5835 type_sources: &HashMap<String, Source>,
5836) -> Vec<Error> {
5837 let mut errors: Vec<Error> = Vec::new();
5838
5839 let source_for = |type_name: &str| -> Option<Source> {
5840 type_sources
5841 .get(type_name)
5842 .or_else(|| type_sources.values().next())
5843 .cloned()
5844 };
5845
5846 let base_type_names: Vec<String> = resolved
5847 .iter()
5848 .filter_map(|(name, lt)| {
5849 if let TypeSpecification::Quantity { units, .. } = <.specifications {
5850 if units.iter().all(|u| u.derived_quantity_factors.is_empty()) {
5851 return Some(name.clone());
5852 }
5853 }
5854 None
5855 })
5856 .collect();
5857
5858 for type_name in &base_type_names {
5859 let base_decomp = {
5860 let lemma_type = resolved.get(type_name).unwrap();
5861 declared_quantity_decomposition(type_name, lemma_type)
5862 };
5863
5864 let lemma_type = resolved.get_mut(type_name).unwrap();
5865 let TypeSpecification::Quantity {
5866 units,
5867 decomposition,
5868 canonical_unit,
5869 ..
5870 } = &mut lemma_type.specifications
5871 else {
5872 continue;
5873 };
5874
5875 let mut canonical = String::new();
5876 for unit in units.0.iter_mut() {
5877 unit.decomposition = base_decomp.clone();
5878 if unit.is_canonical_factor() && canonical.is_empty() {
5879 canonical = unit.name.clone();
5880 }
5881 }
5882
5883 *decomposition = base_decomp;
5884 *canonical_unit = canonical;
5885 }
5886
5887 repair_empty_simple_quantity_decomposition_in_unit_index(unit_index);
5888
5889 let mut lookup = UnitDecompLookup::new();
5890
5891 for (unit_name, lemma_type) in unit_index.iter() {
5892 if let TypeSpecification::Quantity {
5893 decomposition,
5894 units,
5895 ..
5896 } = &lemma_type.specifications
5897 {
5898 if !decomposition.is_empty() {
5899 let quantity_name = lemma_type.name.clone().unwrap_or_default();
5900 let factor = units
5901 .iter()
5902 .find(|u| &u.name == unit_name)
5903 .map(|u| u.factor)
5904 .unwrap_or_else(crate::computation::rational::rational_one);
5905 lookup.insert(
5906 unit_name.clone(),
5907 (quantity_name, decomposition.clone(), factor),
5908 );
5909 }
5910 }
5911 }
5912
5913 for (type_name, lemma_type) in resolved.iter() {
5914 if let TypeSpecification::Quantity {
5915 units,
5916 decomposition,
5917 ..
5918 } = &lemma_type.specifications
5919 {
5920 if !decomposition.is_empty() {
5921 let is_defining_type = lemma_type
5922 .quantity_family_name()
5923 .map(|family| family == type_name.as_str())
5924 .unwrap_or(false);
5925 if !is_defining_type {
5926 continue;
5927 }
5928 for unit in units.iter() {
5929 lookup.insert(
5930 unit.name.clone(),
5931 (type_name.clone(), decomposition.clone(), unit.factor),
5932 );
5933 }
5934 }
5935 }
5936 }
5937
5938 let derived_quantity_type_names_unsorted: Vec<String> = resolved
5939 .iter()
5940 .filter_map(|(name, lemma_type)| {
5941 if let TypeSpecification::Quantity { units, .. } = &lemma_type.specifications {
5942 if units
5943 .iter()
5944 .any(|unit| !unit.derived_quantity_factors.is_empty())
5945 {
5946 return Some(name.clone());
5947 }
5948 }
5949 None
5950 })
5951 .collect();
5952
5953 let derived_quantity_type_names = match sort_derived_quantity_types_for_resolution(
5954 spec_name,
5955 derived_quantity_type_names_unsorted,
5956 resolved,
5957 &lookup,
5958 unit_index,
5959 &|type_name| source_for(type_name),
5960 ) {
5961 Ok(sorted) => sorted,
5962 Err(error) => {
5963 errors.push(error);
5964 return errors;
5965 }
5966 };
5967
5968 for type_name in &derived_quantity_type_names {
5969 let type_source = source_for(type_name);
5970
5971 let units_snapshot = match &resolved[type_name].specifications {
5972 TypeSpecification::Quantity { units, .. } => units.clone(),
5973 _ => continue,
5974 };
5975
5976 let mut resolved_type_decomp: Option<BaseQuantityVector> = None;
5977 let mut canonical = String::new();
5978 let mut unit_errors: Vec<Error> = Vec::new();
5979 let mut resolved_unit_factors: Vec<Option<crate::computation::rational::RationalInteger>> =
5980 vec![None; units_snapshot.len()];
5981
5982 for (unit_idx, unit) in units_snapshot.iter().enumerate() {
5983 if unit.derived_quantity_factors.is_empty() {
5984 let simple_decomp =
5985 declared_quantity_decomposition(type_name, &resolved[type_name]);
5986
5987 if let Some(existing) = &resolved_type_decomp {
5988 if existing != &simple_decomp {
5989 unit_errors.push(Error::validation(
5990 format!(
5991 "In spec '{}': quantity type '{}' has inconsistent unit decompositions. \
5992 Unit '{}' is a simple unit (decomposition {{{}: 1}}) but other units \
5993 have decomposition {:?}.",
5994 spec_name, type_name, unit.name, type_name, existing
5995 ),
5996 type_source.clone(),
5997 None::<String>,
5998 ));
5999 }
6000 } else {
6001 resolved_type_decomp = Some(simple_decomp);
6002 }
6003
6004 resolved_unit_factors[unit_idx] = Some(unit.factor);
6005
6006 if unit.is_canonical_factor() && canonical.is_empty() {
6007 canonical = unit.name.clone();
6008 }
6009 continue;
6010 }
6011
6012 match resolve_compound_unit(
6013 spec_name,
6014 type_name,
6015 &unit.name,
6016 unit.factor,
6017 &unit.derived_quantity_factors,
6018 &lookup,
6019 type_source.as_ref(),
6020 ) {
6021 Ok((unit_decomp, derived_factor)) => {
6022 if let Some(existing) = &resolved_type_decomp {
6023 if existing != &unit_decomp {
6024 unit_errors.push(Error::validation(
6025 format!(
6026 "In spec '{}': quantity type '{}' has inconsistent unit \
6027 decompositions. Unit '{}' resolved to {:?} but other units \
6028 resolved to {:?}. All units of a quantity must measure the same \
6029 physical quantity.",
6030 spec_name, type_name, unit.name, unit_decomp, existing
6031 ),
6032 type_source.clone(),
6033 None::<String>,
6034 ));
6035 }
6036 } else {
6037 resolved_type_decomp = Some(unit_decomp);
6038 }
6039
6040 resolved_unit_factors[unit_idx] = Some(derived_factor);
6041
6042 if derived_factor == crate::computation::rational::rational_one()
6043 && canonical.is_empty()
6044 {
6045 canonical = unit.name.clone();
6046 }
6047 }
6048 Err(err) => unit_errors.push(err),
6049 }
6050 }
6051
6052 if !unit_errors.is_empty() {
6053 errors.extend(unit_errors);
6054 continue;
6055 }
6056
6057 let type_decomp = match resolved_type_decomp {
6058 Some(d) => d,
6059 None => continue,
6060 };
6061
6062 if canonical.is_empty() {
6063 use crate::computation::rational::{checked_div, rational_is_zero};
6064
6065 let Some((normalizer_unit_index, normalizer_factor)) = resolved_unit_factors
6066 .iter()
6067 .enumerate()
6068 .find_map(|(unit_index, factor)| factor.map(|factor| (unit_index, factor)))
6069 else {
6070 errors.push(Error::validation(
6071 format!(
6072 "In spec '{}': quantity type '{}' has no unit with conversion factor 1. \
6073 Exactly one unit must have factor 1.",
6074 spec_name, type_name
6075 ),
6076 type_source.clone(),
6077 None::<String>,
6078 ));
6079 continue;
6080 };
6081
6082 if rational_is_zero(&normalizer_factor) {
6083 errors.push(Error::validation(
6084 format!(
6085 "In spec '{}': quantity type '{}' cannot normalize conversion factors because \
6086 unit '{}' has a zero conversion factor.",
6087 spec_name,
6088 type_name,
6089 units_snapshot.0[normalizer_unit_index].name
6090 ),
6091 type_source.clone(),
6092 None::<String>,
6093 ));
6094 continue;
6095 }
6096
6097 let mut normalization_failed = false;
6098 for (unit_index, resolved_factor) in resolved_unit_factors.iter_mut().enumerate() {
6099 let Some(factor) = resolved_factor.as_ref() else {
6100 continue;
6101 };
6102 match checked_div(factor, &normalizer_factor) {
6103 Ok(normalized_factor) => {
6104 *resolved_factor = Some(normalized_factor);
6105 }
6106 Err(error) => {
6107 normalization_failed = true;
6108 errors.push(Error::validation(
6109 format!(
6110 "In spec '{}': quantity type '{}' overflowed while normalizing \
6111 conversion factor for unit '{}': {}",
6112 spec_name, type_name, units_snapshot.0[unit_index].name, error
6113 ),
6114 type_source.clone(),
6115 None::<String>,
6116 ));
6117 }
6118 }
6119 }
6120
6121 if normalization_failed {
6122 continue;
6123 }
6124
6125 canonical = units_snapshot.0[normalizer_unit_index].name.clone();
6126 }
6127
6128 let lemma_type = resolved.get_mut(type_name).unwrap();
6129 let TypeSpecification::Quantity {
6130 units,
6131 decomposition,
6132 canonical_unit,
6133 ..
6134 } = &mut lemma_type.specifications
6135 else {
6136 continue;
6137 };
6138
6139 for (unit_idx, unit) in units.0.iter_mut().enumerate() {
6140 unit.decomposition = type_decomp.clone();
6141 if let Some(factor) = resolved_unit_factors[unit_idx] {
6142 unit.factor = factor;
6143 }
6144 }
6145 *decomposition = type_decomp.clone();
6146 *canonical_unit = canonical;
6147
6148 for unit in units.0.iter() {
6149 lookup.insert(
6150 unit.name.clone(),
6151 (type_name.clone(), type_decomp.clone(), unit.factor),
6152 );
6153 }
6154 }
6155
6156 for type_name in &base_type_names {
6157 let lemma_type = resolved.get(type_name).unwrap();
6158 let TypeSpecification::Quantity {
6159 units,
6160 canonical_unit,
6161 ..
6162 } = &lemma_type.specifications
6163 else {
6164 continue;
6165 };
6166
6167 if canonical_unit.is_empty() && !units.is_empty() {
6168 errors.push(Error::validation(
6169 format!(
6170 "In spec '{}': quantity type '{}' has no unit with conversion factor 1. \
6171 Exactly one unit must have factor 1.",
6172 spec_name, type_name
6173 ),
6174 source_for(type_name),
6175 None::<String>,
6176 ));
6177 }
6178 }
6179
6180 sync_unit_index_from_resolved(resolved, unit_index);
6181 repair_empty_simple_quantity_decomposition_in_unit_index(unit_index);
6182
6183 errors
6184}
6185
6186fn resolve_compound_unit(
6187 spec_name: &str,
6188 declaring_type_name: &str,
6189 unit_name: &str,
6190 prefix: crate::computation::rational::RationalInteger,
6191 factors: &[(String, i32)],
6192 lookup: &UnitDecompLookup,
6193 source: Option<&Source>,
6194) -> Result<
6195 (
6196 BaseQuantityVector,
6197 crate::computation::rational::RationalInteger,
6198 ),
6199 Error,
6200> {
6201 use crate::computation::rational::{checked_mul, checked_pow_i32};
6202
6203 let mut result: BaseQuantityVector = BaseQuantityVector::new();
6204 let mut derived_factor = prefix;
6205
6206 for (quantity_ref, exponent) in factors {
6207 if let Some(calendar_unit) = CalendarUnit::from_keyword(quantity_ref) {
6208 let calendar_factor = calendar_unit.canonical_factor();
6209 let calendar_decomp = calendar_decomposition();
6210 for (dim, &dim_exp) in &calendar_decomp {
6211 accumulate(&mut result, dim, dim_exp * exponent);
6212 }
6213 let calendar_rational = calendar_factor;
6214 let component_contribution =
6215 checked_pow_i32(&calendar_rational, *exponent).map_err(|error| {
6216 overflow_to_validation_error(
6217 spec_name,
6218 unit_name,
6219 declaring_type_name,
6220 quantity_ref,
6221 error,
6222 source,
6223 )
6224 })?;
6225 derived_factor =
6226 checked_mul(&derived_factor, &component_contribution).map_err(|error| {
6227 overflow_to_validation_error(
6228 spec_name,
6229 unit_name,
6230 declaring_type_name,
6231 quantity_ref,
6232 error,
6233 source,
6234 )
6235 })?;
6236 continue;
6237 }
6238
6239 let (owning_quantity_name, owning_decomp, unit_factor) =
6240 lookup.get(quantity_ref.as_str()).ok_or_else(|| {
6241 Error::validation(
6242 format!(
6243 "In spec '{}': unit '{}' in quantity type '{}' references '{}' which is not a \
6244 known unit of any in-scope quantity type. Add `uses <spec>` (or declare the \
6245 owning quantity type in this spec) so its units are in scope.",
6246 spec_name, unit_name, declaring_type_name, quantity_ref
6247 ),
6248 source.cloned(),
6249 None::<String>,
6250 )
6251 })?;
6252
6253 if owning_quantity_name == declaring_type_name {
6254 return Err(Error::validation(
6255 format!(
6256 "In spec '{}': unit '{}' in quantity type '{}' references unit '{}' which \
6257 belongs to the same quantity type. A quantity cannot reference its own units \
6258 in a compound expression.",
6259 spec_name, unit_name, declaring_type_name, quantity_ref
6260 ),
6261 source.cloned(),
6262 None::<String>,
6263 ));
6264 }
6265
6266 if owning_decomp.is_empty() {
6267 return Err(Error::validation(
6268 format!(
6269 "In spec '{}': unit '{}' in quantity type '{}' references '{}' whose owning \
6270 quantity type '{}' does not yet have a resolved decomposition. Ensure base \
6271 quantities are declared before derived quantities that depend on them.",
6272 spec_name, unit_name, declaring_type_name, quantity_ref, owning_quantity_name
6273 ),
6274 source.cloned(),
6275 None::<String>,
6276 ));
6277 }
6278
6279 for (dim, &dim_exp) in owning_decomp {
6280 accumulate(&mut result, dim, dim_exp * exponent);
6281 }
6282
6283 let component_contribution = checked_pow_i32(unit_factor, *exponent).map_err(|error| {
6284 overflow_to_validation_error(
6285 spec_name,
6286 unit_name,
6287 declaring_type_name,
6288 quantity_ref,
6289 error,
6290 source,
6291 )
6292 })?;
6293 derived_factor =
6294 checked_mul(&derived_factor, &component_contribution).map_err(|error| {
6295 overflow_to_validation_error(
6296 spec_name,
6297 unit_name,
6298 declaring_type_name,
6299 quantity_ref,
6300 error,
6301 source,
6302 )
6303 })?;
6304 }
6305
6306 Ok((result, derived_factor))
6307}
6308
6309fn overflow_to_validation_error(
6310 spec_name: &str,
6311 unit_name: &str,
6312 declaring_type_name: &str,
6313 quantity_ref: &str,
6314 failure: crate::computation::rational::NumericFailure,
6315 source: Option<&Source>,
6316) -> Error {
6317 Error::validation(
6318 format!(
6319 "In spec '{}': unit '{}' in quantity type '{}' overflowed while combining '{}': {}",
6320 spec_name, unit_name, declaring_type_name, quantity_ref, failure
6321 ),
6322 source.cloned(),
6323 None::<String>,
6324 )
6325}
6326
6327fn accumulate(result: &mut BaseQuantityVector, dim: &str, value: i32) {
6328 let entry = result.entry(dim.to_string()).or_insert(0);
6329 *entry += value;
6330 if *entry == 0 {
6331 result.remove(dim);
6332 }
6333}
6334
6335#[cfg(test)]
6336mod tests {
6337 use super::*;
6338
6339 use crate::parsing::ast::{BooleanValue, Reference, Span, Value};
6340
6341 fn test_source() -> Source {
6342 Source::new(
6343 crate::parsing::source::SourceType::Volatile,
6344 Span {
6345 start: 0,
6346 end: 0,
6347 line: 1,
6348 col: 0,
6349 },
6350 )
6351 }
6352
6353 fn build_graph(main_spec: &LemmaSpec, all_specs: &[LemmaSpec]) -> Result<Graph, Vec<Error>> {
6354 use crate::engine::Context;
6355 use crate::planning::discovery;
6356
6357 let mut ctx = Context::new();
6358 let repository = ctx.workspace();
6359 for s in all_specs {
6360 if let Err(e) = ctx.insert_spec(Arc::clone(&repository), Arc::new(s.clone())) {
6361 return Err(vec![e]);
6362 }
6363 }
6364 let effective = EffectiveDate::from_option(main_spec.effective_from().cloned());
6365 let main_spec_arc = ctx
6366 .spec_set(&repository, main_spec.name.as_str())
6367 .and_then(|ss| ss.get_exact(main_spec.effective_from()).cloned())
6368 .expect("main_spec must be in all_specs");
6369 let dag =
6370 discovery::build_dag_for_spec(&ctx, &main_spec_arc, &effective).map_err(
6371 |e| match e {
6372 discovery::DagError::Cycle(es) | discovery::DagError::Other(es) => es,
6373 },
6374 )?;
6375 match Graph::build(&ctx, &repository, &main_spec_arc, &dag, &effective) {
6376 Ok((graph, _types)) => Ok(graph),
6377 Err(errors) => Err(errors),
6378 }
6379 }
6380
6381 fn create_test_spec(name: &str) -> LemmaSpec {
6382 LemmaSpec::new(name.to_string())
6383 }
6384
6385 fn create_literal_data(name: &str, value: Value) -> LemmaData {
6386 LemmaData {
6387 reference: Reference {
6388 segments: Vec::new(),
6389 name: name.to_string(),
6390 },
6391 value: ParsedDataValue::Definition {
6392 base: None,
6393 constraints: None,
6394 value: Some(value),
6395 },
6396 source_location: test_source(),
6397 }
6398 }
6399
6400 fn create_literal_expr(value: Value) -> ast::Expression {
6401 ast::Expression {
6402 kind: ast::ExpressionKind::Literal(value),
6403 source_location: Some(test_source()),
6404 }
6405 }
6406
6407 #[test]
6408 fn should_reject_data_binding_into_non_spec_data() {
6409 let mut spec = create_test_spec("test");
6414 spec = spec.add_data(create_literal_data("x", Value::Number(1.into())));
6415
6416 spec = spec.add_data(LemmaData {
6418 reference: Reference::from_path(vec!["x".to_string(), "y".to_string()]),
6419 value: ParsedDataValue::Definition {
6420 base: None,
6421 constraints: None,
6422 value: Some(Value::Number(2.into())),
6423 },
6424 source_location: test_source(),
6425 });
6426
6427 let result = build_graph(&spec, &[spec.clone()]);
6428 assert!(
6429 result.is_err(),
6430 "Overriding x.y must fail when x is not a spec reference"
6431 );
6432 }
6433
6434 #[test]
6435 fn should_reject_data_and_rule_name_collision() {
6436 let mut spec = create_test_spec("test");
6441 spec = spec.add_data(create_literal_data("x", Value::Number(1.into())));
6442 spec = spec.add_rule(LemmaRule {
6443 name: "x".to_string(),
6444 expression: create_literal_expr(Value::Number(2.into())),
6445 unless_clauses: Vec::new(),
6446 source_location: test_source(),
6447 });
6448
6449 let result = build_graph(&spec, &[spec.clone()]);
6450 assert!(
6451 result.is_err(),
6452 "Data and rule name collisions should be rejected"
6453 );
6454 }
6455
6456 #[test]
6457 fn test_duplicate_data() {
6458 let mut spec = create_test_spec("test");
6459 spec = spec.add_data(create_literal_data(
6460 "age",
6461 Value::Number(rust_decimal::Decimal::from(25)),
6462 ));
6463 spec = spec.add_data(create_literal_data(
6464 "age",
6465 Value::Number(rust_decimal::Decimal::from(30)),
6466 ));
6467
6468 let result = build_graph(&spec, &[spec.clone()]);
6469 assert!(result.is_err(), "Should detect duplicate data");
6470
6471 let errors = result.unwrap_err();
6472 assert!(errors.iter().any(|e| {
6473 let s = e.to_string();
6474 s.contains("already used") && s.contains("age")
6475 }));
6476 }
6477
6478 #[test]
6479 fn test_duplicate_rule() {
6480 let mut spec = create_test_spec("test");
6481
6482 let rule1 = LemmaRule {
6483 name: "test_rule".to_string(),
6484 expression: create_literal_expr(Value::Boolean(BooleanValue::True)),
6485 unless_clauses: Vec::new(),
6486 source_location: test_source(),
6487 };
6488 let rule2 = LemmaRule {
6489 name: "test_rule".to_string(),
6490 expression: create_literal_expr(Value::Boolean(BooleanValue::False)),
6491 unless_clauses: Vec::new(),
6492 source_location: test_source(),
6493 };
6494
6495 spec = spec.add_rule(rule1);
6496 spec = spec.add_rule(rule2);
6497
6498 let result = build_graph(&spec, &[spec.clone()]);
6499 assert!(result.is_err(), "Should detect duplicate rule");
6500
6501 let errors = result.unwrap_err();
6502 assert!(errors.iter().any(
6503 |e| e.to_string().contains("Duplicate rule") && e.to_string().contains("test_rule")
6504 ));
6505 }
6506
6507 #[test]
6508 fn test_missing_data_reference() {
6509 let mut spec = create_test_spec("test");
6510
6511 let missing_data_expr = ast::Expression {
6512 kind: ast::ExpressionKind::Reference(Reference {
6513 segments: Vec::new(),
6514 name: "nonexistent".to_string(),
6515 }),
6516 source_location: Some(test_source()),
6517 };
6518
6519 let rule = LemmaRule {
6520 name: "test_rule".to_string(),
6521 expression: missing_data_expr,
6522 unless_clauses: Vec::new(),
6523 source_location: test_source(),
6524 };
6525 spec = spec.add_rule(rule);
6526
6527 let result = build_graph(&spec, &[spec.clone()]);
6528 assert!(result.is_err(), "Should detect missing data");
6529
6530 let errors = result.unwrap_err();
6531 assert!(errors
6532 .iter()
6533 .any(|e| e.to_string().contains("Reference 'nonexistent' not found")));
6534 }
6535
6536 #[test]
6537 fn test_missing_spec_reference() {
6538 let mut spec = create_test_spec("test");
6539
6540 let data = LemmaData {
6541 reference: Reference {
6542 segments: Vec::new(),
6543 name: "contract".to_string(),
6544 },
6545 value: ParsedDataValue::Import(crate::parsing::ast::SpecRef::same_repository(
6546 "nonexistent",
6547 )),
6548 source_location: test_source(),
6549 };
6550 spec = spec.add_data(data);
6551
6552 let result = build_graph(&spec, &[spec.clone()]);
6553 assert!(result.is_err(), "Should detect missing spec");
6554
6555 let errors = result.unwrap_err();
6556 assert!(
6557 errors.iter().any(|e| e.to_string().contains("nonexistent")),
6558 "Error should mention nonexistent spec: {:?}",
6559 errors.iter().map(|e| e.to_string()).collect::<Vec<_>>()
6560 );
6561 }
6562
6563 #[test]
6564 fn test_data_reference_conversion() {
6565 let mut spec = create_test_spec("test");
6566 spec = spec.add_data(create_literal_data(
6567 "age",
6568 Value::Number(rust_decimal::Decimal::from(25)),
6569 ));
6570
6571 let age_expr = ast::Expression {
6572 kind: ast::ExpressionKind::Reference(Reference {
6573 segments: Vec::new(),
6574 name: "age".to_string(),
6575 }),
6576 source_location: Some(test_source()),
6577 };
6578
6579 let rule = LemmaRule {
6580 name: "test_rule".to_string(),
6581 expression: age_expr,
6582 unless_clauses: Vec::new(),
6583 source_location: test_source(),
6584 };
6585 spec = spec.add_rule(rule);
6586
6587 let result = build_graph(&spec, &[spec.clone()]);
6588 assert!(result.is_ok(), "Should build graph successfully");
6589
6590 let graph = result.unwrap();
6591 let rule_node = graph.rules().values().next().unwrap();
6592
6593 assert!(matches!(
6594 rule_node.branches[0].1.kind,
6595 ExpressionKind::DataPath(_)
6596 ));
6597 }
6598
6599 #[test]
6600 fn test_rule_reference_conversion() {
6601 let mut spec = create_test_spec("test");
6602
6603 let rule1_expr = ast::Expression {
6604 kind: ast::ExpressionKind::Reference(Reference {
6605 segments: Vec::new(),
6606 name: "age".to_string(),
6607 }),
6608 source_location: Some(test_source()),
6609 };
6610
6611 let rule1 = LemmaRule {
6612 name: "rule1".to_string(),
6613 expression: rule1_expr,
6614 unless_clauses: Vec::new(),
6615 source_location: test_source(),
6616 };
6617 spec = spec.add_rule(rule1);
6618
6619 let rule2_expr = ast::Expression {
6620 kind: ast::ExpressionKind::Reference(Reference {
6621 segments: Vec::new(),
6622 name: "rule1".to_string(),
6623 }),
6624 source_location: Some(test_source()),
6625 };
6626
6627 let rule2 = LemmaRule {
6628 name: "rule2".to_string(),
6629 expression: rule2_expr,
6630 unless_clauses: Vec::new(),
6631 source_location: test_source(),
6632 };
6633 spec = spec.add_rule(rule2);
6634
6635 spec = spec.add_data(create_literal_data(
6636 "age",
6637 Value::Number(rust_decimal::Decimal::from(25)),
6638 ));
6639
6640 let result = build_graph(&spec, &[spec.clone()]);
6641 assert!(result.is_ok(), "Should build graph successfully");
6642
6643 let graph = result.unwrap();
6644 let rule2_node = graph
6645 .rules()
6646 .get(&RulePath {
6647 segments: Vec::new(),
6648 rule: "rule2".to_string(),
6649 })
6650 .unwrap();
6651
6652 assert_eq!(rule2_node.depends_on_rules.len(), 1);
6653 assert!(matches!(
6654 rule2_node.branches[0].1.kind,
6655 ExpressionKind::RulePath(_)
6656 ));
6657 }
6658
6659 #[test]
6660 fn test_collect_multiple_errors() {
6661 let mut spec = create_test_spec("test");
6662 spec = spec.add_data(create_literal_data(
6663 "age",
6664 Value::Number(rust_decimal::Decimal::from(25)),
6665 ));
6666 spec = spec.add_data(create_literal_data(
6667 "age",
6668 Value::Number(rust_decimal::Decimal::from(30)),
6669 ));
6670
6671 let missing_data_expr = ast::Expression {
6672 kind: ast::ExpressionKind::Reference(Reference {
6673 segments: Vec::new(),
6674 name: "nonexistent".to_string(),
6675 }),
6676 source_location: Some(test_source()),
6677 };
6678
6679 let rule = LemmaRule {
6680 name: "test_rule".to_string(),
6681 expression: missing_data_expr,
6682 unless_clauses: Vec::new(),
6683 source_location: test_source(),
6684 };
6685 spec = spec.add_rule(rule);
6686
6687 let result = build_graph(&spec, &[spec.clone()]);
6688 assert!(result.is_err(), "Should collect multiple errors");
6689
6690 let errors = result.unwrap_err();
6691 assert!(errors.len() >= 2, "Should have at least 2 errors");
6692 assert!(errors
6693 .iter()
6694 .any(|e| e.to_string().contains("already used")));
6695 assert!(errors
6696 .iter()
6697 .any(|e| e.to_string().contains("Reference 'nonexistent' not found")));
6698 }
6699
6700 #[test]
6701 fn test_type_registration_collects_multiple_errors() {
6702 use crate::parsing::ast::{DataValue, ParentType, PrimitiveKind, SpecRef};
6703
6704 let type_source = Source::new(
6705 crate::parsing::source::SourceType::Volatile,
6706 Span {
6707 start: 0,
6708 end: 0,
6709 line: 1,
6710 col: 0,
6711 },
6712 );
6713 let spec_a = create_test_spec("spec_a")
6714 .with_source_type(crate::parsing::source::SourceType::Volatile)
6715 .add_data(LemmaData {
6716 reference: Reference::local("dep".to_string()),
6717 value: DataValue::Import(SpecRef::same_repository("spec_b")),
6718 source_location: type_source.clone(),
6719 })
6720 .add_data(LemmaData {
6721 reference: Reference::local("money".to_string()),
6722 value: DataValue::Definition {
6723 base: Some(ParentType::Primitive {
6724 primitive: PrimitiveKind::Number,
6725 }),
6726 constraints: None,
6727 value: None,
6728 },
6729 source_location: type_source.clone(),
6730 })
6731 .add_data(LemmaData {
6732 reference: Reference::local("money".to_string()),
6733 value: DataValue::Definition {
6734 base: Some(ParentType::Primitive {
6735 primitive: PrimitiveKind::Number,
6736 }),
6737 constraints: None,
6738 value: None,
6739 },
6740 source_location: type_source,
6741 });
6742
6743 let type_source_b = Source::new(
6744 crate::parsing::source::SourceType::Volatile,
6745 Span {
6746 start: 0,
6747 end: 0,
6748 line: 1,
6749 col: 0,
6750 },
6751 );
6752 let spec_b = create_test_spec("spec_b")
6753 .with_source_type(crate::parsing::source::SourceType::Volatile)
6754 .add_data(LemmaData {
6755 reference: Reference::local("length".to_string()),
6756 value: DataValue::Definition {
6757 base: Some(ParentType::Primitive {
6758 primitive: PrimitiveKind::Number,
6759 }),
6760 constraints: None,
6761 value: None,
6762 },
6763 source_location: type_source_b.clone(),
6764 })
6765 .add_data(LemmaData {
6766 reference: Reference::local("length".to_string()),
6767 value: DataValue::Definition {
6768 base: Some(ParentType::Primitive {
6769 primitive: PrimitiveKind::Number,
6770 }),
6771 constraints: None,
6772 value: None,
6773 },
6774 source_location: type_source_b,
6775 });
6776
6777 let mut sources = HashMap::new();
6778 sources.insert(
6779 crate::parsing::source::SourceType::Volatile.to_string(),
6780 "spec spec_a\nuses dep: spec_b\ndata money: number\ndata money: number".to_string(),
6781 );
6782 sources.insert(
6783 crate::parsing::source::SourceType::Volatile.to_string(),
6784 "spec spec_b\ndata length: number\ndata length: number".to_string(),
6785 );
6786
6787 let result = build_graph(&spec_a, &[spec_a.clone(), spec_b.clone()]);
6788 assert!(
6789 result.is_err(),
6790 "Should fail with duplicate type/data errors"
6791 );
6792 }
6793
6794 #[test]
6799 fn spec_ref_resolves_to_single_spec_by_name() {
6800 let code = r#"spec myspec
6801data x: 10
6802
6803spec consumer
6804uses m: myspec
6805rule result: m.x"#;
6806 let specs = crate::parse(
6807 code,
6808 crate::parsing::source::SourceType::Volatile,
6809 &crate::ResourceLimits::default(),
6810 )
6811 .unwrap()
6812 .into_flattened_specs();
6813 let consumer = specs.iter().find(|d| d.name == "consumer").unwrap();
6814
6815 let graph = build_graph(consumer, &specs).unwrap();
6816 let data_path = DataPath {
6817 segments: vec![PathSegment {
6818 data: "m".to_string(),
6819 spec: "myspec".to_string(),
6820 }],
6821 data: "x".to_string(),
6822 };
6823 assert!(
6824 graph.data.contains_key(&data_path),
6825 "Ref should resolve to myspec. Data: {:?}",
6826 graph.data.keys().collect::<Vec<_>>()
6827 );
6828 }
6829
6830 #[test]
6831 fn spec_ref_to_nonexistent_spec_is_error() {
6832 let code = r#"spec myspec
6833data x: 10
6834
6835spec consumer
6836uses m: nonexistent
6837rule result: m.x"#;
6838 let specs = crate::parse(
6839 code,
6840 crate::parsing::source::SourceType::Volatile,
6841 &crate::ResourceLimits::default(),
6842 )
6843 .unwrap()
6844 .into_flattened_specs();
6845 let consumer = specs.iter().find(|d| d.name == "consumer").unwrap();
6846 let result = build_graph(consumer, &specs);
6847 assert!(result.is_err(), "Should fail for non-existent spec");
6848 }
6849
6850 #[test]
6855 fn import_alias_registered_in_graph() {
6856 let code = r#"
6857spec inner
6858data x: number -> default 1
6859
6860spec outer
6861uses i: inner
6862rule r: i.x
6863"#;
6864 let specs = crate::parse(
6865 code,
6866 crate::parsing::source::SourceType::Volatile,
6867 &crate::ResourceLimits::default(),
6868 )
6869 .unwrap()
6870 .into_flattened_specs();
6871 let outer = specs.iter().find(|s| s.name == "outer").unwrap();
6872 let graph = build_graph(outer, &specs).expect("uses i: inner must plan");
6873
6874 let alias_path = DataPath {
6875 segments: Vec::new(),
6876 data: "i".to_string(),
6877 };
6878 match graph.data().get(&alias_path) {
6879 Some(DataDefinition::Import { spec, .. }) => {
6880 assert_eq!(spec.name, "inner");
6881 }
6882 other => panic!(
6883 "alias path 'i' must be DataDefinition::Import, got {:?}",
6884 other
6885 ),
6886 }
6887
6888 let nested_path = DataPath {
6889 segments: vec![PathSegment {
6890 data: "i".to_string(),
6891 spec: "inner".to_string(),
6892 }],
6893 data: "x".to_string(),
6894 };
6895 assert!(
6896 graph.data().contains_key(&nested_path),
6897 "nested data i.x must exist after nested build_spec"
6898 );
6899 }
6900
6901 #[test]
6902 fn self_reference_is_error() {
6903 let code = "spec myspec\nuses m: myspec";
6904 let specs = crate::parse(
6905 code,
6906 crate::parsing::source::SourceType::Volatile,
6907 &crate::ResourceLimits::default(),
6908 )
6909 .unwrap()
6910 .into_flattened_specs();
6911 let result = build_graph(&specs[0], &specs);
6912 assert!(result.is_err(), "Self-reference should be an error");
6913 let errors = result.unwrap_err();
6914 let joined: String = errors
6915 .iter()
6916 .map(|e| e.to_string())
6917 .collect::<Vec<_>>()
6918 .join(" ");
6919 assert!(
6920 joined.contains("cannot reference itself") && joined.contains("myspec"),
6921 "Error should name self-reference: {:?}",
6922 errors.iter().map(|e| e.to_string()).collect::<Vec<_>>()
6923 );
6924 }
6925}
6926
6927#[derive(Debug, Clone)]
6934pub struct ResolvedSpecTypes {
6935 pub resolved: HashMap<String, LemmaType>,
6938
6939 pub declared_defaults: HashMap<String, ValueKind>,
6944
6945 pub unit_index: HashMap<String, LemmaType>,
6948}
6949
6950#[derive(Debug, Clone, PartialEq)]
6952pub(crate) struct DataTypeDef {
6953 pub parent: ParentType,
6954 pub constraints: Option<Vec<Constraint>>,
6955 pub source: crate::parsing::source::Source,
6956 pub name: String,
6957 pub bound_literal: Option<ast::Value>,
6959}
6960
6961#[derive(Debug, Clone)]
6965pub(crate) struct TypeResolver<'a> {
6966 data_types: Vec<(Arc<LemmaSpec>, HashMap<String, DataTypeDef>)>,
6967 context: &'a Context,
6968 all_registered_specs: Vec<(Arc<LemmaRepository>, Arc<LemmaSpec>)>,
6969}
6970
6971fn inferred_parent_type_from_literal(value: &ast::Value) -> ParentType {
6973 match value {
6974 ast::Value::Number(_) => ParentType::Primitive {
6975 primitive: PrimitiveKind::Number,
6976 },
6977 ast::Value::Text(_) => ParentType::Primitive {
6978 primitive: PrimitiveKind::Text,
6979 },
6980 ast::Value::Boolean(_) => ParentType::Primitive {
6981 primitive: PrimitiveKind::Boolean,
6982 },
6983 ast::Value::Date(_) => ParentType::Primitive {
6984 primitive: PrimitiveKind::Date,
6985 },
6986 ast::Value::Time(_) => ParentType::Primitive {
6987 primitive: PrimitiveKind::Time,
6988 },
6989 ast::Value::NumberWithUnit(_, _) => ParentType::Primitive {
6990 primitive: PrimitiveKind::Quantity,
6991 },
6992 ast::Value::Calendar(_, _) => ParentType::Primitive {
6993 primitive: PrimitiveKind::Calendar,
6994 },
6995 ast::Value::Range(left, right) => {
6996 let primitive = match (left.as_ref(), right.as_ref()) {
6997 (ast::Value::Number(_), ast::Value::Number(_)) => PrimitiveKind::NumberRange,
6998 (ast::Value::Date(_), ast::Value::Date(_)) => PrimitiveKind::DateRange,
6999 (
7000 ast::Value::NumberWithUnit(_, u1),
7001 ast::Value::NumberWithUnit(_, u2),
7002 ) if u1 == u2 && matches!(u1.as_str(), "percent" | "permille") => {
7003 PrimitiveKind::RatioRange
7004 }
7005 (ast::Value::NumberWithUnit(_, _), ast::Value::NumberWithUnit(_, _)) => {
7006 PrimitiveKind::QuantityRange
7007 }
7008 (ast::Value::Calendar(_, _), ast::Value::Calendar(_, _)) => {
7009 PrimitiveKind::CalendarRange
7010 }
7011 _ => unreachable!(
7012 "BUG: inferred_parent_type_from_literal called on invalid range literal; planning must validate range endpoint types first"
7013 ),
7014 };
7015 ParentType::Primitive { primitive }
7016 }
7017 }
7018}
7019
7020impl<'a> TypeResolver<'a> {
7021 pub fn new(context: &'a Context) -> Self {
7022 TypeResolver {
7023 data_types: Vec::new(),
7024 context,
7025 all_registered_specs: Vec::new(),
7026 }
7027 }
7028
7029 pub fn is_registered(&self, spec: &Arc<LemmaSpec>) -> bool {
7030 self.all_registered_specs
7031 .iter()
7032 .any(|(_, s)| Arc::ptr_eq(s, spec))
7033 }
7034
7035 pub fn register_all(
7037 &mut self,
7038 repository: &Arc<LemmaRepository>,
7039 spec: &Arc<LemmaSpec>,
7040 ) -> Vec<Error> {
7041 if !self
7042 .all_registered_specs
7043 .iter()
7044 .any(|(_, s)| Arc::ptr_eq(s, spec))
7045 {
7046 self.all_registered_specs
7047 .push((Arc::clone(repository), Arc::clone(spec)));
7048 }
7049
7050 let mut errors = Vec::new();
7051 for data in &spec.data {
7052 match &data.value {
7053 ParsedDataValue::Definition {
7054 base,
7055 constraints,
7056 value,
7057 } => {
7058 if matches!(
7059 (base.as_ref(), constraints.as_ref(), value.as_ref()),
7060 (None, None, Some(Value::NumberWithUnit(_, _)),)
7061 ) {
7062 continue;
7063 }
7064 let name = &data.reference.name;
7065 let parent = match (base.as_ref(), value.as_ref()) {
7066 (Some(b), _) => b.clone(),
7067 (None, Some(v)) => inferred_parent_type_from_literal(v),
7068 (None, None) => {
7069 errors.push(Error::validation_with_context(
7070 format!(
7071 "Data '{name}' in spec '{}' must declare a type or a literal value",
7072 spec.name
7073 ),
7074 Some(data.source_location.clone()),
7075 None::<String>,
7076 Some(Arc::clone(spec)),
7077 None,
7078 ));
7079 continue;
7080 }
7081 };
7082 let ftd = DataTypeDef {
7083 parent,
7084 constraints: constraints.clone(),
7085 source: data.source_location.clone(),
7086 name: name.clone(),
7087 bound_literal: value.clone(),
7088 };
7089 if let Err(e) = self.register_type(spec, ftd) {
7090 errors.push(e);
7091 }
7092 }
7093 ParsedDataValue::Fill(_) | ParsedDataValue::Import(_) => {}
7094 }
7095 }
7096 errors
7097 }
7098
7099 pub fn register_type(&mut self, spec: &Arc<LemmaSpec>, def: DataTypeDef) -> Result<(), Error> {
7101 let spec_types = if let Some(pos) = self
7102 .data_types
7103 .iter()
7104 .position(|(s, _)| Arc::ptr_eq(s, spec))
7105 {
7106 &mut self.data_types[pos].1
7107 } else {
7108 self.data_types.push((Arc::clone(spec), HashMap::new()));
7109 let last = self.data_types.len() - 1;
7110 &mut self.data_types[last].1
7111 };
7112 if spec_types.contains_key(&def.name) {
7113 return Err(Error::validation_with_context(
7114 format!(
7115 "The name '{}' is already used for data in this spec.",
7116 def.name
7117 ),
7118 Some(def.source.clone()),
7119 None::<String>,
7120 Some(Arc::clone(spec)),
7121 None,
7122 ));
7123 }
7124 spec_types.insert(def.name.clone(), def);
7125 Ok(())
7126 }
7127
7128 pub fn resolve_and_validate(
7131 &self,
7132 spec: &Arc<LemmaSpec>,
7133 at: &EffectiveDate,
7134 ) -> Result<ResolvedSpecTypes, Vec<Error>> {
7135 let mut resolved_types = self.resolve_types_internal(spec, at)?;
7136 let mut errors = Vec::new();
7137
7138 let type_sources: std::collections::HashMap<String, Source> = resolved_types
7140 .resolved
7141 .keys()
7142 .filter_map(|type_name| {
7143 self.data_types
7144 .iter()
7145 .find(|(s, _)| Arc::ptr_eq(s, spec))
7146 .and_then(|(_, defs)| defs.get(type_name.as_str()))
7147 .map(|ftd| (type_name.clone(), ftd.source.clone()))
7148 })
7149 .collect();
7150
7151 let decomp_errors = resolve_quantity_decompositions(
7154 &spec.name,
7155 &mut resolved_types.resolved,
7156 &mut resolved_types.unit_index,
7157 &type_sources,
7158 );
7159 errors.extend(decomp_errors);
7160
7161 for (type_name, lemma_type) in resolved_types.resolved.iter_mut() {
7162 let source = type_sources.get(type_name).cloned().unwrap_or_else(|| {
7163 unreachable!(
7164 "BUG: resolved type '{}' has no corresponding DataTypeDef in spec '{}'",
7165 type_name, spec.name
7166 )
7167 });
7168 if let Err(message) = semantics::finalize_quantity_unit_constraint_magnitudes(
7169 &mut lemma_type.specifications,
7170 resolved_types.declared_defaults.get(type_name),
7171 type_name,
7172 ) {
7173 errors.push(Error::validation_with_context(
7174 format!(
7175 "Type '{}' has invalid quantity unit constraints: {}",
7176 type_name, message
7177 ),
7178 Some(source),
7179 None::<String>,
7180 Some(Arc::clone(spec)),
7181 None,
7182 ));
7183 }
7184 }
7185
7186 sync_unit_index_from_resolved(&resolved_types.resolved, &mut resolved_types.unit_index);
7187
7188 for lemma_type in resolved_types.unit_index.values_mut() {
7189 let type_name = lemma_type
7190 .name
7191 .as_deref()
7192 .or_else(|| lemma_type.quantity_family_name())
7193 .map(str::to_string);
7194 let Some(type_name) = type_name else {
7195 continue;
7196 };
7197 if !lemma_type.is_quantity() {
7198 continue;
7199 }
7200 if let Err(message) = semantics::finalize_quantity_unit_constraint_magnitudes(
7201 &mut lemma_type.specifications,
7202 resolved_types.declared_defaults.get(type_name.as_str()),
7203 type_name.as_str(),
7204 ) {
7205 let source = type_sources
7206 .get(type_name.as_str())
7207 .cloned()
7208 .or_else(|| {
7209 self.data_types.iter().find_map(|(_, defs)| {
7210 defs.get(type_name.as_str()).map(|def| def.source.clone())
7211 })
7212 })
7213 .unwrap_or_else(|| {
7214 unreachable!(
7215 "BUG: quantity type '{}' in unit_index has no DataTypeDef source",
7216 type_name
7217 )
7218 });
7219 errors.push(Error::validation_with_context(
7220 format!(
7221 "Type '{}' has invalid quantity unit constraints: {}",
7222 type_name, message
7223 ),
7224 Some(source),
7225 None::<String>,
7226 Some(Arc::clone(spec)),
7227 None,
7228 ));
7229 }
7230 }
7231
7232 let mut validated_in_unit_index: HashSet<String> = HashSet::new();
7233 for lemma_type in resolved_types.unit_index.values() {
7234 let Some(type_name) = lemma_type.name.as_deref() else {
7235 continue;
7236 };
7237 if !lemma_type.is_quantity() || !validated_in_unit_index.insert(type_name.to_string()) {
7238 continue;
7239 }
7240 let source = type_sources
7241 .get(type_name)
7242 .cloned()
7243 .or_else(|| {
7244 self.data_types
7245 .iter()
7246 .find_map(|(_, defs)| defs.get(type_name).map(|def| def.source.clone()))
7247 })
7248 .unwrap_or_else(|| {
7249 unreachable!(
7250 "BUG: quantity type '{}' in unit_index has no DataTypeDef source",
7251 type_name
7252 )
7253 });
7254 errors.extend(validate_type_specifications(
7255 &lemma_type.specifications,
7256 resolved_types.declared_defaults.get(type_name),
7257 type_name,
7258 &source,
7259 Some(Arc::clone(spec)),
7260 ));
7261 }
7262
7263 for (type_name, lemma_type) in &resolved_types.resolved {
7264 let source = type_sources.get(type_name).cloned().unwrap_or_else(|| {
7265 unreachable!(
7266 "BUG: resolved type '{}' has no corresponding DataTypeDef in spec '{}'",
7267 type_name, spec.name
7268 )
7269 });
7270 let mut spec_errors = validate_type_specifications(
7271 &lemma_type.specifications,
7272 resolved_types.declared_defaults.get(type_name),
7273 type_name,
7274 &source,
7275 Some(Arc::clone(spec)),
7276 );
7277 errors.append(&mut spec_errors);
7278 }
7279
7280 if errors.is_empty() {
7281 Ok(resolved_types)
7282 } else {
7283 Err(errors)
7284 }
7285 }
7286
7287 fn resolve_types_internal(
7292 &self,
7293 spec: &Arc<LemmaSpec>,
7294 at: &EffectiveDate,
7295 ) -> Result<ResolvedSpecTypes, Vec<Error>> {
7296 let mut resolved = HashMap::new();
7297 let mut declared_defaults: HashMap<String, ValueKind> = HashMap::new();
7298 let mut visited: Vec<(Arc<LemmaSpec>, String)> = Vec::new();
7299
7300 if let Some((_, spec_types)) = self.data_types.iter().find(|(s, _)| Arc::ptr_eq(s, spec)) {
7301 for type_name in spec_types.keys() {
7302 match self.resolve_type_internal(spec, type_name, &mut visited, at) {
7303 Ok(Some((resolved_type, declared_default))) => {
7304 resolved.insert(type_name.clone(), resolved_type);
7305 if let Some(dv) = declared_default {
7306 declared_defaults.insert(type_name.clone(), dv);
7307 }
7308 }
7309 Ok(None) => {
7310 unreachable!(
7311 "BUG: registered type '{}' could not be resolved (spec='{}')",
7312 type_name, spec.name
7313 );
7314 }
7315 Err(es) => return Err(es),
7316 }
7317 visited.clear();
7318 }
7319 }
7320
7321 let mut unit_index_tmp: HashMap<String, (LemmaType, Option<DataTypeDef>)> = HashMap::new();
7323 let mut errors = Vec::new();
7324
7325 let prim_ratio = semantics::primitive_ratio();
7326 for unit in Self::extract_units_from_type(&prim_ratio.specifications) {
7327 unit_index_tmp.insert(unit, (prim_ratio.clone(), None));
7328 }
7329
7330 for (type_name, resolved_type) in &resolved {
7331 let data_type_def = self
7332 .data_types
7333 .iter()
7334 .find(|(s, _)| Arc::ptr_eq(s, spec))
7335 .and_then(|(_, defs)| defs.get(type_name.as_str()))
7336 .expect("BUG: type was resolved but not in registry");
7337 let e: Result<(), Error> = if resolved_type.is_quantity() {
7338 Self::add_quantity_units_to_index(
7339 spec,
7340 &mut unit_index_tmp,
7341 resolved_type,
7342 data_type_def,
7343 )
7344 } else if resolved_type.is_ratio() {
7345 Self::add_ratio_units_to_index(
7346 spec,
7347 &mut unit_index_tmp,
7348 resolved_type,
7349 data_type_def,
7350 )
7351 } else {
7352 Ok(())
7353 };
7354 if let Err(e) = e {
7355 errors.push(e);
7356 }
7357 }
7358
7359 for data_row in &spec.data {
7360 let ParsedDataValue::Import(spec_ref) = &data_row.value else {
7361 continue;
7362 };
7363 let (_, imported_spec) =
7364 match self.resolve_spec_for_import(spec, spec_ref, &data_row.source_location, at) {
7365 Ok(x) => x,
7366 Err(_) => {
7367 continue;
7371 }
7372 };
7373 let Some((_, imported_type_map)) = self
7374 .data_types
7375 .iter()
7376 .find(|(s, _)| Arc::ptr_eq(s, &imported_spec))
7377 else {
7378 continue;
7379 };
7380
7381 let mut import_visited: Vec<(Arc<LemmaSpec>, String)> = Vec::new();
7382 for (type_name, def) in imported_type_map.iter() {
7383 if matches!(def.parent, ParentType::Qualified { .. }) {
7384 continue;
7385 }
7386 match self.resolve_type_internal(
7387 &imported_spec,
7388 type_name.as_str(),
7389 &mut import_visited,
7390 at,
7391 ) {
7392 Ok(Some((resolved_type, _))) => {
7393 if resolved_type.is_quantity() {
7394 if let Err(e) = Self::add_quantity_units_to_index(
7395 spec,
7396 &mut unit_index_tmp,
7397 &resolved_type,
7398 def,
7399 ) {
7400 errors.push(e);
7401 }
7402 } else if resolved_type.is_ratio() {
7403 if let Err(e) = Self::add_ratio_units_to_index(
7404 spec,
7405 &mut unit_index_tmp,
7406 &resolved_type,
7407 def,
7408 ) {
7409 errors.push(e);
7410 }
7411 }
7412 }
7413 Ok(None) => {}
7414 Err(_) => {
7415 }
7418 }
7419 import_visited.clear();
7420 }
7421 }
7422
7423 if !errors.is_empty() {
7424 return Err(errors);
7425 }
7426
7427 let unit_index = unit_index_tmp
7428 .into_iter()
7429 .map(|(k, (lt, _))| (k, lt))
7430 .collect();
7431
7432 Ok(ResolvedSpecTypes {
7433 resolved,
7434 declared_defaults,
7435 unit_index,
7436 })
7437 }
7438
7439 fn resolve_type_internal(
7440 &self,
7441 spec: &Arc<LemmaSpec>,
7442 name: &str,
7443 visited: &mut Vec<(Arc<LemmaSpec>, String)>,
7444 at: &EffectiveDate,
7445 ) -> Result<Option<(LemmaType, Option<ValueKind>)>, Vec<Error>> {
7446 if visited
7447 .iter()
7448 .any(|(s, n)| Arc::ptr_eq(s, spec) && n == name)
7449 {
7450 let source_location = self
7451 .data_types
7452 .iter()
7453 .find(|(s, _)| Arc::ptr_eq(s, spec))
7454 .and_then(|(_, dt)| dt.get(name))
7455 .map(|ftd| ftd.source.clone())
7456 .unwrap_or_else(|| {
7457 unreachable!(
7458 "BUG: circular dependency detected for type '{}::{}' but type definition not found in registry",
7459 spec.name, name
7460 )
7461 });
7462 return Err(vec![Error::validation_with_context(
7463 format!(
7464 "Circular dependency detected in type resolution: {}::{}",
7465 spec.name, name
7466 ),
7467 Some(source_location),
7468 None::<String>,
7469 Some(Arc::clone(spec)),
7470 None,
7471 )]);
7472 }
7473 visited.push((Arc::clone(spec), name.to_string()));
7474
7475 let ftd = match self
7476 .data_types
7477 .iter()
7478 .find(|(s, _)| Arc::ptr_eq(s, spec))
7479 .and_then(|(_, dt)| dt.get(name))
7480 {
7481 Some(def) => def.clone(),
7482 None => {
7483 if let Some(pos) = visited
7484 .iter()
7485 .position(|(s, n)| Arc::ptr_eq(s, spec) && n == name)
7486 {
7487 visited.remove(pos);
7488 }
7489 return Ok(None);
7490 }
7491 };
7492
7493 let parent = ftd.parent.clone();
7494 let constraints = ftd.constraints.clone();
7495
7496 let (parent_specs, parent_declared_default) = match self.resolve_parent(
7497 spec,
7498 &parent,
7499 visited,
7500 &ftd.source,
7501 at,
7502 ) {
7503 Ok(Some(pair)) => pair,
7504 Ok(None) => {
7505 if let Some(pos) = visited
7506 .iter()
7507 .position(|(s, n)| Arc::ptr_eq(s, spec) && n == name)
7508 {
7509 visited.remove(pos);
7510 }
7511 return Err(vec![Error::validation_with_context(
7512 format!("Unknown parent '{}' for data definition. Parent must be defined before use. Valid primitive types are: boolean, quantity, number, ratio, text, date, time, duration, percent", parent),
7513 Some(ftd.source.clone()),
7514 None::<String>,
7515 Some(Arc::clone(spec)),
7516 None,
7517 )]);
7518 }
7519 Err(es) => {
7520 if let Some(pos) = visited
7521 .iter()
7522 .position(|(s, n)| Arc::ptr_eq(s, spec) && n == name)
7523 {
7524 visited.remove(pos);
7525 }
7526 return Err(es);
7527 }
7528 };
7529
7530 let mut declared_default = parent_declared_default;
7531 let final_specs = if let Some(constraints) = &constraints {
7532 let constraint_type_name = constraint_application_type_name(&parent, name);
7533 match apply_constraints_to_spec(
7534 spec,
7535 &constraint_type_name,
7536 parent_specs,
7537 constraints,
7538 &ftd.source,
7539 &mut declared_default,
7540 ) {
7541 Ok(specs) => specs,
7542 Err(errors) => {
7543 if let Some(pos) = visited
7544 .iter()
7545 .position(|(s, n)| Arc::ptr_eq(s, spec) && n == name)
7546 {
7547 visited.remove(pos);
7548 }
7549 return Err(errors);
7550 }
7551 }
7552 } else {
7553 parent_specs
7554 };
7555
7556 if let Some(pos) = visited
7557 .iter()
7558 .position(|(s, n)| Arc::ptr_eq(s, spec) && n == name)
7559 {
7560 visited.remove(pos);
7561 }
7562
7563 let extends = {
7564 let parent_display = parent.to_string();
7565 let import_target: Option<Arc<LemmaSpec>> =
7566 if let ParentType::Qualified { spec_alias, .. } = &parent {
7567 let spec_ref = ast::SpecRef::same_repository(spec_alias.clone());
7568 match self.resolve_spec_for_import(spec, &spec_ref, &ftd.source, at) {
7569 Ok((_, arc)) => Some(arc),
7570 Err(e) => return Err(vec![e]),
7571 }
7572 } else {
7573 None
7574 };
7575
7576 let lookup_for_family: Option<(Arc<LemmaSpec>, String)> = match &parent {
7577 ParentType::Primitive { .. } => None,
7578 ParentType::Custom { name } => Some((Arc::clone(spec), name.clone())),
7579 ParentType::Qualified { inner, .. } => {
7580 let target = import_target.as_ref().expect(
7581 "BUG: qualified parent missing resolved import target for family lookup",
7582 );
7583 match inner.as_ref() {
7584 ParentType::Custom { name } => Some((Arc::clone(target), name.clone())),
7585 ParentType::Primitive { .. } => None,
7586 ParentType::Qualified { .. } => {
7587 return Err(vec![Error::validation_with_context(
7588 "Nested qualified parent types are invalid",
7589 Some(ftd.source.clone()),
7590 None::<String>,
7591 Some(Arc::clone(spec)),
7592 None,
7593 )]);
7594 }
7595 }
7596 }
7597 };
7598
7599 let family = match &lookup_for_family {
7600 None => name.to_string(),
7601 Some((r, pn)) => match self.resolve_type_internal(r, pn.as_str(), visited, at) {
7602 Ok(Some((parent_type, _))) => parent_type
7603 .quantity_family_name()
7604 .map(String::from)
7605 .unwrap_or_else(|| name.to_string()),
7606 Ok(None) => name.to_string(),
7607 Err(es) => return Err(es),
7608 },
7609 };
7610
7611 let defining_spec = if let Some(ref arc) = import_target {
7612 TypeDefiningSpec::Import {
7613 spec: Arc::clone(arc),
7614 }
7615 } else {
7616 TypeDefiningSpec::Local
7617 };
7618
7619 TypeExtends::Custom {
7620 parent: parent_display,
7621 family,
7622 defining_spec,
7623 }
7624 };
7625
7626 let declared_default = match &ftd.bound_literal {
7627 Some(lit) => match semantics::parser_value_to_value_kind(lit, &final_specs) {
7628 Ok(vk) => Some(vk),
7629 Err(message) => {
7630 return Err(vec![Error::validation_with_context(
7631 message,
7632 Some(ftd.source.clone()),
7633 None::<String>,
7634 Some(Arc::clone(spec)),
7635 None,
7636 )]);
7637 }
7638 },
7639 None => declared_default,
7640 };
7641
7642 Ok(Some((
7643 LemmaType {
7644 name: Some(name.to_string()),
7645 specifications: final_specs,
7646 extends,
7647 },
7648 declared_default,
7649 )))
7650 }
7651
7652 fn resolve_parent(
7653 &self,
7654 spec: &Arc<LemmaSpec>,
7655 parent: &ParentType,
7656 visited: &mut Vec<(Arc<LemmaSpec>, String)>,
7657 source: &crate::parsing::source::Source,
7658 at: &EffectiveDate,
7659 ) -> Result<Option<(TypeSpecification, Option<ValueKind>)>, Vec<Error>> {
7660 match parent {
7661 ParentType::Primitive { primitive: kind } => {
7662 Ok(Some((semantics::type_spec_for_primitive(*kind), None)))
7663 }
7664 ParentType::Custom { name } => {
7665 let parent_name = name.as_str();
7666 let result = self.resolve_type_internal(spec, parent_name, visited, at);
7667 match result {
7668 Ok(Some((t, declared_default))) => {
7669 Ok(Some((t.specifications, declared_default)))
7670 }
7671 Ok(None) => {
7672 let type_exists = self
7673 .data_types
7674 .iter()
7675 .find(|(s, _)| Arc::ptr_eq(s, spec))
7676 .map(|(_, m)| m.contains_key(parent_name))
7677 .unwrap_or(false);
7678
7679 if !type_exists {
7680 if spec.data.iter().any(|d| {
7681 d.reference.is_local()
7682 && d.reference.name == parent_name
7683 && matches!(&d.value, ParsedDataValue::Import(_))
7684 }) {
7685 return Err(vec![Error::validation_with_context(
7686 format!(
7687 "'{}' names a spec import alias, not a type: use `data x: {}.TypeName` after `uses`",
7688 parent_name, parent_name
7689 ),
7690 Some(source.clone()),
7691 None::<String>,
7692 Some(Arc::clone(spec)),
7693 None,
7694 )]);
7695 }
7696 Err(vec![Error::validation_with_context(
7697 format!("Unknown parent '{}' for data definition. Parent must be defined before use. Valid primitive types are: boolean, quantity, number, ratio, text, date, time, duration, percent", parent),
7698 Some(source.clone()),
7699 None::<String>,
7700 Some(Arc::clone(spec)),
7701 None,
7702 )])
7703 } else {
7704 Ok(None)
7705 }
7706 }
7707 Err(es) => Err(es),
7708 }
7709 }
7710 ParentType::Qualified { spec_alias, inner } => {
7711 let spec_ref = ast::SpecRef::same_repository(spec_alias.clone());
7712 let (_, target_arc) =
7713 match self.resolve_spec_for_import(spec, &spec_ref, source, at) {
7714 Ok(x) => x,
7715 Err(e) => return Err(vec![e]),
7716 };
7717 match inner.as_ref() {
7718 ParentType::Primitive { primitive } => {
7719 Ok(Some((semantics::type_spec_for_primitive(*primitive), None)))
7720 }
7721 ParentType::Custom { name } => {
7722 let result =
7723 self.resolve_type_internal(&target_arc, name.as_str(), visited, at);
7724 match result {
7725 Ok(Some((t, declared_default))) => {
7726 Ok(Some((t.specifications, declared_default)))
7727 }
7728 Ok(None) => {
7729 let type_exists = self
7730 .data_types
7731 .iter()
7732 .find(|(s, _)| Arc::ptr_eq(s, &target_arc))
7733 .map(|(_, m)| m.contains_key(name.as_str()))
7734 .unwrap_or(false);
7735 if !type_exists {
7736 Err(vec![Error::validation_with_context(
7737 format!(
7738 "Type '{}' is not defined in spec '{}' (via import '{}')",
7739 name, target_arc.name, spec_alias
7740 ),
7741 Some(source.clone()),
7742 None::<String>,
7743 Some(Arc::clone(spec)),
7744 None,
7745 )])
7746 } else {
7747 Ok(None)
7748 }
7749 }
7750 Err(es) => Err(es),
7751 }
7752 }
7753 ParentType::Qualified { .. } => Err(vec![Error::validation_with_context(
7754 "Nested qualified parent types are invalid",
7755 Some(source.clone()),
7756 None::<String>,
7757 Some(Arc::clone(spec)),
7758 None,
7759 )]),
7760 }
7761 }
7762 }
7763 }
7764
7765 fn resolve_spec_for_import(
7766 &self,
7767 spec: &Arc<LemmaSpec>,
7768 from: &crate::parsing::ast::SpecRef,
7769 import_site: &crate::parsing::source::Source,
7770 at: &EffectiveDate,
7771 ) -> Result<(Arc<LemmaRepository>, Arc<LemmaSpec>), Error> {
7772 let consumer_repository = self
7773 .all_registered_specs
7774 .iter()
7775 .find(|(_, s)| Arc::ptr_eq(s, spec))
7776 .map(|(r, _)| Arc::clone(r))
7777 .unwrap_or_else(|| self.context.workspace());
7778 discovery::resolve_spec_ref(
7779 self.context,
7780 from,
7781 &consumer_repository,
7782 spec,
7783 at,
7784 Some(import_site.clone()),
7785 )
7786 }
7787
7788 fn add_quantity_units_to_index(
7793 spec: &Arc<LemmaSpec>,
7794 unit_index: &mut HashMap<String, (LemmaType, Option<DataTypeDef>)>,
7795 resolved_type: &LemmaType,
7796 defined_by: &DataTypeDef,
7797 ) -> Result<(), Error> {
7798 let units = Self::extract_units_from_type(&resolved_type.specifications);
7799 for unit in units {
7800 if let Some((existing_type, existing_def)) = unit_index.get(&unit) {
7801 let same_type = existing_def.as_ref() == Some(defined_by);
7802
7803 if same_type {
7804 return Err(Error::validation_with_context(
7805 format!(
7806 "Unit '{}' is defined more than once in type '{}'",
7807 unit, defined_by.name
7808 ),
7809 Some(defined_by.source.clone()),
7810 None::<String>,
7811 Some(Arc::clone(spec)),
7812 None,
7813 ));
7814 }
7815
7816 let existing_name: String = existing_def
7817 .as_ref()
7818 .map(|d| d.name.clone())
7819 .unwrap_or_else(|| existing_type.name());
7820 let current_extends_existing = resolved_type
7821 .extends
7822 .parent_name()
7823 .map(|p| p == existing_name.as_str())
7824 .unwrap_or(false);
7825 let existing_extends_current = existing_type
7826 .extends
7827 .parent_name()
7828 .map(|p| p == defined_by.name.as_str())
7829 .unwrap_or(false);
7830
7831 if existing_type.is_quantity()
7832 && (current_extends_existing || existing_extends_current)
7833 {
7834 if current_extends_existing {
7835 unit_index.insert(unit, (resolved_type.clone(), Some(defined_by.clone())));
7836 }
7837 continue;
7838 }
7839
7840 if existing_type.same_quantity_family(resolved_type) {
7841 continue;
7842 }
7843
7844 return Err(Error::validation_with_context(
7845 format!(
7846 "Ambiguous unit '{}'. Defined in multiple types: '{}' and '{}'",
7847 unit, existing_name, defined_by.name
7848 ),
7849 Some(defined_by.source.clone()),
7850 None::<String>,
7851 Some(Arc::clone(spec)),
7852 None,
7853 ));
7854 }
7855 unit_index.insert(unit, (resolved_type.clone(), Some(defined_by.clone())));
7856 }
7857 Ok(())
7858 }
7859
7860 fn add_ratio_units_to_index(
7861 spec: &Arc<LemmaSpec>,
7862 unit_index: &mut HashMap<String, (LemmaType, Option<DataTypeDef>)>,
7863 resolved_type: &LemmaType,
7864 defined_by: &DataTypeDef,
7865 ) -> Result<(), Error> {
7866 let units = Self::extract_units_from_type(&resolved_type.specifications);
7867 for unit in units {
7868 if let Some((existing_type, existing_def)) = unit_index.get(&unit) {
7869 if existing_type.is_ratio() {
7870 if existing_def.is_none() {
7871 unit_index.insert(
7872 unit.clone(),
7873 (resolved_type.clone(), Some(defined_by.clone())),
7874 );
7875 continue;
7876 }
7877 if existing_type.name() == resolved_type.name() {
7878 continue;
7879 }
7880 if let (
7881 TypeSpecification::Ratio {
7882 units: existing_units,
7883 ..
7884 },
7885 TypeSpecification::Ratio {
7886 units: new_units, ..
7887 },
7888 ) = (&existing_type.specifications, &resolved_type.specifications)
7889 {
7890 let same_factor = existing_units
7891 .iter()
7892 .find(|u| u.name == unit)
7893 .zip(new_units.iter().find(|u| u.name == unit))
7894 .is_some_and(|(eu, nu)| eu.value == nu.value);
7895 if same_factor {
7896 continue;
7897 }
7898 }
7899 let existing_name: String = existing_def
7900 .as_ref()
7901 .map(|d| d.name.clone())
7902 .unwrap_or_else(|| existing_type.name());
7903 return Err(Error::validation_with_context(
7904 format!(
7905 "Ambiguous unit '{}'. Defined in multiple ratio types: '{}' and '{}'",
7906 unit, existing_name, defined_by.name
7907 ),
7908 Some(defined_by.source.clone()),
7909 None::<String>,
7910 Some(Arc::clone(spec)),
7911 None,
7912 ));
7913 }
7914 let existing_name: String = existing_def
7915 .as_ref()
7916 .map(|d| d.name.clone())
7917 .unwrap_or_else(|| existing_type.name());
7918 return Err(Error::validation_with_context(
7919 format!(
7920 "Ambiguous unit '{}'. Defined in multiple types: '{}' and '{}'",
7921 unit, existing_name, defined_by.name
7922 ),
7923 Some(defined_by.source.clone()),
7924 None::<String>,
7925 Some(Arc::clone(spec)),
7926 None,
7927 ));
7928 }
7929 unit_index.insert(unit, (resolved_type.clone(), Some(defined_by.clone())));
7930 }
7931 Ok(())
7932 }
7933
7934 fn extract_units_from_type(specs: &TypeSpecification) -> Vec<String> {
7935 match specs {
7936 TypeSpecification::Quantity { units, .. } => {
7937 units.iter().map(|unit| unit.name.clone()).collect()
7938 }
7939 TypeSpecification::Ratio { units, .. } => {
7940 units.iter().map(|unit| unit.name.clone()).collect()
7941 }
7942 _ => Vec::new(),
7943 }
7944 }
7945}
7946
7947#[cfg(test)]
7948mod type_resolution_tests {
7949 use super::*;
7950 use crate::computation::rational::RationalInteger;
7951 use crate::parse;
7952 use crate::parsing::ast::{
7953 CommandArg, LemmaSpec, ParentType, PrimitiveKind, TypeConstraintCommand,
7954 };
7955 use crate::ResourceLimits;
7956 use rust_decimal::Decimal;
7957 use std::sync::Arc;
7958
7959 fn test_context_and_effective(
7960 specs: &[Arc<LemmaSpec>],
7961 ) -> (&'static Context, &'static EffectiveDate) {
7962 use crate::engine::Context;
7963 let mut ctx = Context::new();
7964 let repository = ctx.workspace();
7965 for s in specs {
7966 ctx.insert_spec(Arc::clone(&repository), Arc::clone(s))
7967 .unwrap();
7968 }
7969 let ctx = Box::leak(Box::new(ctx));
7970 let eff = Box::leak(Box::new(EffectiveDate::Origin));
7971 (ctx, eff)
7972 }
7973
7974 fn dag_and_spec() -> (Vec<Arc<LemmaSpec>>, Arc<LemmaSpec>) {
7975 let spec = LemmaSpec::new("test_spec".to_string());
7976 let arc = Arc::new(spec);
7977 let dag = vec![Arc::clone(&arc)];
7978 (dag, arc)
7979 }
7980
7981 fn resolver_for_code(code: &str) -> (TypeResolver<'static>, Vec<Arc<LemmaSpec>>) {
7982 let specs = parse(
7983 code,
7984 crate::parsing::source::SourceType::Volatile,
7985 &ResourceLimits::default(),
7986 )
7987 .unwrap()
7988 .into_flattened_specs();
7989 let spec_arcs: Vec<Arc<LemmaSpec>> = specs.iter().map(|s| Arc::new(s.clone())).collect();
7990 let (ctx, _) = test_context_and_effective(&spec_arcs);
7991 let repository = ctx.workspace();
7992 let mut resolver = TypeResolver::new(ctx);
7993 for spec_arc in &spec_arcs {
7994 resolver.register_all(&repository, spec_arc);
7995 }
7996 (resolver, spec_arcs)
7997 }
7998
7999 fn resolver_single_spec(code: &str) -> (TypeResolver<'static>, Arc<LemmaSpec>) {
8000 let (resolver, spec_arcs) = resolver_for_code(code);
8001 let spec_arc = spec_arcs.into_iter().next().expect("at least one spec");
8002 (resolver, spec_arc)
8003 }
8004
8005 #[test]
8006 fn test_type_spec_for_primitive_covers_all_variants() {
8007 use crate::parsing::ast::PrimitiveKind;
8008 use crate::planning::semantics::type_spec_for_primitive;
8009
8010 for kind in [
8011 PrimitiveKind::Boolean,
8012 PrimitiveKind::Quantity,
8013 PrimitiveKind::QuantityRange,
8014 PrimitiveKind::Number,
8015 PrimitiveKind::NumberRange,
8016 PrimitiveKind::Percent,
8017 PrimitiveKind::Ratio,
8018 PrimitiveKind::RatioRange,
8019 PrimitiveKind::Text,
8020 PrimitiveKind::Date,
8021 PrimitiveKind::DateRange,
8022 PrimitiveKind::Time,
8023 PrimitiveKind::Calendar,
8024 PrimitiveKind::CalendarRange,
8025 ] {
8026 let spec = type_spec_for_primitive(kind);
8027 assert!(
8028 !matches!(
8029 spec,
8030 crate::planning::semantics::TypeSpecification::Undetermined
8031 ),
8032 "type_spec_for_primitive({:?}) returned Undetermined",
8033 kind
8034 );
8035 }
8036 }
8037
8038 #[test]
8039 fn test_register_data_type_def() {
8040 let (dag, spec_arc) = dag_and_spec();
8041 let (ctx, _) = test_context_and_effective(&dag);
8042 let mut resolver = TypeResolver::new(ctx);
8043 let ftd = DataTypeDef {
8044 parent: ParentType::Primitive {
8045 primitive: PrimitiveKind::Number,
8046 },
8047 constraints: Some(vec![
8048 (
8049 TypeConstraintCommand::Minimum,
8050 vec![CommandArg::Literal(crate::literals::Value::Number(
8051 Decimal::ZERO,
8052 ))],
8053 ),
8054 (
8055 TypeConstraintCommand::Maximum,
8056 vec![CommandArg::Literal(crate::literals::Value::Number(
8057 Decimal::from(150),
8058 ))],
8059 ),
8060 ]),
8061 source: crate::parsing::source::Source::new(
8062 crate::parsing::source::SourceType::Volatile,
8063 crate::parsing::ast::Span {
8064 start: 0,
8065 end: 0,
8066 line: 1,
8067 col: 0,
8068 },
8069 ),
8070 name: "age".to_string(),
8071 bound_literal: None,
8072 };
8073
8074 let result = resolver.register_type(&spec_arc, ftd);
8075 assert!(result.is_ok());
8076 let resolved = resolver
8077 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
8078 .unwrap();
8079 assert!(resolved.resolved.contains_key("age"));
8080 }
8081
8082 #[test]
8083 fn test_register_duplicate_type_fails() {
8084 let (dag, spec_arc) = dag_and_spec();
8085 let (ctx, _) = test_context_and_effective(&dag);
8086 let mut resolver = TypeResolver::new(ctx);
8087 let ftd = DataTypeDef {
8088 parent: ParentType::Primitive {
8089 primitive: PrimitiveKind::Number,
8090 },
8091 constraints: None,
8092 source: crate::parsing::source::Source::new(
8093 crate::parsing::source::SourceType::Volatile,
8094 crate::parsing::ast::Span {
8095 start: 0,
8096 end: 0,
8097 line: 1,
8098 col: 0,
8099 },
8100 ),
8101 name: "money".to_string(),
8102 bound_literal: None,
8103 };
8104 resolver.register_type(&spec_arc, ftd.clone()).unwrap();
8105 let result = resolver.register_type(&spec_arc, ftd);
8106 assert!(result.is_err());
8107 }
8108
8109 #[test]
8110 fn test_resolve_custom_type_from_primitive() {
8111 let (dag, spec_arc) = dag_and_spec();
8112 let (ctx, _) = test_context_and_effective(&dag);
8113 let mut resolver = TypeResolver::new(ctx);
8114 let ftd = DataTypeDef {
8115 parent: ParentType::Primitive {
8116 primitive: PrimitiveKind::Number,
8117 },
8118 constraints: None,
8119 source: crate::parsing::source::Source::new(
8120 crate::parsing::source::SourceType::Volatile,
8121 crate::parsing::ast::Span {
8122 start: 0,
8123 end: 0,
8124 line: 1,
8125 col: 0,
8126 },
8127 ),
8128 name: "money".to_string(),
8129 bound_literal: None,
8130 };
8131
8132 resolver.register_type(&spec_arc, ftd).unwrap();
8133 let resolved = resolver
8134 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
8135 .unwrap();
8136
8137 assert!(resolved.resolved.contains_key("money"));
8138 let money_type = resolved.resolved.get("money").unwrap();
8139 assert_eq!(money_type.name, Some("money".to_string()));
8140 }
8141
8142 #[test]
8143 fn test_child_quantity_type_keeps_declared_name_and_child_units() {
8144 let (resolver, spec_arc) = resolver_single_spec(
8145 r#"spec test
8146data length: quantity
8147 -> unit meter 1
8148data road_length: length
8149 -> unit kilometer 1000"#,
8150 );
8151
8152 let resolved_types = resolver
8153 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
8154 .unwrap();
8155
8156 let road_length_type = resolved_types.resolved.get("road_length").unwrap();
8157 assert_eq!(road_length_type.name.as_deref(), Some("road_length"));
8158
8159 match &road_length_type.specifications {
8160 TypeSpecification::Quantity { units, .. } => {
8161 assert!(units.iter().any(|unit| unit.name == "kilometer"));
8162 }
8163 _ => panic!("Expected Quantity type specifications"),
8164 }
8165
8166 let kilometer_owner = resolved_types.unit_index.get("kilometer").unwrap();
8167 assert_eq!(kilometer_owner.name.as_deref(), Some("road_length"));
8168 }
8169
8170 #[test]
8171 fn test_type_definition_resolution() {
8172 let (resolver, spec_arc) = resolver_single_spec(
8173 r#"spec test
8174data dice: number -> minimum 0 -> maximum 6"#,
8175 );
8176
8177 let resolved_types = resolver
8178 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
8179 .unwrap();
8180 let dice_type = resolved_types.resolved.get("dice").unwrap();
8181
8182 match &dice_type.specifications {
8183 TypeSpecification::Number {
8184 minimum, maximum, ..
8185 } => {
8186 assert_eq!(*minimum, Some(RationalInteger::new(0, 1)));
8187 assert_eq!(*maximum, Some(RationalInteger::new(6, 1)));
8188 }
8189 _ => panic!("Expected Number type specifications"),
8190 }
8191 }
8192
8193 #[test]
8194 fn test_type_definition_with_multiple_commands() {
8195 let (resolver, spec_arc) = resolver_single_spec(
8196 r#"spec test
8197data money: quantity -> decimals 2 -> unit eur 1.0 -> unit usd 1.18"#,
8198 );
8199
8200 let resolved_types = resolver
8201 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
8202 .unwrap();
8203 let money_type = resolved_types.resolved.get("money").unwrap();
8204
8205 match &money_type.specifications {
8206 TypeSpecification::Quantity {
8207 decimals, units, ..
8208 } => {
8209 assert_eq!(*decimals, Some(2));
8210 assert_eq!(units.len(), 2);
8211 assert!(units.iter().any(|u| u.name == "eur"));
8212 assert!(units.iter().any(|u| u.name == "usd"));
8213 }
8214 _ => panic!("Expected Quantity type specifications"),
8215 }
8216 }
8217
8218 #[test]
8219 fn test_number_type_with_decimals() {
8220 let (resolver, spec_arc) = resolver_single_spec(
8221 r#"spec test
8222data price: number -> decimals 2 -> minimum 0"#,
8223 );
8224
8225 let resolved_types = resolver
8226 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
8227 .unwrap();
8228 let price_type = resolved_types.resolved.get("price").unwrap();
8229
8230 match &price_type.specifications {
8231 TypeSpecification::Number {
8232 decimals, minimum, ..
8233 } => {
8234 assert_eq!(*decimals, Some(2));
8235 assert_eq!(*minimum, Some(RationalInteger::new(0, 1)));
8236 }
8237 _ => panic!("Expected Number type specifications with decimals"),
8238 }
8239 }
8240
8241 #[test]
8242 fn test_number_type_decimals_only() {
8243 let (resolver, spec_arc) = resolver_single_spec(
8244 r#"spec test
8245data precise_number: number -> decimals 4"#,
8246 );
8247
8248 let resolved_types = resolver
8249 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
8250 .unwrap();
8251 let precise_type = resolved_types.resolved.get("precise_number").unwrap();
8252
8253 match &precise_type.specifications {
8254 TypeSpecification::Number { decimals, .. } => {
8255 assert_eq!(*decimals, Some(4));
8256 }
8257 _ => panic!("Expected Number type with decimals 4"),
8258 }
8259 }
8260
8261 #[test]
8262 fn test_quantity_type_decimals_only() {
8263 let (resolver, spec_arc) = resolver_single_spec(
8264 r#"spec test
8265data weight: quantity -> unit kg 1 -> decimals 3"#,
8266 );
8267
8268 let resolved_types = resolver
8269 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
8270 .unwrap();
8271 let weight_type = resolved_types.resolved.get("weight").unwrap();
8272
8273 match &weight_type.specifications {
8274 TypeSpecification::Quantity { decimals, .. } => {
8275 assert_eq!(*decimals, Some(3));
8276 }
8277 _ => panic!("Expected Quantity type with decimals 3"),
8278 }
8279 }
8280
8281 #[test]
8282 fn test_ratio_type_accepts_optional_decimals_command() {
8283 let (resolver, spec_arc) = resolver_single_spec(
8284 r#"spec test
8285data ratio_type: ratio -> decimals 2"#,
8286 );
8287
8288 let resolved_types = resolver
8289 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
8290 .unwrap();
8291 let ratio_type = resolved_types.resolved.get("ratio_type").unwrap();
8292
8293 match &ratio_type.specifications {
8294 TypeSpecification::Ratio { decimals, .. } => {
8295 assert_eq!(
8296 *decimals,
8297 Some(2),
8298 "ratio type should accept decimals command"
8299 );
8300 }
8301 _ => panic!("Expected Ratio type with decimals 2"),
8302 }
8303 }
8304
8305 #[test]
8306 fn test_ratio_type_with_default_command() {
8307 let (resolver, spec_arc) = resolver_single_spec(
8308 r#"spec test
8309data percentage: ratio -> minimum 0% -> maximum 100% -> default 50%"#,
8310 );
8311
8312 let resolved_types = resolver
8313 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
8314 .unwrap();
8315 let percentage_type = resolved_types.resolved.get("percentage").unwrap();
8316
8317 match &percentage_type.specifications {
8318 TypeSpecification::Ratio {
8319 minimum, maximum, ..
8320 } => {
8321 assert_eq!(
8322 *minimum,
8323 Some(RationalInteger::new(0, 1)),
8324 "ratio type should have minimum 0"
8325 );
8326 assert_eq!(
8327 *maximum,
8328 Some(RationalInteger::new(1, 1)),
8329 "ratio type should have maximum 1"
8330 );
8331 }
8332 _ => panic!("Expected Ratio type with minimum and maximum"),
8333 }
8334
8335 let declared = resolved_types
8336 .declared_defaults
8337 .get("percentage")
8338 .expect("declared default must be tracked for percentage");
8339 match declared {
8340 ValueKind::Ratio(v, unit) => {
8341 assert_eq!(*v, RationalInteger::new(1, 2));
8342 assert_eq!(unit.as_deref(), Some("percent"));
8343 }
8344 other => panic!("expected Ratio declared default, got {:?}", other),
8345 }
8346 }
8347
8348 #[test]
8349 fn test_quantity_extension_chain_same_family_units_allowed() {
8350 let (resolver, spec_arc) = resolver_single_spec(
8351 r#"spec test
8352data money: quantity -> unit eur 1
8353data money2: money -> unit usd 1.24"#,
8354 );
8355
8356 let result = resolver.resolve_types_internal(&spec_arc, &EffectiveDate::Origin);
8357 assert!(
8358 result.is_ok(),
8359 "Quantity extension chain should resolve: {:?}",
8360 result.err()
8361 );
8362
8363 let resolved = result.unwrap();
8364 assert!(
8365 resolved.unit_index.contains_key("eur"),
8366 "eur should be in unit_index"
8367 );
8368 assert!(
8369 resolved.unit_index.contains_key("usd"),
8370 "usd should be in unit_index"
8371 );
8372 let eur_type = resolved.unit_index.get("eur").unwrap();
8373 let usd_type = resolved.unit_index.get("usd").unwrap();
8374 assert_eq!(
8375 eur_type.name.as_deref(),
8376 Some("money2"),
8377 "more derived type (money2) should own inherited eur"
8378 );
8379 assert_eq!(
8380 usd_type.name.as_deref(),
8381 Some("money2"),
8382 "usd defined on money2 should be owned by money2"
8383 );
8384 }
8385
8386 #[test]
8387 fn test_invalid_parent_type_in_named_type_should_error() {
8388 let (resolver, spec_arc) = resolver_single_spec(
8389 r#"spec test
8390data invalid: nonexistent_type -> minimum 0"#,
8391 );
8392
8393 let result = resolver.resolve_types_internal(&spec_arc, &EffectiveDate::Origin);
8394 assert!(result.is_err(), "Should reject invalid parent type");
8395
8396 let errs = result.unwrap_err();
8397 assert!(!errs.is_empty(), "expected at least one error");
8398 let error_msg = errs[0].to_string();
8399 assert!(
8400 error_msg.contains("Unknown parent") && error_msg.contains("nonexistent_type"),
8401 "Error should mention unknown type. Got: {}",
8402 error_msg
8403 );
8404 }
8405
8406 #[test]
8407 fn test_invalid_primitive_type_name_should_error() {
8408 let (resolver, spec_arc) = resolver_single_spec(
8409 r#"spec test
8410data invalid: choice -> option "a""#,
8411 );
8412
8413 let result = resolver.resolve_types_internal(&spec_arc, &EffectiveDate::Origin);
8414 assert!(result.is_err(), "Should reject invalid type base 'choice'");
8415
8416 let errs = result.unwrap_err();
8417 assert!(!errs.is_empty(), "expected at least one error");
8418 let error_msg = errs[0].to_string();
8419 assert!(
8420 error_msg.contains("Unknown parent") && error_msg.contains("choice"),
8421 "Error should mention unknown type 'choice'. Got: {}",
8422 error_msg
8423 );
8424 }
8425
8426 #[test]
8427 fn test_quantity_extension_overwrites_parent_units() {
8428 let (resolver, spec_arc) = resolver_single_spec(
8429 r#"spec test
8430data money: quantity
8431 -> unit eur 1.00
8432 -> unit usd 0.84
8433
8434data money2: money
8435 -> unit eur 1.20
8436 -> unit usd 1.21
8437 -> unit gbp 1.30"#,
8438 );
8439
8440 let resolved = resolver
8441 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
8442 .unwrap();
8443 let money2 = resolved.resolved.get("money2").unwrap();
8444 match &money2.specifications {
8445 TypeSpecification::Quantity { units, .. } => {
8446 assert_eq!(units.len(), 3);
8447 let eur = units.iter().find(|u| u.name == "eur").unwrap();
8448 let usd = units.iter().find(|u| u.name == "usd").unwrap();
8449 let gbp = units.iter().find(|u| u.name == "gbp").unwrap();
8450 assert_eq!(
8451 crate::commit_rational_to_decimal(&eur.factor).unwrap(),
8452 Decimal::from_str_exact("1.20").unwrap()
8453 );
8454 assert_eq!(
8455 crate::commit_rational_to_decimal(&usd.factor).unwrap(),
8456 Decimal::from_str_exact("1.21").unwrap()
8457 );
8458 assert_eq!(
8459 crate::commit_rational_to_decimal(&gbp.factor).unwrap(),
8460 Decimal::from_str_exact("1.30").unwrap()
8461 );
8462 }
8463 other => panic!("Expected Quantity type specifications, got {:?}", other),
8464 }
8465 }
8466
8467 #[test]
8468 fn test_spec_level_unit_ambiguity_errors_are_reported() {
8469 let (resolver, spec_arc) = resolver_single_spec(
8470 r#"spec test
8471data money_a: quantity
8472 -> unit eur 1.00
8473 -> unit usd 0.84
8474
8475data money_b: quantity
8476 -> unit eur 1.00
8477 -> unit usd 1.20
8478
8479data length_a: quantity
8480 -> unit meter 1.0
8481
8482data length_b: quantity
8483 -> unit meter 1.0"#,
8484 );
8485
8486 let result = resolver.resolve_types_internal(&spec_arc, &EffectiveDate::Origin);
8487 assert!(
8488 result.is_err(),
8489 "Expected ambiguous unit definitions to error"
8490 );
8491
8492 let errs = result.unwrap_err();
8493 assert!(!errs.is_empty(), "expected at least one error");
8494 let error_msg = errs
8495 .iter()
8496 .map(ToString::to_string)
8497 .collect::<Vec<_>>()
8498 .join("; ");
8499 assert!(
8500 error_msg.contains("eur") || error_msg.contains("usd") || error_msg.contains("meter"),
8501 "Error should mention at least one ambiguous unit. Got: {}",
8502 error_msg
8503 );
8504 }
8505
8506 #[test]
8507 fn test_ratio_unit_cross_family_collision_errors() {
8508 let (resolver, spec_arc) = resolver_single_spec(
8509 r#"spec test
8510data q: quantity
8511 -> unit foo 1
8512
8513data r: ratio
8514 -> unit foo 100"#,
8515 );
8516
8517 let result = resolver.resolve_types_internal(&spec_arc, &EffectiveDate::Origin);
8518 assert!(
8519 result.is_err(),
8520 "quantity and ratio must not share a unit name"
8521 );
8522 let error_msg = result
8523 .unwrap_err()
8524 .iter()
8525 .map(ToString::to_string)
8526 .collect::<Vec<_>>()
8527 .join("; ");
8528 assert!(
8529 error_msg.contains("foo"),
8530 "expected cross-family collision on 'foo', got: {}",
8531 error_msg
8532 );
8533 }
8534
8535 #[test]
8536 fn test_same_ratio_unit_same_factor_across_types_allowed() {
8537 let (resolver, spec_arc) = resolver_single_spec(
8538 r#"spec test
8539data spread_a: ratio
8540 -> unit basis_points 10000
8541
8542data spread_b: ratio
8543 -> unit basis_points 10000"#,
8544 );
8545
8546 resolver
8547 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
8548 .expect("same unit name and factor across ratio types must be allowed");
8549 }
8550
8551 #[test]
8552 fn test_different_ratio_unit_factor_across_types_errors() {
8553 let (resolver, spec_arc) = resolver_single_spec(
8554 r#"spec test
8555data spread_a: ratio
8556 -> unit basis_points 10000
8557
8558data spread_b: ratio
8559 -> unit basis_points 5000"#,
8560 );
8561
8562 let result = resolver.resolve_types_internal(&spec_arc, &EffectiveDate::Origin);
8563 assert!(
8564 result.is_err(),
8565 "unrelated ratio types with different factors for the same unit must error"
8566 );
8567 let error_msg = result
8568 .unwrap_err()
8569 .iter()
8570 .map(ToString::to_string)
8571 .collect::<Vec<_>>()
8572 .join("; ");
8573 assert!(
8574 error_msg.contains("spread_a") && error_msg.contains("spread_b"),
8575 "expected ambiguous ratio unit between types, got: {}",
8576 error_msg
8577 );
8578 }
8579
8580 #[test]
8581 fn test_multiple_builtin_ratio_types_share_percent_in_unit_index() {
8582 let (resolver, spec_arc) = resolver_single_spec(
8583 r#"spec targets
8584data standard_margin_pct: ratio
8585 -> minimum 0%
8586 -> default 15%
8587
8588data default_credit_insurance_pct: ratio
8589 -> default 1.5%"#,
8590 );
8591
8592 resolver
8593 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
8594 .expect("multiple ratio types with built-in percent must not conflict");
8595 }
8596
8597 #[test]
8598 fn test_three_ratio_types_share_builtin_and_custom_unit() {
8599 let (resolver, spec_arc) = resolver_single_spec(
8600 r#"spec test
8601data margin: ratio -> default 10%
8602data fee: ratio
8603 -> unit tenths 10
8604data tax: ratio -> default 1%"#,
8605 );
8606
8607 resolver
8608 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
8609 .expect("builtin percent/permille plus custom tenths on second type must load");
8610 }
8611
8612 #[test]
8613 fn test_ratio_unit_index_allows_builtin_after_two_named_types() {
8614 let (resolver, spec_arc) = resolver_single_spec(
8615 r#"spec test
8616data first_ratio: ratio -> default 5%
8617data second_ratio: ratio -> default 10%"#,
8618 );
8619
8620 let resolved = resolver
8621 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
8622 .expect("two ratio types with only builtin units");
8623
8624 assert!(
8625 resolved.unit_index.contains_key("percent"),
8626 "percent must remain in unit index"
8627 );
8628 assert!(
8629 resolved.unit_index.contains_key("permille"),
8630 "permille must remain in unit index"
8631 );
8632 }
8633
8634 #[test]
8635 fn test_number_type_cannot_have_units() {
8636 let (resolver, spec_arc) = resolver_single_spec(
8637 r#"spec test
8638data price: number
8639 -> unit eur 1.00"#,
8640 );
8641
8642 let result = resolver.resolve_types_internal(&spec_arc, &EffectiveDate::Origin);
8643 assert!(result.is_err(), "Number types must reject unit commands");
8644
8645 let errs = result.unwrap_err();
8646 assert!(!errs.is_empty(), "expected at least one error");
8647 let error_msg = errs[0].to_string();
8648 assert!(
8649 error_msg.contains("unit") && error_msg.contains("number"),
8650 "Error should mention units are invalid on number. Got: {}",
8651 error_msg
8652 );
8653 }
8654
8655 #[test]
8656 fn test_extending_type_inherits_units() {
8657 let (resolver, spec_arc) = resolver_single_spec(
8658 r#"spec test
8659data money: quantity
8660 -> unit eur 1.00
8661 -> unit usd 0.84
8662
8663data my_money: money
8664 -> unit gbp 1.30"#,
8665 );
8666
8667 let resolved = resolver
8668 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
8669 .unwrap();
8670 let my_money_type = resolved.resolved.get("my_money").unwrap();
8671
8672 match &my_money_type.specifications {
8673 TypeSpecification::Quantity { units, .. } => {
8674 assert_eq!(units.len(), 3);
8675 assert!(units.iter().any(|u| u.name == "eur"));
8676 assert!(units.iter().any(|u| u.name == "usd"));
8677 assert!(units.iter().any(|u| u.name == "gbp"));
8678 }
8679 other => panic!("Expected Quantity type specifications, got {:?}", other),
8680 }
8681 }
8682
8683 #[test]
8684 fn test_value_copy_quantity_binding_overwrites_unit_factor() {
8685 let (resolver, spec_arc) = resolver_single_spec(
8686 r#"spec test
8687data source_quantity: quantity
8688 -> unit usd 1.00
8689
8690data z: source_quantity
8691 -> unit usd 0.84"#,
8692 );
8693
8694 let resolved = resolver
8695 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
8696 .unwrap();
8697 let z = resolved.resolved.get("z").unwrap();
8698 match &z.specifications {
8699 TypeSpecification::Quantity { units, .. } => {
8700 assert_eq!(units.len(), 1);
8701 let usd = units.iter().find(|u| u.name == "usd").unwrap();
8702 assert_eq!(
8703 crate::commit_rational_to_decimal(&usd.factor).unwrap(),
8704 Decimal::from_str_exact("0.84").unwrap()
8705 );
8706 }
8707 other => panic!("Expected Quantity type specifications, got {:?}", other),
8708 }
8709 }
8710
8711 #[test]
8712 fn test_duplicate_unit_in_same_type_last_wins() {
8713 let (resolver, spec_arc) = resolver_single_spec(
8714 r#"spec test
8715data money: quantity
8716 -> unit eur 1.00
8717 -> unit eur 1.19"#,
8718 );
8719
8720 let resolved = resolver
8721 .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
8722 .unwrap();
8723 let money = resolved.resolved.get("money").unwrap();
8724 match &money.specifications {
8725 TypeSpecification::Quantity { units, .. } => {
8726 assert_eq!(units.len(), 1);
8727 let eur = units.iter().find(|u| u.name == "eur").unwrap();
8728 assert_eq!(
8729 crate::commit_rational_to_decimal(&eur.factor).unwrap(),
8730 Decimal::from_str_exact("1.19").unwrap()
8731 );
8732 }
8733 other => panic!("Expected Quantity type specifications, got {:?}", other),
8734 }
8735 }
8736}
8737
8738pub fn validate_type_specifications(
8752 specs: &TypeSpecification,
8753 declared_default: Option<&ValueKind>,
8754 type_name: &str,
8755 source: &Source,
8756 spec_context: Option<Arc<LemmaSpec>>,
8757) -> Vec<Error> {
8758 let mut errors = Vec::new();
8759
8760 match specs {
8761 TypeSpecification::Quantity {
8762 minimum,
8763 maximum,
8764 decimals,
8765 units,
8766 ..
8767 } => {
8768 if let (Some(min), Some(max)) = (minimum, maximum) {
8770 match (
8771 semantics::quantity_declared_bound_canonical(min, units, type_name, "minimum"),
8772 semantics::quantity_declared_bound_canonical(max, units, type_name, "maximum"),
8773 ) {
8774 (Ok(min_canonical), Ok(max_canonical)) => {
8775 if min_canonical > max_canonical {
8776 errors.push(Error::validation_with_context(
8777 format!(
8778 "Type '{}' has invalid range: minimum {} {} is greater than maximum {} {}",
8779 type_name,
8780 min.0,
8781 min.1,
8782 max.0,
8783 max.1
8784 ),
8785 Some(source.clone()),
8786 None::<String>,
8787 spec_context.clone(),
8788 None,
8789 ));
8790 }
8791 }
8792 (Err(message), _) | (_, Err(message)) => {
8793 errors.push(Error::validation_with_context(
8794 format!(
8795 "Type '{}' has invalid quantity bound: {}",
8796 type_name, message
8797 ),
8798 Some(source.clone()),
8799 None::<String>,
8800 spec_context.clone(),
8801 None,
8802 ));
8803 }
8804 }
8805 }
8806
8807 if minimum.is_some() {
8808 for unit in units.iter() {
8809 if unit.minimum.is_none() {
8810 errors.push(Error::validation_with_context(
8811 format!(
8812 "Type '{}' has minimum bound but unit '{}' is missing per-unit minimum after planning",
8813 type_name, unit.name
8814 ),
8815 Some(source.clone()),
8816 None::<String>,
8817 spec_context.clone(),
8818 None,
8819 ));
8820 }
8821 }
8822 }
8823 if maximum.is_some() {
8824 for unit in units.iter() {
8825 if unit.maximum.is_none() {
8826 errors.push(Error::validation_with_context(
8827 format!(
8828 "Type '{}' has maximum bound but unit '{}' is missing per-unit maximum after planning",
8829 type_name, unit.name
8830 ),
8831 Some(source.clone()),
8832 None::<String>,
8833 spec_context.clone(),
8834 None,
8835 ));
8836 }
8837 }
8838 }
8839 if declared_default.is_some() {
8840 for unit in units.iter() {
8841 if unit.default_magnitude.is_none() {
8842 errors.push(Error::validation_with_context(
8843 format!(
8844 "Type '{}' has default but unit '{}' is missing per-unit default after planning",
8845 type_name, unit.name
8846 ),
8847 Some(source.clone()),
8848 None::<String>,
8849 spec_context.clone(),
8850 None,
8851 ));
8852 }
8853 }
8854 }
8855
8856 if let Some(d) = decimals {
8858 if *d > 28 {
8859 errors.push(Error::validation_with_context(
8860 format!(
8861 "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
8862 type_name, d
8863 ),
8864 Some(source.clone()),
8865 None::<String>,
8866 spec_context.clone(),
8867 None,
8868 ));
8869 }
8870 }
8871
8872 if let Some(ValueKind::Quantity(_def_value, def_unit, _def_decomp)) = declared_default {
8873 if !units.iter().any(|u| u.name == *def_unit) {
8874 errors.push(Error::validation_with_context(
8875 format!(
8876 "Type '{}' default unit '{}' is not a valid unit. Valid units: {}",
8877 type_name,
8878 def_unit,
8879 units
8880 .iter()
8881 .map(|u| u.name.clone())
8882 .collect::<Vec<_>>()
8883 .join(", ")
8884 ),
8885 Some(source.clone()),
8886 None::<String>,
8887 spec_context.clone(),
8888 None,
8889 ));
8890 }
8891 }
8892
8893 if units.is_empty() {
8895 errors.push(Error::validation_with_context(
8896 format!(
8897 "Type '{}' is a quantity type but has no units. Quantity types must define at least one unit (e.g. -> unit eur 1).",
8898 type_name
8899 ),
8900 Some(source.clone()),
8901 None::<String>,
8902 spec_context.clone(),
8903 None,
8904 ));
8905 }
8906
8907 if !units.is_empty() {
8909 let mut seen_names: Vec<String> = Vec::new();
8910 for unit in units.iter() {
8911 if unit.name.trim().is_empty() {
8913 errors.push(Error::validation_with_context(
8914 format!(
8915 "Type '{}' has a unit with empty name. Unit names cannot be empty.",
8916 type_name
8917 ),
8918 Some(source.clone()),
8919 None::<String>,
8920 spec_context.clone(),
8921 None,
8922 ));
8923 }
8924
8925 if seen_names.contains(&unit.name) {
8927 errors.push(Error::validation_with_context(
8928 format!("Type '{}' has duplicate unit name '{}'. Unit names must be unique within a type.", type_name, unit.name),
8929 Some(source.clone()),
8930 None::<String>,
8931 spec_context.clone(),
8932 None,
8933 ));
8934 } else {
8935 seen_names.push(unit.name.clone());
8936 }
8937
8938 if !unit.is_positive_factor() {
8939 let factor = unit.factor.reduced();
8940 errors.push(Error::validation_with_context(
8941 format!("Type '{}' has unit '{}' with invalid value {}/{}. Unit values must be positive (conversion factor relative to type base).", type_name, unit.name, factor.numer(), factor.denom()),
8942 Some(source.clone()),
8943 None::<String>,
8944 spec_context.clone(),
8945 None,
8946 ));
8947 }
8948 }
8949 }
8950 }
8951 TypeSpecification::Number {
8952 minimum,
8953 maximum,
8954 decimals,
8955 ..
8956 } => {
8957 if let (Some(min), Some(max)) = (minimum, maximum) {
8959 if min > max {
8960 errors.push(Error::validation_with_context(
8961 format!(
8962 "Type '{}' has invalid range: minimum {} is greater than maximum {}",
8963 type_name, min, max
8964 ),
8965 Some(source.clone()),
8966 None::<String>,
8967 spec_context.clone(),
8968 None,
8969 ));
8970 }
8971 }
8972
8973 if let Some(d) = decimals {
8975 if *d > 28 {
8976 errors.push(Error::validation_with_context(
8977 format!(
8978 "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
8979 type_name, d
8980 ),
8981 Some(source.clone()),
8982 None::<String>,
8983 spec_context.clone(),
8984 None,
8985 ));
8986 }
8987 }
8988
8989 if let Some(ValueKind::Number(def)) = declared_default {
8990 if let Some(min) = minimum {
8991 if *def < *min {
8992 errors.push(Error::validation_with_context(
8993 format!(
8994 "Type '{}' default value {} is less than minimum {}",
8995 type_name, def, min
8996 ),
8997 Some(source.clone()),
8998 None::<String>,
8999 spec_context.clone(),
9000 None,
9001 ));
9002 }
9003 }
9004 if let Some(max) = maximum {
9005 if *def > *max {
9006 errors.push(Error::validation_with_context(
9007 format!(
9008 "Type '{}' default value {} is greater than maximum {}",
9009 type_name, def, max
9010 ),
9011 Some(source.clone()),
9012 None::<String>,
9013 spec_context.clone(),
9014 None,
9015 ));
9016 }
9017 }
9018 }
9019 }
9021
9022 TypeSpecification::Ratio {
9023 minimum,
9024 maximum,
9025 decimals,
9026 units,
9027 ..
9028 } => {
9029 if let Some(d) = decimals {
9031 if *d > 28 {
9032 errors.push(Error::validation_with_context(
9033 format!(
9034 "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
9035 type_name, d
9036 ),
9037 Some(source.clone()),
9038 None::<String>,
9039 spec_context.clone(),
9040 None,
9041 ));
9042 }
9043 }
9044
9045 if let (Some(min), Some(max)) = (minimum, maximum) {
9047 if min > max {
9048 errors.push(Error::validation_with_context(
9049 format!(
9050 "Type '{}' has invalid range: minimum {} is greater than maximum {}",
9051 type_name, min, max
9052 ),
9053 Some(source.clone()),
9054 None::<String>,
9055 spec_context.clone(),
9056 None,
9057 ));
9058 }
9059 }
9060
9061 if let Some(ValueKind::Ratio(def, _)) = declared_default {
9062 if let Some(min) = minimum {
9063 if *def < *min {
9064 errors.push(Error::validation_with_context(
9065 format!(
9066 "Type '{}' default value {} is less than minimum {}",
9067 type_name, def, min
9068 ),
9069 Some(source.clone()),
9070 None::<String>,
9071 spec_context.clone(),
9072 None,
9073 ));
9074 }
9075 }
9076 if let Some(max) = maximum {
9077 if *def > *max {
9078 errors.push(Error::validation_with_context(
9079 format!(
9080 "Type '{}' default value {} is greater than maximum {}",
9081 type_name, def, max
9082 ),
9083 Some(source.clone()),
9084 None::<String>,
9085 spec_context.clone(),
9086 None,
9087 ));
9088 }
9089 }
9090 }
9091
9092 if !units.is_empty() {
9096 let mut seen_names: Vec<String> = Vec::new();
9097 for unit in units.iter() {
9098 if unit.name.trim().is_empty() {
9100 errors.push(Error::validation_with_context(
9101 format!(
9102 "Type '{}' has a unit with empty name. Unit names cannot be empty.",
9103 type_name
9104 ),
9105 Some(source.clone()),
9106 None::<String>,
9107 spec_context.clone(),
9108 None,
9109 ));
9110 }
9111
9112 if seen_names.contains(&unit.name) {
9114 errors.push(Error::validation_with_context(
9115 format!("Type '{}' has duplicate unit name '{}'. Unit names must be unique within a type.", type_name, unit.name),
9116 Some(source.clone()),
9117 None::<String>,
9118 spec_context.clone(),
9119 None,
9120 ));
9121 } else {
9122 seen_names.push(unit.name.clone());
9123 }
9124
9125 if *unit.value.numer() <= 0 {
9126 let factor = unit.value.reduced();
9127 errors.push(Error::validation_with_context(
9128 format!("Type '{}' has unit '{}' with invalid value {}/{}. Unit values must be positive (conversion factor relative to type base).", type_name, unit.name, factor.numer(), factor.denom()),
9129 Some(source.clone()),
9130 None::<String>,
9131 spec_context.clone(),
9132 None,
9133 ));
9134 }
9135 }
9136 }
9137 }
9138
9139 TypeSpecification::Text {
9140 length, options, ..
9141 } => {
9142 if let Some(ValueKind::Text(def)) = declared_default {
9143 let def_len = def.len();
9144
9145 if let Some(len) = length {
9146 if def_len != *len {
9147 errors.push(Error::validation_with_context(
9148 format!("Type '{}' default value length {} does not match required length {}", type_name, def_len, len),
9149 Some(source.clone()),
9150 None::<String>,
9151 spec_context.clone(),
9152 None,
9153 ));
9154 }
9155 }
9156 if !options.is_empty() && !options.contains(def) {
9157 errors.push(Error::validation_with_context(
9158 format!(
9159 "Type '{}' default value '{}' is not in allowed options: {:?}",
9160 type_name, def, options
9161 ),
9162 Some(source.clone()),
9163 None::<String>,
9164 spec_context.clone(),
9165 None,
9166 ));
9167 }
9168 }
9169 }
9170
9171 TypeSpecification::Date {
9172 minimum,
9173 maximum,
9174 ..
9175 } => {
9176 if let (Some(min), Some(max)) = (minimum, maximum) {
9178 let min_sem = semantics::date_time_to_semantic(min);
9179 let max_sem = semantics::date_time_to_semantic(max);
9180 if semantics::compare_semantic_dates(&min_sem, &max_sem) == Ordering::Greater {
9181 errors.push(Error::validation_with_context(
9182 format!(
9183 "Type '{}' has invalid date range: minimum {} is after maximum {}",
9184 type_name, min, max
9185 ),
9186 Some(source.clone()),
9187 None::<String>,
9188 spec_context.clone(),
9189 None,
9190 ));
9191 }
9192 }
9193
9194 if let Some(ValueKind::Date(def)) = declared_default {
9195 if let Some(min) = minimum {
9196 let min_sem = semantics::date_time_to_semantic(min);
9197 if semantics::compare_semantic_dates(def, &min_sem) == Ordering::Less {
9198 errors.push(Error::validation_with_context(
9199 format!(
9200 "Type '{}' default date {} is before minimum {}",
9201 type_name, def, min
9202 ),
9203 Some(source.clone()),
9204 None::<String>,
9205 spec_context.clone(),
9206 None,
9207 ));
9208 }
9209 }
9210 if let Some(max) = maximum {
9211 let max_sem = semantics::date_time_to_semantic(max);
9212 if semantics::compare_semantic_dates(def, &max_sem) == Ordering::Greater {
9213 errors.push(Error::validation_with_context(
9214 format!(
9215 "Type '{}' default date {} is after maximum {}",
9216 type_name, def, max
9217 ),
9218 Some(source.clone()),
9219 None::<String>,
9220 spec_context.clone(),
9221 None,
9222 ));
9223 }
9224 }
9225 }
9226 }
9227
9228 TypeSpecification::Time {
9229 minimum,
9230 maximum,
9231 ..
9232 } => {
9233 if let (Some(min), Some(max)) = (minimum, maximum) {
9235 let min_sem = semantics::time_to_semantic(min);
9236 let max_sem = semantics::time_to_semantic(max);
9237 if semantics::compare_semantic_times(&min_sem, &max_sem) == Ordering::Greater {
9238 errors.push(Error::validation_with_context(
9239 format!(
9240 "Type '{}' has invalid time range: minimum {} is after maximum {}",
9241 type_name, min, max
9242 ),
9243 Some(source.clone()),
9244 None::<String>,
9245 spec_context.clone(),
9246 None,
9247 ));
9248 }
9249 }
9250
9251 if let Some(ValueKind::Time(def)) = declared_default {
9252 if let Some(min) = minimum {
9253 let min_sem = semantics::time_to_semantic(min);
9254 if semantics::compare_semantic_times(def, &min_sem) == Ordering::Less {
9255 errors.push(Error::validation_with_context(
9256 format!(
9257 "Type '{}' default time {} is before minimum {}",
9258 type_name, def, min
9259 ),
9260 Some(source.clone()),
9261 None::<String>,
9262 spec_context.clone(),
9263 None,
9264 ));
9265 }
9266 }
9267 if let Some(max) = maximum {
9268 let max_sem = semantics::time_to_semantic(max);
9269 if semantics::compare_semantic_times(def, &max_sem) == Ordering::Greater {
9270 errors.push(Error::validation_with_context(
9271 format!(
9272 "Type '{}' default time {} is after maximum {}",
9273 type_name, def, max
9274 ),
9275 Some(source.clone()),
9276 None::<String>,
9277 spec_context.clone(),
9278 None,
9279 ));
9280 }
9281 }
9282 }
9283 }
9284
9285 TypeSpecification::NumberRange { .. }
9286 | TypeSpecification::DateRange { .. }
9287 | TypeSpecification::QuantityRange { .. }
9288 | TypeSpecification::RatioRange { .. }
9289 | TypeSpecification::CalendarRange { .. }
9290 | TypeSpecification::Boolean { .. }
9291 | TypeSpecification::Calendar { .. } => {
9292 }
9294 TypeSpecification::Veto { .. } => {
9295 }
9298 TypeSpecification::Undetermined => unreachable!(
9299 "BUG: validate_type_specification_constraints called with Undetermined sentinel type; this type exists only during type inference"
9300 ),
9301 }
9302
9303 errors
9304}
9305
9306#[cfg(test)]
9307mod validation_tests {
9308 use super::*;
9309 use crate::computation::rational::RationalInteger;
9310 use crate::parsing::ast::{CommandArg, TypeConstraintCommand};
9311 use crate::planning::semantics::TypeSpecification;
9312 use rust_decimal::Decimal;
9313
9314 fn test_source() -> Source {
9315 Source::new(
9316 crate::parsing::source::SourceType::Volatile,
9317 crate::parsing::ast::Span {
9318 start: 0,
9319 end: 0,
9320 line: 1,
9321 col: 0,
9322 },
9323 )
9324 }
9325
9326 fn apply(
9327 specs: TypeSpecification,
9328 command: TypeConstraintCommand,
9329 args: &[CommandArg],
9330 ) -> TypeSpecification {
9331 let mut default = None;
9332 specs
9333 .apply_constraint("test", command, args, &mut default)
9334 .unwrap()
9335 }
9336
9337 fn number_arg(n: i64) -> CommandArg {
9338 CommandArg::Literal(crate::literals::Value::Number(Decimal::from(n)))
9339 }
9340
9341 fn date_arg(s: &str) -> CommandArg {
9342 let dt = s.parse::<crate::literals::DateTimeValue>().expect("date");
9343 CommandArg::Literal(crate::literals::Value::Date(dt))
9344 }
9345
9346 fn time_arg(s: &str) -> CommandArg {
9347 let t = s.parse::<crate::literals::TimeValue>().expect("time");
9348 CommandArg::Literal(crate::literals::Value::Time(t))
9349 }
9350
9351 #[test]
9352 fn validate_number_minimum_greater_than_maximum() {
9353 let mut specs = TypeSpecification::number();
9354 specs = apply(specs, TypeConstraintCommand::Minimum, &[number_arg(100)]);
9355 specs = apply(specs, TypeConstraintCommand::Maximum, &[number_arg(50)]);
9356
9357 let src = test_source();
9358 let errors = validate_type_specifications(&specs, None, "test", &src, None);
9359 assert_eq!(errors.len(), 1);
9360 assert!(errors[0]
9361 .to_string()
9362 .contains("minimum 100 is greater than maximum 50"));
9363 }
9364
9365 #[test]
9366 fn validate_number_default_below_minimum() {
9367 let specs = TypeSpecification::Number {
9368 minimum: Some(RationalInteger::new(10, 1)),
9369 maximum: None,
9370 decimals: None,
9371 help: String::new(),
9372 };
9373 let default = ValueKind::Number(RationalInteger::new(5, 1));
9374
9375 let src = test_source();
9376 let errors = validate_type_specifications(&specs, Some(&default), "test", &src, None);
9377 assert_eq!(errors.len(), 1);
9378 assert!(errors[0]
9379 .to_string()
9380 .contains("default value 5 is less than minimum 10"));
9381 }
9382
9383 #[test]
9384 fn validate_number_default_above_maximum() {
9385 let specs = TypeSpecification::Number {
9386 minimum: None,
9387 maximum: Some(RationalInteger::new(100, 1)),
9388 decimals: None,
9389 help: String::new(),
9390 };
9391 let default = ValueKind::Number(RationalInteger::new(150, 1));
9392
9393 let src = test_source();
9394 let errors = validate_type_specifications(&specs, Some(&default), "test", &src, None);
9395 assert_eq!(errors.len(), 1);
9396 assert!(errors[0]
9397 .to_string()
9398 .contains("default value 150 is greater than maximum 100"));
9399 }
9400
9401 #[test]
9402 fn validate_number_default_valid() {
9403 let specs = TypeSpecification::Number {
9404 minimum: Some(RationalInteger::new(0, 1)),
9405 maximum: Some(RationalInteger::new(100, 1)),
9406 decimals: None,
9407 help: String::new(),
9408 };
9409 let default = ValueKind::Number(RationalInteger::new(50, 1));
9410
9411 let src = test_source();
9412 let errors = validate_type_specifications(&specs, Some(&default), "test", &src, None);
9413 assert!(errors.is_empty());
9414 }
9415
9416 #[test]
9417 fn text_minimum_command_is_rejected() {
9418 let specs = TypeSpecification::text();
9419 let res = specs.apply_constraint(
9420 "test",
9421 TypeConstraintCommand::Minimum,
9422 &[number_arg(5)],
9423 &mut None,
9424 );
9425 assert!(res.is_err());
9426 assert!(res
9427 .unwrap_err()
9428 .contains("Invalid command 'minimum' for text type"));
9429 }
9430
9431 #[test]
9432 fn text_maximum_command_is_rejected() {
9433 let specs = TypeSpecification::text();
9434 let res = specs.apply_constraint(
9435 "test",
9436 TypeConstraintCommand::Maximum,
9437 &[number_arg(5)],
9438 &mut None,
9439 );
9440 assert!(res.is_err());
9441 assert!(res
9442 .unwrap_err()
9443 .contains("Invalid command 'maximum' for text type"));
9444 }
9445
9446 #[test]
9447 fn validate_text_default_not_in_options() {
9448 let specs = TypeSpecification::Text {
9449 length: None,
9450 options: vec!["red".to_string(), "blue".to_string()],
9451 help: String::new(),
9452 };
9453 let default = ValueKind::Text("green".to_string());
9454
9455 let src = test_source();
9456 let errors = validate_type_specifications(&specs, Some(&default), "test", &src, None);
9457 assert_eq!(errors.len(), 1);
9458 assert!(errors[0]
9459 .to_string()
9460 .contains("default value 'green' is not in allowed options"));
9461 }
9462
9463 #[test]
9464 fn validate_ratio_minimum_greater_than_maximum() {
9465 let specs = TypeSpecification::Ratio {
9466 minimum: Some(RationalInteger::new(2, 1)),
9467 maximum: Some(RationalInteger::new(1, 1)),
9468 decimals: None,
9469 units: crate::planning::semantics::RatioUnits::new(),
9470 help: String::new(),
9471 };
9472
9473 let src = test_source();
9474 let errors = validate_type_specifications(&specs, None, "test", &src, None);
9475 assert_eq!(errors.len(), 1);
9476 assert!(errors[0]
9477 .to_string()
9478 .contains("minimum 2 is greater than maximum 1"));
9479 }
9480
9481 #[test]
9482 fn validate_date_minimum_after_maximum() {
9483 let mut specs = TypeSpecification::date();
9484 specs = apply(
9485 specs,
9486 TypeConstraintCommand::Minimum,
9487 &[date_arg("2024-12-31")],
9488 );
9489 specs = apply(
9490 specs,
9491 TypeConstraintCommand::Maximum,
9492 &[date_arg("2024-01-01")],
9493 );
9494
9495 let src = test_source();
9496 let errors = validate_type_specifications(&specs, None, "test", &src, None);
9497 assert_eq!(errors.len(), 1);
9498 assert!(
9499 errors[0].to_string().contains("minimum")
9500 && errors[0].to_string().contains("is after maximum")
9501 );
9502 }
9503
9504 #[test]
9505 fn validate_date_valid_range() {
9506 let mut specs = TypeSpecification::date();
9507 specs = apply(
9508 specs,
9509 TypeConstraintCommand::Minimum,
9510 &[date_arg("2024-01-01")],
9511 );
9512 specs = apply(
9513 specs,
9514 TypeConstraintCommand::Maximum,
9515 &[date_arg("2024-12-31")],
9516 );
9517
9518 let src = test_source();
9519 let errors = validate_type_specifications(&specs, None, "test", &src, None);
9520 assert!(errors.is_empty());
9521 }
9522
9523 #[test]
9524 fn validate_time_minimum_after_maximum() {
9525 let mut specs = TypeSpecification::time();
9526 specs = apply(
9527 specs,
9528 TypeConstraintCommand::Minimum,
9529 &[time_arg("23:00:00")],
9530 );
9531 specs = apply(
9532 specs,
9533 TypeConstraintCommand::Maximum,
9534 &[time_arg("10:00:00")],
9535 );
9536
9537 let src = test_source();
9538 let errors = validate_type_specifications(&specs, None, "test", &src, None);
9539 assert_eq!(errors.len(), 1);
9540 assert!(
9541 errors[0].to_string().contains("minimum")
9542 && errors[0].to_string().contains("is after maximum")
9543 );
9544 }
9545}