1use crate::computation::arithmetic::{SignatureFactor, SignatureIndex};
8use crate::computation::rational::{NumericFailure, RationalInteger};
9use crate::engine::Context;
10use crate::limits::MAX_UNIT_EXPONENT;
11use crate::literals::{MeasureUnit, MeasureUnits};
12use crate::parsing::ast::{
13 self as ast, CommandArg, Constraint, EffectiveDate, LemmaRepository, LemmaSpec, ParentType,
14 PrimitiveKind, TypeConstraintCommand, Value,
15};
16use crate::parsing::source::Source;
17use crate::planning::discovery;
18use crate::planning::execution_plan;
19use crate::planning::instance_list;
20use crate::planning::semantics::{
21 self, apply_list_with_rows, apply_option_constraint, apply_spec_member_command,
22 bound_value_kind_from_raw_suggestion, calendar_decomposition, calendar_unit_factor,
23 canonicalize_signature, duration_decomposition, value_kind_matches_spec, BaseMeasureVector,
24 BoundValueKind, ConstraintNames, DataPath, LemmaType, ListKey, LiteralValue, PathSegment,
25 RawSuggestion, ReferenceTarget, RulePath, SpecIdentity, SpecVersionKey, TypeDefiningSpec,
26 TypeExtends, TypeFamily, TypeOptions, TypeSpecification, TypedLiteral, UnitTokenResolver,
27 ValueKind,
28};
29use crate::planning::unit_index::{
30 PlanningOwner, PlanningUnitIndex, UnitIndex, UnitMergeConflict, UnitOwner,
31};
32use crate::Error;
33use ast::DataValue as ParsedDataValue;
34use indexmap::IndexMap;
35use rust_decimal::Decimal;
36use std::cmp::Ordering;
37use std::collections::{BTreeSet, HashMap, HashSet, VecDeque};
38use std::fmt;
39use std::sync::Arc;
40
41pub(crate) type ResolvedTypesMap<'a> =
42 Vec<(Arc<LemmaRepository>, &'a LemmaSpec, ResolvedSpecTypes)>;
43
44type ParentLookupOk = (
46 TypeSpecification,
47 Option<RawSuggestion>,
48 Option<RawSuggestion>,
49 Arc<LemmaType>,
50);
51
52type ResolvedTypeEntry = (Arc<LemmaType>, Option<RawSuggestion>, Option<RawSuggestion>);
54
55fn local_type_entries(resolved_types: &ResolvedSpecTypes) -> HashMap<String, ResolvedTypeEntry> {
56 resolved_types
57 .resolved
58 .iter()
59 .map(|(name, lemma_type)| {
60 (
61 name.clone(),
62 (
63 Arc::clone(lemma_type),
64 resolved_types.source_defaults.get(name).cloned(),
65 resolved_types.source_value_defaults.get(name).cloned(),
66 ),
67 )
68 })
69 .collect()
70}
71
72pub(crate) fn constraint_application_type_name(parent: &ParentType, data_name: &str) -> String {
74 let mut current = parent;
75 loop {
76 match current {
77 ParentType::Custom { name, .. } => return name.clone(),
78 ParentType::Qualified { inner, .. }
79 | ParentType::Ranged { inner, .. }
80 | ParentType::List { inner, .. } => current = inner.as_ref(),
81 ParentType::Primitive { .. } => return data_name.to_string(),
82 }
83 }
84}
85
86pub(crate) fn preserve_measure_binding(
89 schema_type: &Arc<LemmaType>,
90 previous: &LemmaType,
91) -> Arc<LemmaType> {
92 match previous.unit() {
93 Some(unit)
94 if matches!(
95 &schema_type.specifications,
96 TypeSpecification::Measure { .. } | TypeSpecification::Ratio { .. }
97 ) =>
98 {
99 Arc::new(schema_type.as_ref().clone().with_unit(unit))
100 }
101 Some(_) | None => Arc::clone(schema_type),
102 }
103}
104
105pub(crate) fn coerce_literal_to_schema_type(
106 lit: &TypedLiteral,
107 schema_type: &Arc<LemmaType>,
108) -> Result<TypedLiteral, String> {
109 fn range_endpoint_schema_type(schema_type: &LemmaType) -> Option<Arc<LemmaType>> {
110 schema_type
111 .specifications
112 .element_from_range()
113 .map(|element_spec| Arc::new(LemmaType::primitive(element_spec)))
114 }
115
116 let schema_ref = schema_type.as_ref();
117 match (&schema_ref.specifications, &lit.value) {
118 (
119 TypeSpecification::NumberRange { .. }
120 | TypeSpecification::DateRange { .. }
121 | TypeSpecification::TimeRange { .. }
122 | TypeSpecification::RatioRange { .. }
123 | TypeSpecification::MeasureRange { .. },
124 ValueKind::Range(left, right),
125 ) if !lit
126 .lemma_type
127 .specifications
128 .equal_ignoring_unit(&schema_ref.specifications) =>
129 {
130 let endpoint_schema_type =
131 range_endpoint_schema_type(schema_ref).unwrap_or_else(|| {
132 unreachable!("BUG: range_endpoint_schema_type missing range schema arm")
133 });
134 let left_schema =
135 preserve_measure_binding(&endpoint_schema_type, left.lemma_type.as_ref());
136 let right_schema =
137 preserve_measure_binding(&endpoint_schema_type, right.lemma_type.as_ref());
138 let coerced_left = coerce_scalar_literal(left, &left_schema)?;
139 let coerced_right = coerce_scalar_literal(right, &right_schema)?;
140 Ok(TypedLiteral {
141 value: ValueKind::Range(Box::new(coerced_left), Box::new(coerced_right)),
142 lemma_type: Arc::clone(schema_type),
143 })
144 }
145 _ => coerce_scalar_literal(lit, schema_type),
146 }
147}
148
149pub(crate) fn coerce_scalar_literal(
152 lit: &TypedLiteral,
153 schema_type: &Arc<LemmaType>,
154) -> Result<TypedLiteral, String> {
155 let schema_ref = schema_type.as_ref();
156 if lit
157 .lemma_type
158 .specifications
159 .equal_ignoring_unit(&schema_ref.specifications)
160 {
161 if !value_kind_matches_spec(&lit.value, &schema_ref.specifications) {
162 panic!(
163 "BUG: LiteralValue value kind {:?} inconsistent with lemma_type {:?}",
164 lit.value, lit.lemma_type.specifications
165 );
166 }
167 let mut out = lit.clone();
168 out.lemma_type = preserve_measure_binding(schema_type, &lit.lemma_type);
169 return Ok(out);
170 }
171 match (&schema_ref.specifications, &lit.value) {
172 (TypeSpecification::Number { .. }, ValueKind::Number(_))
173 | (TypeSpecification::Text { .. }, ValueKind::Text(_))
174 | (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
175 | (TypeSpecification::Date { .. }, ValueKind::Date(_))
176 | (TypeSpecification::Time { .. }, ValueKind::Time(_)) => {
177 let mut out = lit.clone();
178 out.lemma_type = Arc::clone(schema_type);
179 Ok(out)
180 }
181 (TypeSpecification::Measure { .. }, ValueKind::Measure(_)) => {
182 if !lit.lemma_type.same_measure_family(schema_ref)
183 && !lit.lemma_type.compatible_with_anonymous_measure(schema_ref)
184 && !lit
185 .lemma_type
186 .specifications
187 .equal_ignoring_unit(&schema_ref.specifications)
188 {
189 return Err(format!(
190 "value {} cannot be used as type {}: incompatible measure families",
191 lit,
192 schema_ref.name()
193 ));
194 }
195 let mut out = lit.clone();
196 out.lemma_type = preserve_measure_binding(schema_type, &lit.lemma_type);
197 Ok(out)
198 }
199 (TypeSpecification::Ratio { .. }, ValueKind::Ratio(_)) => {
200 let mut out = lit.clone();
201 out.lemma_type = preserve_measure_binding(schema_type, &lit.lemma_type);
202 Ok(out)
203 }
204 _ => Err(format!(
205 "value {} cannot be used as type {}",
206 lit,
207 schema_ref.name()
208 )),
209 }
210}
211
212fn should_defer_ranged_constraints(parent: &ParentType) -> bool {
216 matches!(parent, ParentType::Ranged { .. })
217 && matches!(
218 element_parent_type(parent),
219 ParentType::Custom { .. } | ParentType::Qualified { .. }
220 )
221}
222
223pub(crate) struct ConstraintSuggestionSink<'a> {
228 pub(crate) suggestion: &'a mut Option<RawSuggestion>,
229 pub(crate) value: &'a mut Option<RawSuggestion>,
230 pub(crate) resolve_unit: Option<&'a UnitTokenResolver<'a>>,
231}
232
233pub(crate) fn apply_constraints_to_spec(
234 spec: &LemmaSpec,
235 type_name: &str,
236 data_name: &str,
237 lemma_type: &mut LemmaType,
238 constraints: &[Constraint],
239 source: &Source,
240 sink: ConstraintSuggestionSink<'_>,
241) -> Result<(), Vec<Error>> {
242 let mut errors = Vec::new();
243
244 let mut seen_unit_names: std::collections::HashSet<String> = Default::default();
245 let mut seen_singleton_commands: std::collections::HashSet<TypeConstraintCommand> =
246 Default::default();
247 let mut has_suggest = false;
248 let mut has_value = false;
249 for row in constraints.iter() {
250 let command = row.command;
251 let args = &row.args;
252 if command == TypeConstraintCommand::Suggest {
253 has_suggest = true;
254 }
255 if command == TypeConstraintCommand::With {
256 has_value = true;
257 }
258 if command == TypeConstraintCommand::Unit {
259 if let Some(CommandArg::Label(name)) = args.first() {
260 if !seen_unit_names.insert(name.clone()) {
261 errors.push(Error::validation_with_context(
262 format!(
263 "Duplicate unit '{}': each unit name may appear at most once per type definition",
264 name
265 ),
266 Some(source.clone()),
267 None::<String>,
268 Some(spec),
269 None,
270 ));
271 }
272 }
273 }
274 if matches!(
275 command,
276 TypeConstraintCommand::Minimum
277 | TypeConstraintCommand::Maximum
278 | TypeConstraintCommand::Decimals
279 | TypeConstraintCommand::Suggest
280 | TypeConstraintCommand::With
281 ) && !(command == TypeConstraintCommand::With
282 && matches!(lemma_type.specifications, TypeSpecification::List { .. }))
283 && !seen_singleton_commands.insert(command)
284 {
285 errors.push(Error::validation_with_context(
286 format!(
287 "Duplicate '{command}' constraint: each may appear at most once per type declaration"
288 ),
289 Some(source.clone()),
290 None::<String>,
291 Some(spec),
292 None,
293 ));
294 }
295 }
296 if has_suggest && has_value {
297 errors.push(Error::validation_with_context(
298 "Cannot use both 'with' and 'suggest' on the same data declaration".to_string(),
299 Some(source.clone()),
300 None::<String>,
301 Some(spec),
302 None,
303 ));
304 }
305 if !errors.is_empty() {
306 return Err(errors);
307 }
308
309 let ConstraintSuggestionSink {
310 suggestion: declared_suggestion,
311 value: declared_value,
312 resolve_unit,
313 } = sink;
314 let names = ConstraintNames {
315 type_name,
316 data_name,
317 };
318
319 let mut apply_one = |lemma_type: &mut LemmaType,
320 command: TypeConstraintCommand,
321 args: &[CommandArg],
322 declared_suggestion: &mut Option<RawSuggestion>,
323 declared_value: &mut Option<RawSuggestion>| {
324 let mut suggestion_before = declared_suggestion.clone();
325 let mut value_before = declared_value.clone();
326 let applied = if matches!(
327 command,
328 TypeConstraintCommand::Option | TypeConstraintCommand::Options
329 ) {
330 apply_option_constraint(lemma_type, command, args, resolve_unit, spec)
331 } else if matches!(
332 command,
333 TypeConstraintCommand::Suggest | TypeConstraintCommand::With
334 ) && matches!(lemma_type.specifications, TypeSpecification::Spec { .. })
335 {
336 let target = match command {
337 TypeConstraintCommand::Suggest => &mut suggestion_before,
338 TypeConstraintCommand::With => &mut value_before,
339 _ => unreachable!("BUG: only Suggest or With in this arm"),
340 };
341 apply_spec_member_command(lemma_type, command, args, spec, target)
342 } else if matches!(lemma_type.specifications, TypeSpecification::List { .. })
343 && command == TypeConstraintCommand::With
344 {
345 apply_list_with_rows(
346 lemma_type,
347 names,
348 args,
349 &mut value_before,
350 resolve_unit,
351 spec,
352 )
353 } else {
354 lemma_type.apply_constraint(
355 names,
356 command,
357 args,
358 &mut suggestion_before,
359 &mut value_before,
360 resolve_unit,
361 )
362 };
363 match applied {
364 Ok(()) => {
365 *declared_suggestion = suggestion_before;
366 *declared_value = value_before;
367 }
368 Err(e) => {
369 errors.push(Error::validation_with_context(
370 format!("Failed to apply constraint '{}': {}", command, e),
371 Some(source.clone()),
372 None::<String>,
373 Some(spec),
374 None,
375 ));
376 }
377 }
378 };
379
380 for row in constraints {
381 let command = row.command;
382 let args = &row.args;
383 if matches!(
384 command,
385 TypeConstraintCommand::Unit | TypeConstraintCommand::Trait
386 ) {
387 apply_one(
388 lemma_type,
389 command,
390 args,
391 declared_suggestion,
392 declared_value,
393 );
394 }
395 }
396 for row in constraints {
397 let command = row.command;
398 let args = &row.args;
399 if !matches!(
400 command,
401 TypeConstraintCommand::Unit | TypeConstraintCommand::Trait
402 ) {
403 apply_one(
404 lemma_type,
405 command,
406 args,
407 declared_suggestion,
408 declared_value,
409 );
410 }
411 }
412 if !errors.is_empty() {
413 return Err(errors);
414 }
415 Ok(())
416}
417
418type UnitDecompLookup = HashMap<String, (Option<String>, BaseMeasureVector, RationalInteger)>;
419
420fn local_owner_name(owner: &PlanningOwner) -> Option<String> {
421 match owner {
422 PlanningOwner::Local { type_name } => Some(type_name.clone()),
423 PlanningOwner::PrimitiveRatio | PlanningOwner::Imported { .. } => None,
424 }
425}
426
427fn unwrap_unique_resolved_type(arc: Arc<LemmaType>) -> LemmaType {
428 Arc::try_unwrap(arc).expect("BUG: resolved LemmaType Arc unique during type resolution")
429}
430
431fn declared_measure_decomposition(lemma_type: &LemmaType) -> BaseMeasureVector {
432 match &lemma_type.specifications {
433 TypeSpecification::Measure { traits, .. }
434 if traits.contains(&semantics::MeasureTrait::Duration) =>
435 {
436 duration_decomposition()
437 }
438 TypeSpecification::Measure { traits, .. }
439 if traits.contains(&semantics::MeasureTrait::Calendar) =>
440 {
441 calendar_decomposition()
442 }
443 _ => {
444 let family = lemma_type
445 .measure_family()
446 .expect("BUG: named measure type must carry a family");
447 [(semantics::Dimension::Family(family.clone()), 1i32)]
448 .into_iter()
449 .collect()
450 }
451 }
452}
453
454fn local_owning_measure_type_for_unit(
456 unit_name: &str,
457 lookup: &UnitDecompLookup,
458 unit_index: &PlanningUnitIndex,
459 resolved: &IndexMap<String, Arc<LemmaType>>,
460) -> Option<String> {
461 if let Some((owning_measure_name, _, _)) = lookup.get(unit_name) {
462 return owning_measure_name.clone();
463 }
464 unit_index
465 .resolve_owner(unit_name, resolved)
466 .ok()
467 .and_then(|(_, owner)| local_owner_name(owner))
468}
469
470fn sort_derived_measure_types_for_resolution(
472 spec_name: &str,
473 derived_measure_type_names: Vec<String>,
474 resolved: &IndexMap<String, Arc<LemmaType>>,
475 lookup: &UnitDecompLookup,
476 unit_index: &PlanningUnitIndex,
477 type_sources: &HashMap<String, Source>,
478) -> Result<Vec<String>, Error> {
479 let derived_measure_type_count = derived_measure_type_names.len();
480 if derived_measure_type_count == 0 {
481 return Ok(derived_measure_type_names);
482 }
483
484 let type_index: HashMap<&str, usize> = derived_measure_type_names
485 .iter()
486 .enumerate()
487 .map(|(index, name)| (name.as_str(), index))
488 .collect();
489
490 let mut dependency_sets: Vec<BTreeSet<usize>> =
491 vec![BTreeSet::new(); derived_measure_type_count];
492
493 for (dependent_index, type_name) in derived_measure_type_names.iter().enumerate() {
494 let TypeSpecification::Measure { units, .. } = &resolved[type_name].specifications else {
495 continue;
496 };
497 for unit in units.iter() {
498 for (factor_unit_name, _) in &unit.derived_measure_factors {
499 let Some(owning_measure_name) = local_owning_measure_type_for_unit(
500 factor_unit_name,
501 lookup,
502 unit_index,
503 resolved,
504 ) else {
505 continue;
506 };
507 let Some(dependency_index) = type_index.get(owning_measure_name.as_str()).copied()
508 else {
509 continue;
510 };
511 if dependency_index == dependent_index {
512 continue;
513 }
514 dependency_sets[dependent_index].insert(dependency_index);
515 }
516 }
517 }
518
519 let mut in_degree = vec![0usize; derived_measure_type_count];
520 let mut dependents: Vec<Vec<usize>> = vec![Vec::new(); derived_measure_type_count];
521
522 for (dependent_index, dependencies) in dependency_sets.iter().enumerate() {
523 for &dependency_index in dependencies {
524 in_degree[dependent_index] += 1;
525 dependents[dependency_index].push(dependent_index);
526 }
527 }
528
529 let mut queue: VecDeque<usize> = (0..derived_measure_type_count)
530 .filter(|&index| in_degree[index] == 0)
531 .collect();
532 let mut sorted_indices: Vec<usize> = Vec::with_capacity(derived_measure_type_count);
533
534 while let Some(index) = queue.pop_front() {
535 sorted_indices.push(index);
536 for &dependent_index in &dependents[index] {
537 in_degree[dependent_index] -= 1;
538 if in_degree[dependent_index] == 0 {
539 queue.push_back(dependent_index);
540 }
541 }
542 }
543
544 if sorted_indices.len() != derived_measure_type_count {
545 let mut cycle_type_names: Vec<String> = (0..derived_measure_type_count)
546 .filter(|&index| in_degree[index] > 0)
547 .map(|index| derived_measure_type_names[index].clone())
548 .collect();
549 cycle_type_names.sort();
550 return Err(Error::validation(
551 format!(
552 "In spec '{}': circular compound measure type dependency among: {}",
553 spec_name,
554 cycle_type_names.join(", ")
555 ),
556 type_sources.get(&cycle_type_names[0]).cloned(),
557 None::<String>,
558 ));
559 }
560
561 Ok(sorted_indices
562 .into_iter()
563 .map(|index| derived_measure_type_names[index].clone())
564 .collect())
565}
566
567pub(crate) struct SignatureIndexBuild {
573 pub index: SignatureIndex,
574 pub deferred_cross_type: Vec<(String, BaseMeasureVector)>,
575}
576
577pub(crate) fn build_signature_index(
578 spec_name: &str,
579 unit_index: &UnitIndex,
580 already_resolved: &ResolvedTypesMap<'_>,
581) -> Result<SignatureIndexBuild, Error> {
582 use crate::computation::arithmetic::SignatureIndex;
583 let mut signature_index: SignatureIndex = SignatureIndex::new();
584 let mut deferred_cross_type = Vec::new();
585 let mut sorted_units: Vec<_> = unit_index.iter_owners().collect();
586 sorted_units.sort_by_key(|(name, _)| *name);
587 for (unit_name, owner) in sorted_units {
588 let unit_name = unit_name.to_string();
589 let lemma_type = owner.lemma_type();
590 let TypeSpecification::Measure { units, .. } = &lemma_type.specifications else {
591 continue;
592 };
593 let unit = units.get(unit_name.as_str()).map_err(|_| {
594 Error::validation(
595 format!(
596 "In spec '{}': unit_index entry '{}' is not declared on measure type '{}'",
597 spec_name,
598 unit_name,
599 lemma_type.name()
600 ),
601 None::<Source>,
602 None::<String>,
603 )
604 })?;
605 if lemma_type.measure_type_decomposition().is_none() {
606 continue;
607 }
608 if measure_unit_is_simple(unit) && unit_index.owners_for(&unit_name).len() != 1 {
612 continue;
613 }
614 let owning_type_name = lemma_type.name();
615 let signature = if owner.import_path().is_empty() {
616 resolved_signature_key(
617 &unit.derived_measure_factors,
618 &unit_name,
619 lemma_type,
620 unit_index,
621 )
622 } else if let Some(key) = imported_signature_key(already_resolved, &unit_name, lemma_type) {
623 key
624 } else if measure_unit_is_simple(unit) {
625 resolved_signature_key(
628 &unit.derived_measure_factors,
629 &unit_name,
630 lemma_type,
631 unit_index,
632 )
633 } else {
634 continue;
637 };
638 match signature_index.get(&signature) {
639 Some((existing_unit_name, existing_owning_type))
640 if existing_owning_type.name() != lemma_type.name() =>
641 {
642 let existing_type_name = existing_owning_type.name();
643 let decomposition = lemma_type
644 .measure_type_decomposition()
645 .expect("BUG: indexed measure unit has no decomposition")
646 .clone();
647 deferred_cross_type.push((
648 format!(
649 "In spec '{spec_name}': ambiguous unit signature {signature:?}: matched by '{existing_unit_name}' in type '{existing_type_name}' and '{unit_name}' in type '{owning_type_name}'. \
650 Rename one or differentiate factors."
651 ),
652 decomposition,
653 ));
654 }
655 _ => {
656 signature_index.insert(signature, (unit_name.clone(), Arc::clone(lemma_type)));
657 }
658 }
659 }
660 Ok(SignatureIndexBuild {
661 index: signature_index,
662 deferred_cross_type,
663 })
664}
665
666fn resolved_signature_key(
667 factors: &[(String, i32)],
668 unit_name: &str,
669 lemma_type: &LemmaType,
670 unit_index: &UnitIndex,
671) -> Vec<SignatureFactor> {
672 use crate::computation::arithmetic::SignatureFactor;
673 let mut key = Vec::with_capacity(factors.len());
674 for (factor_name, exponent) in factors {
675 let (name, family) = if factor_name == unit_name {
676 (
677 factor_name.clone(),
678 lemma_type
679 .measure_family()
680 .expect("BUG: indexed measure unit has no family")
681 .clone(),
682 )
683 } else {
684 let (bare, owner) = unit_index.resolve_owner(factor_name).unwrap_or_else(|err| {
685 panic!(
686 "BUG: compound factor '{factor_name}' on unit '{unit_name}' must resolve: {err}"
687 )
688 });
689 (
690 bare,
691 owner
692 .lemma_type()
693 .measure_family()
694 .expect("BUG: resolved factor owner is not a named measure")
695 .clone(),
696 )
697 };
698 key.push(SignatureFactor {
699 name,
700 family,
701 exponent: *exponent,
702 });
703 }
704 key.sort_by(|left, right| {
705 (&left.family, left.name.as_str(), left.exponent).cmp(&(
706 &right.family,
707 right.name.as_str(),
708 right.exponent,
709 ))
710 });
711 key
712}
713
714fn imported_signature_key(
717 already_resolved: &ResolvedTypesMap<'_>,
718 unit_name: &str,
719 lemma_type: &Arc<LemmaType>,
720) -> Option<Vec<SignatureFactor>> {
721 for (_, _, types) in already_resolved {
722 for (key, (name, owner)) in &types.signature_index {
723 if name == unit_name && Arc::ptr_eq(owner, lemma_type) {
724 return Some(key.clone());
725 }
726 }
727 }
728 None
729}
730
731pub(crate) fn deferred_signature_errors(
732 main_spec: &LemmaSpec,
733 resolved_types: &ResolvedTypesMap,
734 inferred: &HashMap<RulePath, Arc<LemmaType>>,
735) -> Vec<Error> {
736 let Some(types) = resolved_types
737 .iter()
738 .find_map(|(_, spec, types)| discovery::same_loaded_spec(spec, main_spec).then_some(types))
739 else {
740 return Vec::new();
741 };
742 let mut errors = Vec::new();
743 for (message, decomposition) in &types.deferred_signature_collisions {
744 let produced = inferred.values().any(|lemma_type| {
745 lemma_type.is_anonymous_measure()
746 && lemma_type.measure_type_decomposition() == Some(decomposition)
747 });
748 if produced {
749 continue;
750 }
751 errors.push(Error::validation(
752 message.clone(),
753 None::<Source>,
754 None::<String>,
755 ));
756 }
757 errors
758}
759
760fn refresh_named_range_specs(
763 resolver: &TypeResolver<'_>,
764 spec: &LemmaSpec,
765 data_defs: &HashMap<String, DataTypeDef>,
766 resolved_types: &mut ResolvedSpecTypes,
767 already_resolved: &ResolvedTypesMap<'_>,
768 at: &EffectiveDate,
769) -> Vec<Error> {
770 let local = local_type_entries(resolved_types);
771 let resolved = &mut resolved_types.resolved;
772 let declared_suggestions = &mut resolved_types.declared_suggestions;
773 let mut errors = Vec::new();
774 for (type_name, def) in data_defs {
775 let ParentType::Ranged { .. } = &def.parent else {
776 continue;
777 };
778 let element = element_parent_type(&def.parent);
779 if matches!(element, ParentType::Primitive { .. }) {
780 continue;
781 }
782 let scope = ParentLookup {
783 resolver,
784 spec,
785 source: &def.source,
786 at,
787 already_resolved,
788 resolved: &local,
789 site: TypeUseSite::DataDefinition,
790 };
791 let endpoint_type = match lookup_parent_type(&scope, element) {
792 Ok((.., endpoint_type)) => endpoint_type,
793 Err(lookup_errors) => {
794 errors.extend(lookup_errors);
795 continue;
796 }
797 };
798 let range_spec = endpoint_type
799 .specifications
800 .range_from_element()
801 .expect("BUG: ranged element was checked rangeable when the type was resolved");
802 let lemma_type = resolved
803 .get_mut(type_name.as_str())
804 .expect("BUG: ranged type resolved before its range spec is refreshed");
805 let mut updated = lemma_type.as_ref().clone();
806 updated.specifications = range_spec;
807 *lemma_type = Arc::new(updated);
808
809 if let Some(BoundValueKind {
810 value: ValueKind::Range(left, right),
811 ..
812 }) = declared_suggestions.get_mut(type_name.as_str())
813 {
814 let left_typed = left.as_ref().clone();
815 let right_typed = right.as_ref().clone();
816 let coerced_left = match coerce_literal_to_schema_type(&left_typed, &endpoint_type) {
817 Ok(coerced) => coerced,
818 Err(message) => {
819 errors.push(Error::validation_with_context(
820 format!(
821 "In spec '{}': named range default left endpoint for '{}': {message}",
822 spec.name, type_name
823 ),
824 Some(def.source.clone()),
825 None::<String>,
826 Some(spec),
827 None,
828 ));
829 continue;
830 }
831 };
832 let coerced_right = match coerce_literal_to_schema_type(&right_typed, &endpoint_type) {
833 Ok(coerced) => coerced,
834 Err(message) => {
835 errors.push(Error::validation_with_context(
836 format!(
837 "In spec '{}': named range default right endpoint for '{}': {message}",
838 spec.name, type_name
839 ),
840 Some(def.source.clone()),
841 None::<String>,
842 Some(spec),
843 None,
844 ));
845 continue;
846 }
847 };
848 *declared_suggestions
849 .get_mut(type_name.as_str())
850 .expect("BUG: named range default removed while refreshing endpoints") =
851 BoundValueKind {
852 value: ValueKind::Range(Box::new(coerced_left), Box::new(coerced_right)),
853 unit: None,
854 instance_factors: None,
855 };
856 }
857 }
858 errors
859}
860
861fn apply_deferred_named_range_constraints(
863 spec: &LemmaSpec,
864 data_defs: &HashMap<String, DataTypeDef>,
865 resolved: &mut TypeMap,
866 declared_suggestions: &mut IndexMap<String, BoundValueKind>,
867 declared_values: &mut IndexMap<String, BoundValueKind>,
868 type_sources: &HashMap<String, Source>,
869 unit_index: &PlanningUnitIndex,
870) -> Vec<Error> {
871 let mut errors = Vec::new();
872 for (type_name, def) in data_defs {
873 if !should_defer_ranged_constraints(&def.parent) {
874 continue;
875 }
876 let Some(constraints) = &def.constraints else {
877 continue;
878 };
879 let Some(lemma_type) = resolved.get(type_name.as_str()).cloned() else {
880 continue;
881 };
882 let mut declared_suggestion: Option<RawSuggestion> = None;
883 let mut declared_value: Option<RawSuggestion> = None;
884 let resolve = UnitTokenResolver::Planning {
885 unit_index,
886 named_types: resolved,
887 };
888 let mut updated = lemma_type.as_ref().clone();
889 match apply_constraints_to_spec(
890 spec,
891 &constraint_application_type_name(&def.parent, type_name),
892 type_name,
893 &mut updated,
894 constraints,
895 &def.source,
896 ConstraintSuggestionSink {
897 suggestion: &mut declared_suggestion,
898 value: &mut declared_value,
899 resolve_unit: Some(&resolve),
900 },
901 ) {
902 Ok(()) => {
903 let updated_arc = Arc::new(updated);
904 if let Some(raw) = declared_suggestion {
905 match bound_value_kind_from_raw_suggestion(
906 raw,
907 &updated_arc.specifications,
908 type_name.as_str(),
909 ) {
910 Ok(value_kind) => {
911 declared_suggestions.insert(type_name.clone(), value_kind);
912 }
913 Err(message) => {
914 let source = type_sources
915 .get(type_name.as_str())
916 .cloned()
917 .unwrap_or_else(|| def.source.clone());
918 errors.push(Error::validation_with_context(
919 message,
920 Some(source),
921 None::<String>,
922 Some(spec),
923 None,
924 ));
925 }
926 }
927 }
928 if let Some(raw) = declared_value {
929 match bound_value_kind_from_raw_suggestion(
930 raw,
931 &updated_arc.specifications,
932 type_name.as_str(),
933 ) {
934 Ok(value_kind) => {
935 declared_values.insert(type_name.clone(), value_kind);
936 }
937 Err(message) => {
938 let source = type_sources
939 .get(type_name.as_str())
940 .cloned()
941 .unwrap_or_else(|| def.source.clone());
942 errors.push(Error::validation_with_context(
943 message,
944 Some(source),
945 None::<String>,
946 Some(spec),
947 None,
948 ));
949 }
950 }
951 }
952 resolved.insert(type_name.clone(), updated_arc);
953 }
954 Err(constraint_errors) => errors.extend(constraint_errors),
955 }
956 }
957 errors
958}
959
960#[derive(Clone, Copy)]
962enum TypeUseSite {
963 DataDefinition,
964 RuleListType,
965}
966
967impl fmt::Display for TypeUseSite {
968 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
969 match self {
970 TypeUseSite::DataDefinition => write!(f, "data definition"),
971 TypeUseSite::RuleListType => write!(f, "list type in a rule"),
972 }
973 }
974}
975
976#[derive(Clone, Copy)]
979struct ParentLookup<'s, 'a> {
980 resolver: &'s TypeResolver<'a>,
981 spec: &'s LemmaSpec,
982 source: &'s Source,
983 at: &'s EffectiveDate,
984 already_resolved: &'s ResolvedTypesMap<'a>,
985 resolved: &'s HashMap<String, ResolvedTypeEntry>,
986 site: TypeUseSite,
987}
988
989fn lookup_parent_type(
990 scope: &ParentLookup<'_, '_>,
991 parent: &ParentType,
992) -> Result<ParentLookupOk, Vec<Error>> {
993 let mut wrappers = Vec::new();
994 let mut leaf = parent;
995 while let ParentType::List { inner } | ParentType::Ranged { inner } = leaf {
996 wrappers.push(leaf);
997 leaf = inner.as_ref();
998 }
999 let mut lookup = lookup_leaf_parent_type(scope, leaf)?;
1000 while let Some(wrapper) = wrappers.pop() {
1001 lookup = lookup_wrapper_parent_type(scope, wrapper, lookup)?;
1002 }
1003 Ok(lookup)
1004}
1005
1006fn lookup_wrapper_parent_type(
1008 scope: &ParentLookup<'_, '_>,
1009 wrapper: &ParentType,
1010 inner_lookup: ParentLookupOk,
1011) -> Result<ParentLookupOk, Vec<Error>> {
1012 let ParentLookup { spec, source, .. } = *scope;
1013 let (element_specs, .., element_type) = inner_lookup;
1014 match wrapper {
1015 ParentType::List { inner } => {
1016 let element = if matches!(inner.as_ref(), ParentType::Ranged { .. }) {
1017 Arc::new(LemmaType::primitive(element_specs))
1018 } else {
1019 element_type
1020 };
1021 let lemma_type = Arc::new(LemmaType::list_of(element));
1022 Ok((lemma_type.specifications.clone(), None, None, lemma_type))
1023 }
1024 ParentType::Ranged { inner } => {
1025 let range_spec = element_specs.range_from_element().ok_or_else(|| {
1026 vec![Error::validation_with_context(
1027 format!(
1028 "'{inner}' is not rangeable: only measure, number, ratio, date, and time types support ranges"
1029 ),
1030 Some(source.clone()),
1031 None::<String>,
1032 Some(spec),
1033 None,
1034 )]
1035 })?;
1036 Ok((range_spec, None, None, element_type))
1037 }
1038 ParentType::Primitive { .. } | ParentType::Custom { .. } | ParentType::Qualified { .. } => {
1039 unreachable!("BUG: lookup_wrapper_parent_type called on a leaf parent")
1040 }
1041 }
1042}
1043
1044fn lookup_leaf_parent_type(
1046 scope: &ParentLookup<'_, '_>,
1047 parent: &ParentType,
1048) -> Result<ParentLookupOk, Vec<Error>> {
1049 let ParentLookup {
1050 resolver,
1051 spec,
1052 source,
1053 at,
1054 already_resolved,
1055 resolved,
1056 site,
1057 } = *scope;
1058 match parent {
1059 ParentType::List { .. } | ParentType::Ranged { .. } => {
1060 unreachable!("BUG: list and range wrappers are unwound before the leaf lookup")
1061 }
1062 ParentType::Primitive { primitive: kind } => {
1063 let lemma_type = Arc::clone(semantics::lemma_type_for_primitive(*kind));
1064 Ok((
1065 lemma_type.as_ref().specifications.clone(),
1066 None,
1067 None,
1068 lemma_type,
1069 ))
1070 }
1071 ParentType::Custom { name, .. } => {
1072 if let Some((parent_type, parent_suggestion, parent_value)) =
1073 resolved.get(name.as_str())
1074 {
1075 return Ok((
1076 parent_type.as_ref().specifications.clone(),
1077 parent_suggestion.clone(),
1078 parent_value.clone(),
1079 Arc::clone(parent_type),
1080 ));
1081 }
1082 let type_exists = resolver
1083 .data_types
1084 .iter()
1085 .find(|(s, _)| discovery::same_loaded_spec(s, spec))
1086 .map(|(_, type_map)| type_map.contains_key(name.as_str()))
1087 .unwrap_or(false);
1088 if !type_exists {
1089 if spec.data.iter().any(|data| {
1090 data.reference.is_local()
1091 && data.reference.name == name.as_str()
1092 && data.value.uses_spec_ref().is_some()
1093 }) {
1094 return Err(vec![Error::validation_with_context(
1095 format!(
1096 "'{name}' names a spec import alias, not a type. Use `data x: {name}.TypeName` after `uses`"
1097 ),
1098 Some(source.clone()),
1099 None::<String>,
1100 Some(spec),
1101 None,
1102 )]);
1103 }
1104 if spec.rules.iter().any(|rule| rule.name == name.as_str()) {
1105 return Err(vec![Error::validation_with_context(
1106 format!(
1107 "Unknown parent '{parent}' for {site}: '{name}' is a local rule, not a type. Use a type name, or a reference expression if you meant the rule's value."
1108 ),
1109 Some(source.clone()),
1110 None::<String>,
1111 Some(spec),
1112 None,
1113 )]);
1114 }
1115 let mut imported_aliases: Vec<&str> = Vec::new();
1116 for data_row in &spec.data {
1117 let Some(spec_ref) = data_row.value.uses_spec_ref() else {
1118 continue;
1119 };
1120 let import_alias = &data_row.reference.name;
1121 let (_, imported_spec) =
1122 match resolver.resolve_spec_for_import(spec, spec_ref, source, at) {
1123 Ok(pair) => pair,
1124 Err(_) => continue,
1125 };
1126 let Some(imported_resolved) = already_resolved
1127 .iter()
1128 .find(|(_, s, _)| discovery::same_loaded_spec(s, imported_spec))
1129 .map(|(_, _, r)| r)
1130 else {
1131 continue;
1132 };
1133 if imported_resolved.resolved.contains_key(name.as_str()) {
1134 imported_aliases.push(import_alias.as_str());
1135 }
1136 }
1137 if !imported_aliases.is_empty() {
1138 let forms = imported_aliases
1139 .iter()
1140 .map(|alias| format!("{alias}.{name}"))
1141 .collect::<Vec<_>>()
1142 .join(", ");
1143 return Err(vec![Error::validation_with_context(
1144 format!(
1145 "Unknown parent '{parent}' for {site}: '{name}' is declared by import{}; write {forms}",
1146 if imported_aliases.len() == 1 {
1147 format!(" '{}'", imported_aliases[0])
1148 } else {
1149 format!("s {}", imported_aliases.join(", "))
1150 }
1151 ),
1152 Some(source.clone()),
1153 None::<String>,
1154 Some(spec),
1155 None,
1156 )]);
1157 }
1158 if resolver
1159 .context
1160 .spec_set(resolver.registered_repository(spec), name)
1161 .is_some()
1162 {
1163 return Err(vec![Error::validation_with_context(
1164 format!(
1165 "Unknown parent '{parent}' for {site}: '{name}' is a spec, not a name in spec '{}'. Only the local names of `uses` lines exist inside a spec",
1166 spec.name
1167 ),
1168 Some(source.clone()),
1169 Some(format!(
1170 "Add `uses {name}`, or write the local name of the `uses` line that imports '{name}'."
1171 )),
1172 Some(spec),
1173 None,
1174 )]);
1175 }
1176 return Err(vec![Error::validation_with_context(
1177 format!(
1178 "Unknown parent '{parent}' for {site}. Parent must be defined before use. Valid primitive types are: boolean, measure, number, ratio, text, date, time"
1179 ),
1180 Some(source.clone()),
1181 None::<String>,
1182 Some(spec),
1183 None,
1184 )]);
1185 }
1186 Err(vec![Error::validation_with_context(
1187 format!(
1188 "Circular dependency detected in type resolution: {}::{}",
1189 spec.name, name
1190 ),
1191 Some(source.clone()),
1192 None::<String>,
1193 Some(spec),
1194 None,
1195 )])
1196 }
1197 ParentType::Qualified { spec_alias, inner } => {
1198 let import_row = spec
1199 .data
1200 .iter()
1201 .find(|row| row.reference.segments.is_empty() && row.reference.name == *spec_alias);
1202 let spec_ref = import_row.and_then(|row| {
1203 row.value
1204 .uses_spec_ref()
1205 .cloned()
1206 .or_else(|| discovery::data_instance_list_spec_ref(spec, row))
1207 });
1208 let Some(spec_ref) = spec_ref else {
1209 if spec.rules.iter().any(|rule| rule.name == *spec_alias) {
1210 return Err(vec![Error::validation_with_context(
1211 format!(
1212 "'{spec_alias}' is a local rule, not a type path. Row fields in type position name a `uses` import or a `data` instance list, not a rule."
1213 ),
1214 Some(source.clone()),
1215 None::<String>,
1216 Some(spec),
1217 None,
1218 )]);
1219 }
1220 return Err(vec![Error::validation_with_context(
1221 format!("'{spec_alias}' is not a `uses` import alias in this spec"),
1222 Some(source.clone()),
1223 Some(format!(
1224 "Add `uses {spec_alias}: <target_spec>`, then write `{spec_alias}.<type>`."
1225 )),
1226 Some(spec),
1227 None,
1228 )]);
1229 };
1230 let dynamic_effective = matches!(
1231 import_row.map(|row| &row.value),
1232 Some(ParsedDataValue::Import {
1233 effective_ref: Some(_),
1234 ..
1235 })
1236 );
1237 match inner.as_ref() {
1238 ParentType::Primitive { primitive: kind } => {
1239 let lemma_type = Arc::clone(semantics::lemma_type_for_primitive(*kind));
1240 Ok((
1241 lemma_type.as_ref().specifications.clone(),
1242 None,
1243 None,
1244 lemma_type,
1245 ))
1246 }
1247 ParentType::Custom { name, .. } if dynamic_effective => {
1248 union_dynamic_import_parent_type(
1249 resolver,
1250 spec,
1251 source,
1252 already_resolved,
1253 &spec_ref,
1254 spec_alias,
1255 name.as_str(),
1256 )
1257 }
1258 ParentType::Custom { name, .. } => {
1259 let (import_repository, target_spec) =
1260 match resolver.resolve_spec_for_import(spec, &spec_ref, source, at) {
1261 Ok(import_pair) => import_pair,
1262 Err(error) => return Err(vec![error]),
1263 };
1264 let Some(resolved_spec_types) = already_resolved
1265 .iter()
1266 .find(|(_, imported, _)| discovery::same_loaded_spec(imported, target_spec))
1267 .map(|(_, _, resolved_spec_types)| resolved_spec_types)
1268 else {
1269 return Err(vec![Error::validation_with_context(
1270 format!(
1271 "Cannot resolve type '{name}' from spec '{}' (via import '{spec_alias}'): spec '{}' failed type resolution",
1272 target_spec.name, target_spec.name
1273 ),
1274 Some(source.clone()),
1275 None::<String>,
1276 Some(spec),
1277 None,
1278 )]);
1279 };
1280 let Some(parent_type) = resolved_spec_types.resolved.get(name.as_str()) else {
1281 let type_exists = resolver
1282 .data_types
1283 .iter()
1284 .find(|(s, _)| discovery::same_loaded_spec(s, target_spec))
1285 .map(|(_, type_map)| type_map.contains_key(name.as_str()))
1286 .unwrap_or(false);
1287 if !type_exists {
1288 return Err(vec![Error::validation_with_context(
1289 format!(
1290 "Type '{name}' is not defined in spec '{}' (via import '{spec_alias}')",
1291 target_spec.name
1292 ),
1293 Some(source.clone()),
1294 None::<String>,
1295 Some(spec),
1296 None,
1297 )]);
1298 }
1299 return Err(vec![Error::validation_with_context(
1300 format!(
1301 "Circular dependency detected in type resolution: {}::{}",
1302 target_spec.name, name
1303 ),
1304 Some(source.clone()),
1305 None::<String>,
1306 Some(spec),
1307 None,
1308 )]);
1309 };
1310 let import_effective = spec_ref.at(at);
1311 let identity =
1312 SpecIdentity::of(&import_repository, target_spec, import_effective);
1313 let hop = PathSegment::import(spec_alias, &identity);
1314 let parent_type = Arc::new(parent_type.with_scale_import_hop(&hop));
1315 Ok((
1316 parent_type.as_ref().specifications.clone(),
1317 resolved_spec_types
1318 .source_defaults
1319 .get(name.as_str())
1320 .cloned(),
1321 resolved_spec_types
1322 .source_value_defaults
1323 .get(name.as_str())
1324 .cloned(),
1325 parent_type,
1326 ))
1327 }
1328 ParentType::Qualified { .. } => Err(vec![Error::validation_with_context(
1329 "Nested qualified parent types are invalid",
1330 Some(source.clone()),
1331 None::<String>,
1332 Some(spec),
1333 None,
1334 )]),
1335 ParentType::Ranged { .. } => Err(vec![Error::validation_with_context(
1336 "Nested ranged parent types are invalid",
1337 Some(source.clone()),
1338 None::<String>,
1339 Some(spec),
1340 None,
1341 )]),
1342 ParentType::List { .. } => Err(vec![Error::validation_with_context(
1343 "Nested list parent types are invalid",
1344 Some(source.clone()),
1345 None::<String>,
1346 Some(spec),
1347 None,
1348 )]),
1349 }
1350 }
1351 }
1352}
1353
1354fn union_dynamic_import_parent_type(
1358 resolver: &TypeResolver<'_>,
1359 spec: &LemmaSpec,
1360 source: &Source,
1361 already_resolved: &ResolvedTypesMap<'_>,
1362 spec_ref: &ast::SpecRef,
1363 spec_alias: &str,
1364 type_name: &str,
1365) -> Result<ParentLookupOk, Vec<Error>> {
1366 let resolved_repo = match discovery::resolve_spec_ref_repository(
1367 resolver.context,
1368 spec_ref,
1369 Some(source),
1370 spec.name.as_str(),
1371 Some(spec),
1372 ) {
1373 Ok(resolved) => resolved,
1374 Err(error) => return Err(vec![error]),
1375 };
1376 let repository = match &resolved_repo.repository {
1377 Some(explicit) => Arc::clone(explicit),
1378 None => Arc::clone(resolver.registered_repository(spec)),
1379 };
1380 let dep_name = crate::parsing::ast::ascii_lowercase_logical_name(spec_ref.name.clone());
1381 let mut candidates: Vec<&Arc<LemmaType>> = Vec::new();
1382 for (repo, imported, resolved_types) in already_resolved {
1383 if !Arc::ptr_eq(repo, &repository) {
1384 continue;
1385 }
1386 if crate::parsing::ast::ascii_lowercase_logical_name(imported.name.clone()) != dep_name {
1387 continue;
1388 }
1389 let Some(parent_type) = resolved_types.resolved.get(type_name) else {
1390 let type_exists = resolver
1391 .data_types
1392 .iter()
1393 .find(|(s, _)| discovery::same_loaded_spec(s, imported))
1394 .map(|(_, type_map)| type_map.contains_key(type_name))
1395 .unwrap_or(false);
1396 if !type_exists {
1397 return Err(vec![Error::validation_with_context(
1398 format!(
1399 "Type '{type_name}' is not defined in spec '{}' (via import '{spec_alias}')",
1400 imported.name
1401 ),
1402 Some(source.clone()),
1403 None::<String>,
1404 Some(spec),
1405 None,
1406 )]);
1407 }
1408 return Err(vec![Error::validation_with_context(
1409 format!(
1410 "Circular dependency detected in type resolution: {}::{type_name}",
1411 imported.name
1412 ),
1413 Some(source.clone()),
1414 None::<String>,
1415 Some(spec),
1416 None,
1417 )]);
1418 };
1419 candidates.push(parent_type);
1420 }
1421 if candidates.is_empty() {
1422 return Err(vec![Error::validation_with_context(
1423 format!(
1424 "Cannot resolve type '{type_name}' from dependency '{}' (via import '{spec_alias}'): no candidate body has completed type resolution",
1425 spec_ref.name
1426 ),
1427 Some(source.clone()),
1428 None::<String>,
1429 Some(spec),
1430 None,
1431 )]);
1432 }
1433 let first = candidates[0];
1434 for later in &candidates[1..] {
1435 if !first.same_interface_ignoring_options(later) {
1436 return Err(vec![Error::validation_with_context(
1437 format!(
1438 "'{spec_alias}.{type_name}' changes across versions of '{}' under `-> effective`",
1439 spec_ref.name
1440 ),
1441 Some(source.clone()),
1442 Some(format!(
1443 "Keep '{type_name}' interface-compatible across every temporal version of '{}', or pin the import to a literal date.",
1444 spec_ref.name
1445 )),
1446 Some(spec),
1447 None,
1448 )]);
1449 }
1450 if first.options.rule != later.options.rule {
1451 return Err(vec![Error::validation_with_context(
1452 format!(
1453 "'{spec_alias}.{type_name}' has different `-> options` rules across versions of '{}' under `-> effective`",
1454 spec_ref.name
1455 ),
1456 Some(source.clone()),
1457 Some(
1458 "Use the same options rule name on every version, or only inline `-> option` values."
1459 .to_string(),
1460 ),
1461 Some(spec),
1462 None,
1463 )]);
1464 }
1465 }
1466 let mut unioned = first.as_ref().clone();
1467 let mut values: Vec<semantics::OptionValue> = Vec::new();
1468 for candidate in &candidates {
1469 for option in &candidate.options.values {
1470 if !values.iter().any(|seen| seen == option) {
1471 values.push(option.clone());
1472 }
1473 }
1474 }
1475 values.sort_by(|left, right| {
1476 left.value
1477 .value
1478 .to_string()
1479 .cmp(&right.value.value.to_string())
1480 .then_with(|| left.value.unit.cmp(&right.value.unit))
1481 });
1482 unioned.options.values = values;
1483 let first_imported = already_resolved
1484 .iter()
1485 .find(|(repo, imported, _)| {
1486 Arc::ptr_eq(repo, &repository)
1487 && crate::parsing::ast::ascii_lowercase_logical_name(imported.name.clone())
1488 == dep_name
1489 })
1490 .map(|(_, imported, _)| *imported)
1491 .expect("BUG: union candidates require a resolved import body");
1492 let identity = SpecIdentity::of(
1493 &repository,
1494 first_imported,
1495 first_imported.effective_from.clone(),
1496 );
1497 let hop = PathSegment::import(spec_alias, &identity);
1498 let unioned = unioned.with_scale_import_hop(&hop);
1499 Ok((
1500 unioned.specifications.clone(),
1501 None,
1502 None,
1503 Arc::new(unioned),
1504 ))
1505}
1506
1507fn topo_sort_type_names(
1509 spec: &LemmaSpec,
1510 data_defs: &HashMap<String, DataTypeDef>,
1511 mut type_names: Vec<String>,
1512) -> Result<Vec<String>, Vec<Error>> {
1513 type_names.sort();
1514 if type_names.is_empty() {
1515 return Ok(type_names);
1516 }
1517 let type_index: HashMap<&str, usize> = type_names
1518 .iter()
1519 .enumerate()
1520 .map(|(index, name)| (name.as_str(), index))
1521 .collect();
1522 let type_count = type_names.len();
1523 let mut in_degree = vec![0usize; type_count];
1524 let mut dependents: Vec<Vec<usize>> = vec![Vec::new(); type_count];
1525 for (child_index, type_name) in type_names.iter().enumerate() {
1526 let custom_parent = match element_parent_type(&data_defs[type_name].parent) {
1527 ParentType::Custom { name, .. } => Some(name.as_str()),
1528 _ => None,
1529 };
1530 let Some(parent_name) = custom_parent else {
1531 continue;
1532 };
1533 let Some(&parent_index) = type_index.get(parent_name) else {
1534 continue;
1535 };
1536 if parent_index == child_index {
1537 continue;
1538 }
1539 in_degree[child_index] += 1;
1540 dependents[parent_index].push(child_index);
1541 }
1542 let mut queue: std::collections::VecDeque<usize> = (0..type_count)
1543 .filter(|&index| in_degree[index] == 0)
1544 .collect();
1545 let mut sorted = Vec::with_capacity(type_count);
1546 while let Some(index) = queue.pop_front() {
1547 sorted.push(type_names[index].clone());
1548 for &dependent_index in &dependents[index] {
1549 in_degree[dependent_index] -= 1;
1550 if in_degree[dependent_index] == 0 {
1551 queue.push_back(dependent_index);
1552 }
1553 }
1554 }
1555 if sorted.len() != type_count {
1556 let cycle_type_name = type_names
1557 .iter()
1558 .find(|name| in_degree[type_index[name.as_str()]] > 0)
1559 .expect("BUG: incomplete topo sort without cycle participant");
1560 return Err(vec![Error::validation_with_context(
1561 format!(
1562 "Circular dependency detected in type resolution: {}::{}",
1563 spec.name, cycle_type_name
1564 ),
1565 Some(data_defs[cycle_type_name].source.clone()),
1566 None::<String>,
1567 Some(spec),
1568 None,
1569 )]);
1570 }
1571 Ok(sorted)
1572}
1573
1574fn partition_list_type_names(
1579 data_defs: &HashMap<String, DataTypeDef>,
1580) -> (Vec<String>, Vec<String>) {
1581 fn has_list(parent: &ParentType) -> bool {
1582 let mut current = parent;
1583 loop {
1584 match current {
1585 ParentType::List { .. } => return true,
1586 ParentType::Ranged { inner } | ParentType::Qualified { inner, .. } => {
1587 current = inner.as_ref();
1588 }
1589 ParentType::Primitive { .. } | ParentType::Custom { .. } => return false,
1590 }
1591 }
1592 }
1593 let mut list_names: HashSet<String> = data_defs
1594 .iter()
1595 .filter(|(_, def)| has_list(&def.parent))
1596 .map(|(name, _)| name.clone())
1597 .collect();
1598 loop {
1599 let before = list_names.len();
1600 for (name, def) in data_defs {
1601 if let ParentType::Custom { name: parent, .. } = element_parent_type(&def.parent) {
1602 if list_names.contains(parent) {
1603 list_names.insert(name.clone());
1604 }
1605 }
1606 }
1607 if list_names.len() == before {
1608 break;
1609 }
1610 }
1611 let scalar_names = data_defs
1612 .keys()
1613 .filter(|name| !list_names.contains(*name))
1614 .cloned()
1615 .collect();
1616 (scalar_names, list_names.into_iter().collect())
1617}
1618
1619type TypeMap = IndexMap<String, Arc<LemmaType>>;
1620
1621fn local_data_or_rule(spec: &LemmaSpec, name: &str) -> bool {
1623 let is_data = spec
1624 .data
1625 .iter()
1626 .any(|d| d.reference.is_local() && d.reference.name == name);
1627 let is_rule = spec.rules.iter().any(|r| r.name == name);
1628 is_data || is_rule
1629}
1630
1631fn is_uses_alias(spec: &LemmaSpec, alias: &str) -> bool {
1633 spec.data.iter().any(|d| {
1634 d.reference.is_local()
1635 && d.reference.name == alias
1636 && matches!(d.value, ParsedDataValue::Import { .. })
1637 })
1638}
1639
1640fn classify_unit_payload_reference(
1645 reference: &ast::Reference,
1646 spec: &LemmaSpec,
1647 unit_index: &PlanningUnitIndex,
1648 resolved: &TypeMap,
1649) -> Result<UnitPayloadClass, String> {
1650 let local = reference.segments.is_empty() && local_data_or_rule(spec, &reference.name);
1651 let unit_ref = reference.to_string();
1652 let resolves_as_unit = unit_index
1653 .resolve_with_named_types(&unit_ref, resolved)
1654 .is_ok();
1655 if local && resolves_as_unit {
1656 return Err(format!(
1657 "unit scale reference '{reference}' is ambiguous: it names both a local data or rule and a unit path"
1658 ));
1659 }
1660 if local {
1661 return Ok(UnitPayloadClass::Scale);
1662 }
1663 if resolves_as_unit {
1664 return Ok(UnitPayloadClass::Compound);
1665 }
1666 if matches!(reference.segments.as_slice(), [alias] if is_uses_alias(spec, alias)) {
1667 return Ok(UnitPayloadClass::Scale);
1668 }
1669 Err(format!(
1670 "Unknown unit '{unit_ref}'. Declare it on a measure or ratio type, or import it with uses."
1671 ))
1672}
1673
1674enum UnitPayloadClass {
1675 Scale,
1676 Compound,
1677}
1678
1679fn rewrite_unit_path_scale_payloads(
1684 resolver: &TypeResolver<'_>,
1685 spec: &LemmaSpec,
1686 at: &EffectiveDate,
1687 already_resolved: &ResolvedTypesMap<'_>,
1688 resolved: &mut TypeMap,
1689 unit_index: &PlanningUnitIndex,
1690 type_sources: &HashMap<String, Source>,
1691) -> Vec<Error> {
1692 let mut errors = Vec::new();
1693 let type_names: Vec<String> = resolved.keys().cloned().collect();
1694 for type_name in type_names {
1695 let lemma_type = resolved
1696 .get(&type_name)
1697 .expect("BUG: type_name comes from resolved keys");
1698 let TypeSpecification::Measure { units, .. } = &lemma_type.specifications else {
1699 continue;
1700 };
1701 let unit_owned_locally = |unit_name: &str| {
1702 unit_index.owners_for(unit_name).iter().any(|owner| {
1703 matches!(
1704 owner,
1705 PlanningOwner::Local {
1706 type_name: local_name
1707 } if local_name == type_name.as_str()
1708 )
1709 })
1710 };
1711 let source = type_sources.get(&type_name).cloned();
1712 let mut compounds: Vec<(usize, String)> = Vec::new();
1713 let mut scale_targets: Vec<(usize, ReferenceTarget)> = Vec::new();
1714 for (idx, unit) in units.iter().enumerate() {
1715 let Some((segments, name)) = unit.scale_reference_parts() else {
1716 continue;
1717 };
1718 if unit.resolved_scale_target().is_some() {
1719 continue;
1720 }
1721 let reference = ast::Reference {
1722 segments: segments.to_vec(),
1723 name: name.to_string(),
1724 };
1725 let source = source.clone().unwrap_or_else(|| {
1726 panic!("BUG: measure type '{type_name}' with a unit payload has no type source")
1727 });
1728 if !unit_owned_locally(&unit.name) {
1731 match resolve_number_scale_reference(
1732 resolver,
1733 spec,
1734 at,
1735 already_resolved,
1736 &reference,
1737 &source,
1738 ) {
1739 Ok(target) => scale_targets.push((idx, target)),
1740 Err(error) => errors.push(error),
1741 }
1742 continue;
1743 }
1744 match classify_unit_payload_reference(&reference, spec, unit_index, resolved) {
1745 Ok(UnitPayloadClass::Scale) => {
1746 match resolve_number_scale_reference(
1747 resolver,
1748 spec,
1749 at,
1750 already_resolved,
1751 &reference,
1752 &source,
1753 ) {
1754 Ok(target) => scale_targets.push((idx, target)),
1755 Err(error) => errors.push(error),
1756 }
1757 }
1758 Ok(UnitPayloadClass::Compound) => {
1759 compounds.push((idx, reference.to_string()));
1760 }
1761 Err(message) => {
1762 errors.push(Error::validation(
1763 format!(
1764 "In spec '{}': unit '{}' in measure type '{}': {}",
1765 spec.name, unit.name, type_name, message
1766 ),
1767 Some(source),
1768 None::<String>,
1769 ));
1770 }
1771 }
1772 }
1773 if compounds.is_empty() && scale_targets.is_empty() {
1774 continue;
1775 }
1776 let mut owned = lemma_type.as_ref().clone();
1777 let TypeSpecification::Measure { units, .. } = &mut owned.specifications else {
1778 unreachable!("BUG: measure type still a measure after clone");
1779 };
1780 for (idx, target) in scale_targets {
1781 units.0[idx] = units.0[idx].clone().with_resolved_scale(target);
1782 }
1783 for (idx, referenced) in compounds {
1784 let name = units.0[idx].name.clone();
1785 units.0[idx] =
1786 MeasureUnit::from_decimal_factor(name, Decimal::ONE, vec![(referenced, 1)])
1787 .expect("BUG: factor 1 always converts to a rational");
1788 }
1789 resolved.insert(type_name, Arc::new(owned));
1790 }
1791 errors
1792}
1793
1794fn resolve_number_scale_reference(
1798 resolver: &TypeResolver<'_>,
1799 spec: &LemmaSpec,
1800 at: &EffectiveDate,
1801 already_resolved: &ResolvedTypesMap<'_>,
1802 reference: &ast::Reference,
1803 source: &Source,
1804) -> Result<ReferenceTarget, Error> {
1805 let scale_reference_error = |message: String| {
1806 Error::validation_with_context(
1807 message,
1808 Some(source.clone()),
1809 None::<String>,
1810 Some(spec),
1811 None,
1812 )
1813 };
1814 if reference.segments.is_empty() {
1815 let is_data = spec
1816 .data
1817 .iter()
1818 .any(|d| d.reference.is_local() && d.reference.name == reference.name);
1819 let is_rule = spec.rules.iter().any(|r| r.name == reference.name);
1820 if is_data && is_rule {
1821 return Err(scale_reference_error(format!(
1822 "Reference target '{}' is ambiguous: both a data and a rule in spec '{}'",
1823 reference.name, spec.name
1824 )));
1825 }
1826 if is_data {
1827 let is_import = spec.data.iter().any(|d| {
1828 d.reference.is_local()
1829 && d.reference.name == reference.name
1830 && matches!(d.value, ParsedDataValue::Import { .. })
1831 });
1832 if is_import {
1833 return Err(scale_reference_error(format!(
1834 "cannot supply spec instance '{}' as a value",
1835 reference.name
1836 )));
1837 }
1838 return Ok(ReferenceTarget::Data(DataPath::local(
1839 reference.name.clone(),
1840 )));
1841 }
1842 if is_rule {
1843 return Ok(ReferenceTarget::Rule(RulePath::new(
1844 Vec::new(),
1845 reference.name.clone(),
1846 )));
1847 }
1848 return Err(scale_reference_error(format!(
1849 "Reference target '{}' not found in spec '{}'",
1850 reference.name, spec.name
1851 )));
1852 }
1853 let [alias] = reference.segments.as_slice() else {
1854 return Err(scale_reference_error(format!(
1855 "unit scale reference '{reference}' is not a data or rule path in spec '{}'",
1856 spec.name
1857 )));
1858 };
1859 let Some(import_row) = spec.data.iter().find(|d| {
1860 d.reference.is_local()
1861 && d.reference.name == *alias
1862 && matches!(d.value, ParsedDataValue::Import { .. })
1863 }) else {
1864 return Err(scale_reference_error(format!(
1865 "Reference target '{}' not found in spec '{}'",
1866 reference, spec.name
1867 )));
1868 };
1869 let Some(spec_ref) = import_row.value.uses_spec_ref() else {
1870 return Err(scale_reference_error(format!(
1871 "Reference target '{}' not found in spec '{}'",
1872 reference, spec.name
1873 )));
1874 };
1875 let (import_repository, target_spec) =
1876 resolver.resolve_spec_for_import(spec, spec_ref, source, at)?;
1877 let Some((_, _, _)) = already_resolved
1878 .iter()
1879 .find(|(_, imported, _)| discovery::same_loaded_spec(imported, target_spec))
1880 else {
1881 return Err(scale_reference_error(format!(
1882 "Reference target '{}' not found in spec '{}'",
1883 reference.name, target_spec.name
1884 )));
1885 };
1886 let is_data = target_spec
1887 .data
1888 .iter()
1889 .any(|d| d.reference.is_local() && d.reference.name == reference.name);
1890 let is_rule = target_spec.rules.iter().any(|r| r.name == reference.name);
1891 if is_data && is_rule {
1892 return Err(scale_reference_error(format!(
1893 "Reference target '{}' is ambiguous: both a data and a rule in spec '{}'",
1894 reference.name, target_spec.name
1895 )));
1896 }
1897 let import_effective = spec_ref.at(at);
1898 let identity = SpecIdentity::of(&import_repository, target_spec, import_effective);
1899 let hop = PathSegment::import(alias, &identity);
1900 if is_data {
1901 return Ok(ReferenceTarget::Data(DataPath {
1902 segments: vec![hop],
1903 data: reference.name.clone(),
1904 }));
1905 }
1906 if is_rule {
1907 return Ok(ReferenceTarget::Rule(RulePath {
1908 segments: vec![hop],
1909 rule: reference.name.clone(),
1910 }));
1911 }
1912 Err(scale_reference_error(format!(
1913 "Reference target '{}' not found in spec '{}'",
1914 reference.name, target_spec.name
1915 )))
1916}
1917
1918fn resolve_measure_decompositions(
1919 spec_name: &str,
1920 this_spec: &SpecVersionKey,
1921 mut resolved: TypeMap,
1922 unit_index: &PlanningUnitIndex,
1923 type_sources: &HashMap<String, Source>,
1924) -> (TypeMap, Vec<Error>) {
1925 let mut errors: Vec<Error> = Vec::new();
1926
1927 let source_for = |type_name: &str| -> Option<Source> { type_sources.get(type_name).cloned() };
1928
1929 let base_type_names: Vec<String> = resolved
1930 .iter()
1931 .filter_map(|(name, lt)| {
1932 if let TypeSpecification::Measure { units, .. } = <.specifications {
1933 if units.iter().all(measure_unit_is_simple) {
1934 return Some(name.clone());
1935 }
1936 }
1937 None
1938 })
1939 .collect();
1940
1941 for type_name in &base_type_names {
1942 let owned = unwrap_unique_resolved_type(
1943 resolved
1944 .shift_remove(type_name)
1945 .expect("BUG: type_name comes from resolved's own keys"),
1946 );
1947 let base_decomp = declared_measure_decomposition(&owned);
1948
1949 let reserved_collision = reserved_calendar_units_in_measure(&owned);
1950 if !reserved_collision.is_empty() && !owned.has_trait_calendar() {
1951 errors.push(Error::validation(
1952 format!(
1953 "In spec '{}': measure type '{}' declares unit(s) {:?} which are reserved calendar unit names.",
1954 spec_name, type_name, reserved_collision
1955 ),
1956 source_for(type_name),
1957 None::<String>,
1958 ));
1959 resolved.insert(type_name.clone(), Arc::new(owned));
1960 continue;
1961 }
1962
1963 let updated = owned.map_measure(|units, _| {
1964 let units = units.map(|u| u.with_decomposition(base_decomp.clone()));
1965 (units, Some(base_decomp.clone()))
1966 });
1967 resolved.insert(type_name.clone(), Arc::new(updated));
1968 }
1969
1970 for lemma_type in resolved.values_mut() {
1971 let TypeSpecification::MeasureRange { units, .. } = &lemma_type.specifications else {
1972 continue;
1973 };
1974 if units.is_empty() {
1975 continue;
1976 }
1977 let family = lemma_type
1978 .family()
1979 .expect("BUG: measure range declaring units must be a named custom type");
1980 let range_decomp: BaseMeasureVector =
1981 [(semantics::Dimension::Family(family.clone()), 1i32)]
1982 .into_iter()
1983 .collect();
1984 let TypeSpecification::MeasureRange {
1985 units,
1986 decomposition,
1987 ..
1988 } = &mut Arc::get_mut(lemma_type)
1989 .expect("BUG: resolved LemmaType Arc unique during type resolution")
1990 .specifications
1991 else {
1992 unreachable!("BUG: matched MeasureRange above");
1993 };
1994 *units = std::mem::take(units).map(|u| u.with_decomposition(range_decomp.clone()));
1995 *decomposition = Some(range_decomp);
1996 }
1997
1998 let mut lookup = UnitDecompLookup::new();
1999
2000 for (unit_name, owner) in unit_index.iter_owners() {
2001 let unit_name = unit_name.to_string();
2002 let lemma_type = owner.lemma_type(&resolved);
2003 if let TypeSpecification::Measure {
2004 decomposition: Some(decomposition),
2005 units,
2006 ..
2007 } = &lemma_type.specifications
2008 {
2009 let measure_name = local_owner_name(owner);
2010 let unit = units
2011 .iter()
2012 .find(|u| u.name == unit_name)
2013 .unwrap_or_else(|| {
2014 panic!(
2015 "BUG: unit_name '{}' from unit_index must be in its owning type's units",
2016 unit_name
2017 )
2018 });
2019 let Some(factor) = unit.literal_factor().cloned() else {
2020 continue;
2021 };
2022 if unit_index.has_unique_owner(unit_name.as_str()) {
2024 lookup.insert(
2025 unit_name.clone(),
2026 (measure_name, decomposition.clone(), factor),
2027 );
2028 }
2029 }
2030 }
2031
2032 for (type_name, lemma_type) in resolved.iter() {
2033 if let TypeSpecification::Measure {
2034 units,
2035 decomposition: Some(decomposition),
2036 ..
2037 } = &lemma_type.specifications
2038 {
2039 let is_defining_type = lemma_type.measure_family().is_some_and(|family| {
2040 family.is_declared_in(this_spec) && family.name == *type_name
2041 });
2042 if !is_defining_type {
2043 continue;
2044 }
2045 for unit in units.iter() {
2046 let declared_by_other_family =
2047 unit_index.owners_for(&unit.name).iter().any(|owner| {
2048 !owner
2049 .lemma_type(&resolved)
2050 .same_measure_family(lemma_type.as_ref())
2051 });
2052 if declared_by_other_family {
2053 continue;
2054 }
2055 let Some(factor) = unit.literal_factor().cloned() else {
2056 continue;
2057 };
2058 lookup.insert(
2059 unit.name.clone(),
2060 (Some(type_name.clone()), decomposition.clone(), factor),
2061 );
2062 }
2063 }
2064 }
2065
2066 let derived_measure_type_names_unsorted: Vec<String> = resolved
2067 .iter()
2068 .filter_map(|(name, lemma_type)| {
2069 if let TypeSpecification::Measure { units, .. } = &lemma_type.specifications {
2070 if units.iter().any(|unit| !measure_unit_is_simple(unit)) {
2071 return Some(name.clone());
2072 }
2073 }
2074 None
2075 })
2076 .collect();
2077
2078 let derived_measure_type_names = match sort_derived_measure_types_for_resolution(
2079 spec_name,
2080 derived_measure_type_names_unsorted,
2081 &resolved,
2082 &lookup,
2083 unit_index,
2084 type_sources,
2085 ) {
2086 Ok(sorted) => sorted,
2087 Err(error) => {
2088 errors.push(error);
2089 return (resolved, errors);
2090 }
2091 };
2092
2093 for type_name in &derived_measure_type_names {
2094 let type_source = source_for(type_name);
2095
2096 let (units_snapshot, already_resolved_body) = match &resolved[type_name].specifications {
2097 TypeSpecification::Measure {
2098 units,
2099 decomposition,
2100 ..
2101 } => (units.clone(), decomposition.clone()),
2102 _ => continue,
2103 };
2104
2105 let body_already_resolved = already_resolved_body.is_some();
2109 let unit_owned_locally = |unit_name: &str| {
2110 unit_index.owners_for(unit_name).iter().any(|owner| {
2111 matches!(
2112 owner,
2113 PlanningOwner::Local {
2114 type_name: local_name
2115 } if local_name == type_name
2116 )
2117 })
2118 };
2119 if body_already_resolved && !units_snapshot.iter().any(|u| unit_owned_locally(&u.name)) {
2120 for unit in units_snapshot.iter() {
2121 if matches!(&unit.scale, crate::literals::UnitScale::Unresolved { .. }) {
2122 panic!(
2123 "BUG: inherited unit '{}' on '{}' still has Unresolved scale; declaring family must classify and resolve unit payloads before dependents clone",
2124 unit.name, type_name
2125 );
2126 }
2127 }
2128 continue;
2129 }
2130
2131 let mut resolved_type_decomp: Option<BaseMeasureVector> = already_resolved_body;
2132 let mut unit_errors: Vec<Error> = Vec::new();
2133 let mut resolved_unit_scales: Vec<Option<ResolvedCompoundScale>> =
2134 vec![None; units_snapshot.len()];
2135 let mut resolve_to_simple: Vec<bool> = vec![false; units_snapshot.len()];
2136
2137 for (unit_idx, unit) in units_snapshot.iter().enumerate() {
2138 if calendar_unit_factor(&unit.name).is_some()
2139 && !resolved[type_name].has_trait_calendar()
2140 {
2141 unit_errors.push(Error::validation(
2142 format!(
2143 "In spec '{}': measure type '{}' declares unit '{}' which is a reserved calendar unit name.",
2144 spec_name, type_name, unit.name
2145 ),
2146 type_source.clone(),
2147 None::<String>,
2148 ));
2149 continue;
2150 }
2151 if measure_unit_is_simple(unit) {
2152 let simple_decomp = declared_measure_decomposition(&resolved[type_name]);
2153
2154 if let Some(existing) = &resolved_type_decomp {
2155 if existing != &simple_decomp {
2156 unit_errors.push(Error::validation(
2157 format!(
2158 "In spec '{}': measure type '{}' has inconsistent unit decompositions. \
2159 Unit '{}' is a simple unit (decomposition {}) but other units \
2160 have decomposition {}.",
2161 spec_name,
2162 type_name,
2163 unit.name,
2164 semantics::format_signature_operator_style(
2165 &semantics::base_measure_vector_as_signature(&simple_decomp)
2166 ),
2167 semantics::format_signature_operator_style(
2168 &semantics::base_measure_vector_as_signature(existing)
2169 )
2170 ),
2171 type_source.clone(),
2172 None::<String>,
2173 ));
2174 }
2175 } else {
2176 resolved_type_decomp = Some(simple_decomp);
2177 }
2178
2179 if let Some(factor) = unit.literal_factor() {
2180 resolved_unit_scales[unit_idx] =
2181 Some(ResolvedCompoundScale::Literal(factor.clone()));
2182 }
2183 continue;
2184 }
2185
2186 if body_already_resolved && !unit_owned_locally(&unit.name) {
2187 if let Some(factor) = unit.literal_factor() {
2188 resolved_unit_scales[unit_idx] =
2189 Some(ResolvedCompoundScale::Literal(factor.clone()));
2190 } else if unit.is_dynamic() {
2191 resolved_unit_scales[unit_idx] = Some(ResolvedCompoundScale::Dynamic {
2192 scale: unit.scale.clone(),
2193 coefficient: unit.scale_coefficient(),
2194 exponent: unit.scale_exponent(),
2195 });
2196 }
2197 continue;
2198 }
2199
2200 match resolve_compound_unit(
2201 spec_name,
2202 type_name,
2203 unit,
2204 &lookup,
2205 unit_index,
2206 &resolved,
2207 type_source.as_ref(),
2208 ) {
2209 Ok((unit_decomp, derived_scale)) => {
2210 if let [(referenced, 1)] = unit.derived_measure_factors.as_slice() {
2211 let (bare_name, owner) = unit_index
2212 .resolve_owner(referenced, &resolved)
2213 .unwrap_or_else(|err| {
2214 panic!(
2215 "BUG: factor '{referenced}' resolved for its conversion but not for its owner: {err}"
2216 )
2217 });
2218 let owning_type = owner.lemma_type(&resolved);
2219 let owner_family = owning_type
2220 .measure_family()
2221 .expect("BUG: resolved unit owner is not a named measure");
2222 let declaring_family = resolved[type_name]
2223 .measure_family()
2224 .expect("BUG: declaring measure has no family");
2225 if owner_family != declaring_family {
2226 unit_errors.push(Error::validation(
2227 format!(
2228 "In spec '{spec_name}': unit '{}' in measure type '{type_name}' cannot use a single unit from another measure. '{bare_name}' belongs to '{}'.",
2229 unit.name, owner_family.name
2230 ),
2231 type_source.clone(),
2232 None::<String>,
2233 ));
2234 }
2235 }
2236 if unit_decomp == declared_measure_decomposition(resolved[type_name].as_ref()) {
2239 resolve_to_simple[unit_idx] = true;
2240 }
2241 if let Some(existing) = &resolved_type_decomp {
2242 if existing != &unit_decomp {
2243 unit_errors.push(Error::validation(
2244 format!(
2245 "In spec '{}': measure type '{}' has inconsistent unit \
2246 decompositions. Unit '{}' resolved to {:?} but other units \
2247 resolved to {:?}. All units of a measure must measure the same \
2248 physical measure.",
2249 spec_name, type_name, unit.name, unit_decomp, existing
2250 ),
2251 type_source.clone(),
2252 None::<String>,
2253 ));
2254 }
2255 } else {
2256 resolved_type_decomp = Some(unit_decomp);
2257 }
2258
2259 resolved_unit_scales[unit_idx] = Some(derived_scale);
2260 }
2261 Err(err) => unit_errors.push(err),
2262 }
2263 }
2264
2265 if !unit_errors.is_empty() {
2266 errors.extend(unit_errors);
2267 continue;
2268 }
2269
2270 let type_decomp = match resolved_type_decomp {
2271 Some(d) => d,
2272 None => continue,
2273 };
2274
2275 let owned = unwrap_unique_resolved_type(
2276 resolved
2277 .shift_remove(type_name)
2278 .expect("BUG: type_name comes from resolved's own keys"),
2279 );
2280
2281 let updated = owned.map_measure(|units, _| {
2282 let units = MeasureUnits(
2283 units
2284 .0
2285 .into_iter()
2286 .enumerate()
2287 .map(|(idx, u)| {
2288 let u = u.with_decomposition(type_decomp.clone());
2289 let u = match resolved_unit_scales[idx].clone() {
2290 Some(ResolvedCompoundScale::Literal(factor)) => u.with_factor(factor),
2291 Some(ResolvedCompoundScale::Dynamic {
2292 scale,
2293 coefficient,
2294 exponent,
2295 }) => u.with_dynamic_scale(scale, coefficient, exponent),
2296 None => u,
2297 };
2298 if resolve_to_simple[idx] {
2299 u.with_derived_measure_factors(Vec::new())
2300 } else {
2301 u
2302 }
2303 })
2304 .collect(),
2305 );
2306 (units, Some(type_decomp.clone()))
2307 });
2308
2309 if let TypeSpecification::Measure { units, .. } = &updated.specifications {
2310 for unit in units.iter() {
2311 if body_already_resolved && !unit_owned_locally(&unit.name) {
2314 continue;
2315 }
2316 let Some(factor) = unit.literal_factor().cloned() else {
2317 continue;
2318 };
2319 lookup.insert(
2320 unit.name.clone(),
2321 (Some(type_name.clone()), type_decomp.clone(), factor),
2322 );
2323 }
2324 }
2325 resolved.insert(type_name.clone(), Arc::new(updated));
2326 }
2327
2328 let resolved = canonicalize_unit_signatures(resolved);
2329
2330 (resolved, errors)
2331}
2332
2333fn finalize_lemma_measure_magnitudes(
2338 lemma_type: LemmaType,
2339 declared_suggestion: Option<&BoundValueKind>,
2340 type_name: &str,
2341) -> Result<LemmaType, String> {
2342 let LemmaType {
2343 name,
2344 mut specifications,
2345 extends,
2346 options,
2347 } = lemma_type;
2348 semantics::finalize_measure_unit_constraint_magnitudes(
2349 &mut specifications,
2350 declared_suggestion.map(|bound| &bound.value),
2351 type_name,
2352 )?;
2353 Ok(LemmaType {
2354 name,
2355 specifications,
2356 extends,
2357 options,
2358 })
2359}
2360
2361fn finalize_measure_magnitudes_in_resolved(
2362 resolved: IndexMap<String, Arc<LemmaType>>,
2363 declared_suggestions: &IndexMap<String, BoundValueKind>,
2364 type_sources: &HashMap<String, Source>,
2365 spec_name: &str,
2366 spec: &LemmaSpec,
2367) -> (IndexMap<String, Arc<LemmaType>>, Vec<Error>) {
2368 resolved.into_iter().fold(
2369 (IndexMap::new(), Vec::new()),
2370 |(mut acc, mut errors), (type_name, arc)| {
2371 let source = type_sources.get(&type_name).cloned().unwrap_or_else(|| {
2372 unreachable!(
2373 "BUG: resolved type '{}' has no corresponding DataTypeDef in spec '{}'",
2374 type_name, spec_name
2375 )
2376 });
2377 let owned = unwrap_unique_resolved_type(arc);
2378 let lemma_type = match finalize_lemma_measure_magnitudes(
2379 owned.clone(),
2380 declared_suggestions.get(&type_name),
2381 &type_name,
2382 ) {
2383 Ok(lt) => lt,
2384 Err(message) => {
2385 errors.push(Error::validation_with_context(
2386 format!(
2387 "Type '{}' has invalid measure unit constraints: {}",
2388 type_name, message
2389 ),
2390 Some(source),
2391 None::<String>,
2392 Some(spec),
2393 None,
2394 ));
2395 owned
2396 }
2397 };
2398 acc.insert(type_name, Arc::new(lemma_type));
2399 (acc, errors)
2400 },
2401 )
2402}
2403
2404fn canonicalize_unit_signatures(types: TypeMap) -> TypeMap {
2412 types
2413 .into_iter()
2414 .map(|(name, arc)| {
2415 (
2416 name,
2417 Arc::new(canonicalize_lemma_unit_signatures(
2418 unwrap_unique_resolved_type(arc),
2419 )),
2420 )
2421 })
2422 .collect()
2423}
2424
2425fn canonicalize_lemma_unit_signatures(lemma_type: LemmaType) -> LemmaType {
2426 lemma_type.map_measure(|units, decomposition| {
2427 let units = units.map(canonicalize_unit_for_measure);
2428 (units, decomposition)
2429 })
2430}
2431
2432fn reserved_calendar_units_in_measure(lemma_type: &LemmaType) -> Vec<String> {
2433 let TypeSpecification::Measure { units, .. } = &lemma_type.specifications else {
2434 return Vec::new();
2435 };
2436 units
2437 .iter()
2438 .filter(|u| calendar_unit_factor(&u.name).is_some())
2439 .map(|u| u.name.clone())
2440 .collect()
2441}
2442
2443fn measure_unit_is_simple(unit: &MeasureUnit) -> bool {
2444 unit.derived_measure_factors.is_empty()
2445 || (unit.derived_measure_factors.len() == 1
2446 && unit.derived_measure_factors[0].0 == unit.name
2447 && unit.derived_measure_factors[0].1 == 1)
2448}
2449
2450fn canonicalize_unit_for_measure(unit: MeasureUnit) -> MeasureUnit {
2451 let factors = if measure_unit_is_simple(&unit) {
2452 vec![(unit.name.clone(), 1)]
2453 } else {
2454 canonicalize_signature(&unit.derived_measure_factors)
2455 };
2456 unit.with_derived_measure_factors(factors)
2457}
2458
2459#[derive(Clone, Debug)]
2461enum ResolvedCompoundScale {
2462 Literal(RationalInteger),
2463 Dynamic {
2466 scale: crate::literals::UnitScale,
2467 coefficient: RationalInteger,
2468 exponent: i32,
2469 },
2470}
2471
2472fn resolve_compound_unit(
2473 spec_name: &str,
2474 declaring_type_name: &str,
2475 unit: &MeasureUnit,
2476 lookup: &UnitDecompLookup,
2477 unit_index: &PlanningUnitIndex,
2478 resolved: &IndexMap<String, Arc<LemmaType>>,
2479 source: Option<&Source>,
2480) -> Result<(BaseMeasureVector, ResolvedCompoundScale), Error> {
2481 use crate::computation::bigint::BigInt;
2482 use crate::computation::rational::{checked_mul, checked_pow_integer, rational_one};
2483
2484 let mut result: BaseMeasureVector = BaseMeasureVector::new();
2485 let mut coefficient = unit.literal_factor().cloned().unwrap_or_else(rational_one);
2486 let mut dynamic: Option<(crate::literals::UnitScale, i32)> = None;
2487
2488 for (measure_ref, exponent) in &unit.derived_measure_factors {
2489 let (owning_measure_name, owning_decomp, component) = if let Some(entry) =
2490 lookup.get(measure_ref.as_str())
2491 {
2492 (
2493 entry.0.clone(),
2494 entry.1.clone(),
2495 CompoundComponent::Literal(entry.2.clone()),
2496 )
2497 } else {
2498 let (bare, owner) = unit_index
2499 .resolve_owner(measure_ref, resolved)
2500 .map_err(|err| {
2501 Error::validation(
2502 format!(
2503 "In spec '{}': unit '{}' in measure type '{}' references '{}': {}. \
2504 Add `uses <spec>` (or declare the owning measure type in this spec) \
2505 so its units are in scope.",
2506 spec_name, unit.name, declaring_type_name, measure_ref, err
2507 ),
2508 source.cloned(),
2509 None::<String>,
2510 )
2511 })?;
2512 let owning_type = owner.lemma_type(resolved);
2513 let TypeSpecification::Measure {
2514 decomposition: Some(decomposition),
2515 units,
2516 ..
2517 } = &owning_type.specifications
2518 else {
2519 return Err(Error::validation(
2520 format!(
2521 "In spec '{}': unit '{}' in measure type '{}' references '{}' which did not resolve to a measure unit",
2522 spec_name, unit.name, declaring_type_name, measure_ref
2523 ),
2524 source.cloned(),
2525 None::<String>,
2526 ));
2527 };
2528 let referenced = units.iter().find(|u| u.name == bare).unwrap_or_else(|| {
2529 panic!(
2530 "BUG: resolved unit '{}' must be declared on type '{}'",
2531 bare,
2532 owning_type.name()
2533 )
2534 });
2535 let component = if let Some(factor) = referenced.literal_factor().cloned() {
2536 CompoundComponent::Literal(factor)
2537 } else {
2538 CompoundComponent::Dynamic {
2539 scale: referenced.scale.clone(),
2540 bare: bare.to_string(),
2541 coefficient: referenced.scale_coefficient(),
2542 exponent: referenced.scale_exponent(),
2543 }
2544 };
2545 (local_owner_name(owner), decomposition.clone(), component)
2546 };
2547 let owning_decomp = &owning_decomp;
2548
2549 if owning_measure_name.as_deref() == Some(declaring_type_name) {
2550 return Err(Error::validation(
2551 format!(
2552 "In spec '{}': unit '{}' in measure type '{}' references unit '{}' which \
2553 belongs to the same measure type. A measure cannot reference its own units \
2554 in a compound expression.",
2555 spec_name, unit.name, declaring_type_name, measure_ref
2556 ),
2557 source.cloned(),
2558 None::<String>,
2559 ));
2560 }
2561
2562 for (dim, &dim_exp) in owning_decomp {
2563 let combined = dim_exp
2564 .checked_mul(*exponent)
2565 .and_then(|value| accumulate(&mut result, dim, value));
2566 if combined.is_none() {
2567 return Err(unit_exponent_limit_error(
2568 spec_name,
2569 &unit.name,
2570 declaring_type_name,
2571 source,
2572 ));
2573 }
2574 }
2575
2576 match component {
2577 CompoundComponent::Literal(unit_factor) => {
2578 let component_contribution =
2579 checked_pow_integer(&unit_factor, &BigInt::from_i64(i64::from(*exponent)))
2580 .map_err(|error| {
2581 overflow_to_validation_error(
2582 spec_name,
2583 &unit.name,
2584 declaring_type_name,
2585 measure_ref,
2586 error,
2587 source,
2588 )
2589 })?;
2590 coefficient =
2591 checked_mul(&coefficient, &component_contribution).map_err(|error| {
2592 overflow_to_validation_error(
2593 spec_name,
2594 &unit.name,
2595 declaring_type_name,
2596 measure_ref,
2597 error,
2598 source,
2599 )
2600 })?;
2601 }
2602 CompoundComponent::Dynamic {
2603 scale,
2604 bare,
2605 coefficient: base_coefficient,
2606 exponent: base_exponent,
2607 } => {
2608 let raised_coefficient =
2609 checked_pow_integer(&base_coefficient, &BigInt::from_i64(i64::from(*exponent)))
2610 .map_err(|error| {
2611 overflow_to_validation_error(
2612 spec_name,
2613 &unit.name,
2614 declaring_type_name,
2615 measure_ref,
2616 error,
2617 source,
2618 )
2619 })?;
2620 coefficient = checked_mul(&coefficient, &raised_coefficient).map_err(|error| {
2621 overflow_to_validation_error(
2622 spec_name,
2623 &unit.name,
2624 declaring_type_name,
2625 measure_ref,
2626 error,
2627 source,
2628 )
2629 })?;
2630 let component_exponent = base_exponent
2631 .checked_mul(*exponent)
2632 .ok_or_else(|| {
2633 Error::validation(
2634 format!(
2635 "In spec '{spec_name}': unit '{}' in measure type '{declaring_type_name}' overflowed combining dynamic unit '{bare}' exponents.",
2636 unit.name
2637 ),
2638 source.cloned(),
2639 None::<String>,
2640 )
2641 })?;
2642 match &mut dynamic {
2643 None => {
2644 dynamic = Some((scale, component_exponent));
2645 }
2646 Some((existing_scale, existing_exponent)) => {
2647 if !same_dynamic_scale_identity(existing_scale, &scale) {
2648 return Err(Error::validation(
2649 format!(
2650 "In spec '{spec_name}': unit '{}' in measure type '{declaring_type_name}' combines distinct dynamic units in one compound expression.",
2651 unit.name
2652 ),
2653 source.cloned(),
2654 None::<String>,
2655 ));
2656 }
2657 *existing_exponent = existing_exponent
2658 .checked_add(component_exponent)
2659 .ok_or_else(|| {
2660 Error::validation(
2661 format!(
2662 "In spec '{spec_name}': unit '{}' in measure type '{declaring_type_name}' overflowed combining dynamic unit '{bare}' exponents.",
2663 unit.name
2664 ),
2665 source.cloned(),
2666 None::<String>,
2667 )
2668 })?;
2669 }
2670 }
2671 }
2672 }
2673 }
2674
2675 let derived_scale = match dynamic {
2676 None => ResolvedCompoundScale::Literal(coefficient),
2677 Some((scale, exponent)) => ResolvedCompoundScale::Dynamic {
2678 scale,
2679 coefficient,
2680 exponent,
2681 },
2682 };
2683 Ok((result, derived_scale))
2684}
2685
2686enum CompoundComponent {
2687 Literal(RationalInteger),
2688 Dynamic {
2689 scale: crate::literals::UnitScale,
2690 bare: String,
2691 coefficient: RationalInteger,
2692 exponent: i32,
2693 },
2694}
2695
2696fn same_dynamic_scale_identity(
2697 left: &crate::literals::UnitScale,
2698 right: &crate::literals::UnitScale,
2699) -> bool {
2700 match (left, right) {
2701 (
2702 crate::literals::UnitScale::Unresolved {
2703 segments: left_segments,
2704 name: left_name,
2705 ..
2706 },
2707 crate::literals::UnitScale::Unresolved {
2708 segments: right_segments,
2709 name: right_name,
2710 ..
2711 },
2712 ) => left_segments == right_segments && left_name == right_name,
2713 (
2714 crate::literals::UnitScale::Reference {
2715 target: left_target,
2716 ..
2717 },
2718 crate::literals::UnitScale::Reference {
2719 target: right_target,
2720 ..
2721 },
2722 ) => left_target == right_target,
2723 _ => false,
2724 }
2725}
2726
2727fn overflow_to_validation_error(
2728 spec_name: &str,
2729 unit_name: &str,
2730 declaring_type_name: &str,
2731 measure_ref: &str,
2732 failure: NumericFailure,
2733 source: Option<&Source>,
2734) -> Error {
2735 Error::validation(
2736 format!(
2737 "In spec '{}': unit '{}' in measure type '{}' overflowed while combining '{}': {}",
2738 spec_name, unit_name, declaring_type_name, measure_ref, failure
2739 ),
2740 source.cloned(),
2741 None::<String>,
2742 )
2743}
2744
2745fn unit_exponent_limit_error(
2746 spec_name: &str,
2747 unit_name: &str,
2748 declaring_type_name: &str,
2749 source: Option<&Source>,
2750) -> Error {
2751 Error::validation(
2752 format!(
2753 "In spec '{}': unit '{}' in measure type '{}' has a unit exponent exceeding the maximum of {}",
2754 spec_name,
2755 unit_name,
2756 declaring_type_name,
2757 MAX_UNIT_EXPONENT
2758 ),
2759 source.cloned(),
2760 None::<String>,
2761 )
2762}
2763
2764fn accumulate(
2766 result: &mut BaseMeasureVector,
2767 dim: &semantics::Dimension,
2768 value: i32,
2769) -> Option<()> {
2770 let entry = result.entry(dim.clone()).or_insert(0);
2771 *entry = entry.checked_add(value)?;
2772 if entry.unsigned_abs() > MAX_UNIT_EXPONENT.unsigned_abs() {
2773 return None;
2774 }
2775 if *entry == 0 {
2776 result.remove(dim);
2777 }
2778 Some(())
2779}
2780
2781#[derive(Debug, Clone, Default)]
2787pub struct ResolvedSpecTypes {
2788 pub resolved: IndexMap<String, Arc<LemmaType>>,
2791
2792 pub declared_suggestions: IndexMap<String, BoundValueKind>,
2798
2799 pub declared_values: IndexMap<String, BoundValueKind>,
2803
2804 pub(crate) raw_suggestions: Vec<(String, RawSuggestion)>,
2806
2807 pub(crate) raw_values: Vec<(String, RawSuggestion)>,
2809
2810 pub(crate) source_defaults: IndexMap<String, RawSuggestion>,
2812
2813 pub(crate) source_value_defaults: IndexMap<String, RawSuggestion>,
2815
2816 pub(crate) rule_list_types: Vec<(ParentType, Arc<LemmaType>)>,
2819
2820 pub unit_index: crate::planning::unit_index::UnitIndex,
2823
2824 pub(crate) deferred_signature_collisions:
2827 Vec<(String, crate::planning::semantics::BaseMeasureVector)>,
2828
2829 planning_unit_index: PlanningUnitIndex,
2831
2832 pub signature_index: crate::computation::arithmetic::SignatureIndex,
2835}
2836
2837#[derive(Debug, Clone, PartialEq)]
2839pub(crate) struct DataTypeDef {
2840 pub parent: ParentType,
2841 pub constraints: Option<Vec<Constraint>>,
2842 pub source: crate::parsing::source::Source,
2843 pub name: String,
2844 pub bound_literal: Option<ast::Value>,
2846}
2847
2848#[derive(Debug, Clone)]
2852pub(crate) struct TypeResolver<'a> {
2853 data_types: Vec<(&'a LemmaSpec, HashMap<String, DataTypeDef>)>,
2854 context: &'a Context,
2855 all_registered_specs: Vec<(Arc<LemmaRepository>, &'a LemmaSpec)>,
2856 specs_with_failed_registration: Vec<&'a LemmaSpec>,
2861 slices: HashMap<SpecVersionKey, Arc<instance_list::VersionSlices>>,
2863}
2864
2865fn element_parent_type(parent: &ParentType) -> &ParentType {
2867 let mut current = parent;
2868 while let ParentType::Ranged { inner } | ParentType::List { inner } = current {
2869 current = inner.as_ref();
2870 }
2871 current
2872}
2873
2874pub(crate) fn time_range_endpoints_share_timezone(
2875 left: &semantics::SemanticTime,
2876 right: &semantics::SemanticTime,
2877) -> bool {
2878 match (&left.timezone, &right.timezone) {
2879 (None, None) => true,
2880 (Some(left_tz), Some(right_tz)) => left_tz.offset_minutes == right_tz.offset_minutes,
2881 _ => false,
2882 }
2883}
2884
2885fn parser_value_base_type_display_name(value: &ast::Value) -> &'static str {
2889 match value {
2890 ast::Value::Number(_) => "number",
2891 ast::Value::Text(_) => "text",
2892 ast::Value::Boolean(_) => "boolean",
2893 ast::Value::Date(_) => "date",
2894 ast::Value::Time(_) => "time",
2895 ast::Value::NumberWithUnit(_, unit) => match unit.as_str() {
2896 "percent" | "permille" => "ratio",
2897 _ => "measure",
2898 },
2899 ast::Value::Range(_, _) => "range",
2900 }
2901}
2902
2903fn inferred_parent_type_from_literal(value: &ast::Value) -> Result<ParentType, String> {
2908 let parent_type = match value {
2909 ast::Value::Number(_) => ParentType::Primitive {
2910 primitive: PrimitiveKind::Number,
2911 },
2912 ast::Value::Text(_) => ParentType::Primitive {
2913 primitive: PrimitiveKind::Text,
2914 },
2915 ast::Value::Boolean(_) => ParentType::Primitive {
2916 primitive: PrimitiveKind::Boolean,
2917 },
2918 ast::Value::Date(_) => ParentType::Primitive {
2919 primitive: PrimitiveKind::Date,
2920 },
2921 ast::Value::Time(_) => ParentType::Primitive {
2922 primitive: PrimitiveKind::Time,
2923 },
2924 ast::Value::NumberWithUnit(_, _) => ParentType::Primitive {
2925 primitive: PrimitiveKind::Measure,
2926 },
2927 ast::Value::Range(left, right) => {
2928 let primitive = match (left.as_ref(), right.as_ref()) {
2929 (ast::Value::Number(_), ast::Value::Number(_)) => PrimitiveKind::NumberRange,
2930 (ast::Value::Date(_), ast::Value::Date(_)) => PrimitiveKind::DateRange,
2931 (ast::Value::Time(_), ast::Value::Time(_)) => PrimitiveKind::TimeRange,
2932 (ast::Value::NumberWithUnit(_, u1), ast::Value::NumberWithUnit(_, u2))
2933 if u1 == u2 && matches!(u1.as_str(), "percent" | "permille") =>
2934 {
2935 PrimitiveKind::RatioRange
2936 }
2937 (ast::Value::NumberWithUnit(_, _), ast::Value::NumberWithUnit(_, _)) => {
2938 PrimitiveKind::MeasureRange
2939 }
2940 (left_value, right_value) => {
2941 return Err(format!(
2942 "range endpoints must have the same supported base type, got {} and {}",
2943 parser_value_base_type_display_name(left_value),
2944 parser_value_base_type_display_name(right_value)
2945 ));
2946 }
2947 };
2948 ParentType::Primitive { primitive }
2949 }
2950 };
2951 Ok(parent_type)
2952}
2953
2954impl<'a> TypeResolver<'a> {
2955 pub fn new(
2956 context: &'a Context,
2957 slices: HashMap<SpecVersionKey, Arc<instance_list::VersionSlices>>,
2958 ) -> Self {
2959 TypeResolver {
2960 data_types: Vec::new(),
2961 context,
2962 all_registered_specs: Vec::new(),
2963 specs_with_failed_registration: Vec::new(),
2964 slices,
2965 }
2966 }
2967
2968 fn identity_at(
2970 &self,
2971 repository: &LemmaRepository,
2972 spec: &LemmaSpec,
2973 at: &EffectiveDate,
2974 ) -> SpecIdentity {
2975 self.slices
2976 .get(&SpecVersionKey::of(repository, spec))
2977 .expect("BUG: a spec type resolution reaches is in the graph's dependency order")
2978 .identity_at(repository, spec, at)
2979 }
2980
2981 fn rule_spec_element_list_type(
2984 &self,
2985 spec: &LemmaSpec,
2986 parent: &ParentType,
2987 source: &Source,
2988 at: &EffectiveDate,
2989 ) -> Option<Arc<LemmaType>> {
2990 let (name, spec_ref) = Self::list_of_spec_name(spec, parent)?;
2991 let (row_repository, row_spec) = match discovery::resolve_spec_ref_or_self(
2992 self.context,
2993 &spec_ref,
2994 self.registered_repository(spec),
2995 spec,
2996 at,
2997 Some(source.clone()),
2998 ) {
2999 Ok(pair) => pair,
3000 Err(_) => return None,
3001 };
3002 let identity = self.identity_at(&row_repository, row_spec, &spec_ref.at(at));
3003 let element = Arc::new(LemmaType::primitive(TypeSpecification::Spec {
3004 spec: identity,
3005 }));
3006 Some(Arc::new(LemmaType {
3007 name: Some(format!("{name} list")),
3008 specifications: TypeSpecification::List {
3009 element,
3010 key: ListKey::Numbered,
3011 help: String::new(),
3012 },
3013 extends: TypeExtends::Primitive,
3014 options: TypeOptions::default(),
3015 }))
3016 }
3017
3018 fn list_of_spec_name<'p>(
3022 spec: &LemmaSpec,
3023 parent: &'p ParentType,
3024 ) -> Option<(&'p str, ast::SpecRef)> {
3025 let ParentType::List { inner } = parent else {
3026 return None;
3027 };
3028 let ParentType::Custom { name } = inner.as_ref() else {
3029 return None;
3030 };
3031 discovery::list_element_spec_ref(spec, inner).map(|spec_ref| (name.as_str(), spec_ref))
3032 }
3033
3034 pub fn is_registered(&self, spec: &LemmaSpec) -> bool {
3035 self.all_registered_specs
3036 .iter()
3037 .any(|(_, s)| discovery::same_loaded_spec(s, spec))
3038 }
3039
3040 fn registered_repository(&self, spec: &LemmaSpec) -> &Arc<LemmaRepository> {
3042 self.all_registered_specs
3043 .iter()
3044 .find(|(_, registered)| discovery::same_loaded_spec(registered, spec))
3045 .map(|(repository, _)| repository)
3046 .expect("BUG: types are resolved only for specs registered with their repository")
3047 }
3048
3049 fn mark_registration_failure(&mut self, spec: &'a LemmaSpec) {
3051 if !self
3052 .specs_with_failed_registration
3053 .iter()
3054 .any(|s| discovery::same_loaded_spec(s, spec))
3055 {
3056 self.specs_with_failed_registration.push(spec);
3057 }
3058 }
3059
3060 pub fn registration_failed(&self, spec: &LemmaSpec) -> bool {
3065 self.specs_with_failed_registration
3066 .iter()
3067 .any(|s| discovery::same_loaded_spec(s, spec))
3068 }
3069
3070 pub fn register_all(
3072 &mut self,
3073 repository: &Arc<LemmaRepository>,
3074 spec: &'a LemmaSpec,
3075 ) -> Vec<Error> {
3076 self.record_repository(repository, spec);
3077
3078 let mut errors = Vec::new();
3079 for data in &spec.data {
3080 match &data.value {
3081 ParsedDataValue::Definition {
3082 base,
3083 constraints,
3084 value,
3085 list: _,
3086 } => {
3087 if matches!(
3088 (base.as_ref(), constraints.as_ref(), value.as_ref()),
3089 (None, None, Some(Value::NumberWithUnit(_, _)),)
3090 ) {
3091 continue;
3092 }
3093 let name = &data.reference.name;
3094 let parent = match (base.as_ref(), value.as_ref()) {
3095 (Some(b), _) => b.clone(),
3096 (None, Some(v)) => match inferred_parent_type_from_literal(v) {
3097 Ok(parent) => parent,
3098 Err(message) => {
3099 errors.push(Error::validation_with_context(
3100 message,
3101 Some(data.source_location.clone()),
3102 None::<String>,
3103 Some(spec),
3104 None,
3105 ));
3106 self.mark_registration_failure(spec);
3107 continue;
3108 }
3109 },
3110 (None, None) => {
3111 errors.push(Error::validation_with_context(
3112 format!(
3113 "Data '{name}' in spec '{}' must declare a type or a literal value",
3114 spec.name
3115 ),
3116 Some(data.source_location.clone()),
3117 None::<String>,
3118 Some(spec),
3119 None,
3120 ));
3121 self.mark_registration_failure(spec);
3122 continue;
3123 }
3124 };
3125 if Self::list_of_spec_name(spec, &parent).is_some() {
3128 continue;
3129 }
3130 let ftd = DataTypeDef {
3131 parent,
3132 constraints: constraints.clone(),
3133 source: data.source_location.clone(),
3134 name: name.clone(),
3135 bound_literal: value.clone(),
3136 };
3137 if let Err(e) = self.register_type(repository, spec, ftd) {
3138 errors.push(e);
3139 }
3140 }
3141 ParsedDataValue::Import { .. } | ParsedDataValue::ImportList { .. } => {}
3142 }
3143 }
3144 errors
3145 }
3146
3147 fn record_repository(&mut self, repository: &Arc<LemmaRepository>, spec: &'a LemmaSpec) {
3148 if !self
3149 .all_registered_specs
3150 .iter()
3151 .any(|(_, s)| discovery::same_loaded_spec(s, spec))
3152 {
3153 self.all_registered_specs
3154 .push((Arc::clone(repository), spec));
3155 }
3156 }
3157
3158 pub fn register_type(
3160 &mut self,
3161 repository: &Arc<LemmaRepository>,
3162 spec: &'a LemmaSpec,
3163 def: DataTypeDef,
3164 ) -> Result<(), Error> {
3165 self.record_repository(repository, spec);
3166 let spec_types = if let Some(pos) = self
3167 .data_types
3168 .iter()
3169 .position(|(s, _)| discovery::same_loaded_spec(s, spec))
3170 {
3171 &mut self.data_types[pos].1
3172 } else {
3173 self.data_types.push((spec, HashMap::new()));
3174 let last = self.data_types.len() - 1;
3175 &mut self.data_types[last].1
3176 };
3177 if spec_types.contains_key(&def.name) {
3178 return Err(Error::validation_with_context(
3179 format!(
3180 "The name '{}' is already used for data in this spec.",
3181 def.name
3182 ),
3183 Some(def.source.clone()),
3184 None::<String>,
3185 Some(spec),
3186 None,
3187 ));
3188 }
3189 spec_types.insert(def.name.clone(), def);
3190 Ok(())
3191 }
3192
3193 pub fn resolve_and_validate(
3196 &self,
3197 spec: &LemmaSpec,
3198 at: &EffectiveDate,
3199 already_resolved: &ResolvedTypesMap,
3200 ) -> Result<ResolvedSpecTypes, Vec<Error>> {
3201 let (mut resolved_types, list_type_names) =
3202 self.resolve_types_internal(spec, at, already_resolved)?;
3203 resolved_types.source_defaults = resolved_types
3204 .raw_suggestions
3205 .iter()
3206 .map(|(name, raw)| (name.clone(), raw.clone()))
3207 .collect();
3208 resolved_types.source_value_defaults = resolved_types
3209 .raw_values
3210 .iter()
3211 .map(|(name, raw)| (name.clone(), raw.clone()))
3212 .collect();
3213 let mut errors = Vec::new();
3214
3215 let type_sources: std::collections::HashMap<String, Source> = resolved_types
3217 .resolved
3218 .keys()
3219 .chain(list_type_names.iter())
3220 .filter_map(|type_name| {
3221 self.data_types
3222 .iter()
3223 .find(|(s, _)| discovery::same_loaded_spec(s, spec))
3224 .and_then(|(_, defs)| defs.get(type_name.as_str()))
3225 .map(|ftd| (type_name.clone(), ftd.source.clone()))
3226 })
3227 .collect();
3228
3229 errors.extend(rewrite_unit_path_scale_payloads(
3231 self,
3232 spec,
3233 at,
3234 already_resolved,
3235 &mut resolved_types.resolved,
3236 &resolved_types.planning_unit_index,
3237 &type_sources,
3238 ));
3239 let this_spec = SpecVersionKey::of(self.registered_repository(spec), spec);
3240 let (new_resolved, decomp_errors) = resolve_measure_decompositions(
3241 &spec.name,
3242 &this_spec,
3243 std::mem::take(&mut resolved_types.resolved),
3244 &resolved_types.planning_unit_index,
3245 &type_sources,
3246 );
3247 resolved_types.resolved = new_resolved;
3248 errors.extend(decomp_errors);
3249
3250 for (type_name, raw) in std::mem::take(&mut resolved_types.raw_suggestions) {
3251 let lemma_type = resolved_types
3252 .resolved
3253 .get(&type_name)
3254 .expect("BUG: raw default for type not in resolved");
3255 match bound_value_kind_from_raw_suggestion(raw, &lemma_type.specifications, &type_name)
3256 {
3257 Ok(value_kind) => {
3258 resolved_types
3259 .declared_suggestions
3260 .insert(type_name, value_kind);
3261 }
3262 Err(message) => {
3263 let source = type_sources
3264 .get(&type_name)
3265 .cloned()
3266 .unwrap_or_else(|| unreachable!("BUG: type '{}' has no source", type_name));
3267 errors.push(Error::validation_with_context(
3268 message,
3269 Some(source),
3270 None::<String>,
3271 Some(spec),
3272 None,
3273 ));
3274 }
3275 }
3276 }
3277
3278 for (type_name, raw) in std::mem::take(&mut resolved_types.raw_values) {
3279 let lemma_type = resolved_types
3280 .resolved
3281 .get(&type_name)
3282 .expect("BUG: raw fill for type not in resolved");
3283 match bound_value_kind_from_raw_suggestion(raw, &lemma_type.specifications, &type_name)
3284 {
3285 Ok(value_kind) => {
3286 resolved_types.declared_values.insert(type_name, value_kind);
3287 }
3288 Err(message) => {
3289 let source = type_sources
3290 .get(&type_name)
3291 .cloned()
3292 .unwrap_or_else(|| unreachable!("BUG: type '{}' has no source", type_name));
3293 errors.push(Error::validation_with_context(
3294 message,
3295 Some(source),
3296 None::<String>,
3297 Some(spec),
3298 None,
3299 ));
3300 }
3301 }
3302 }
3303
3304 if let Some((_, data_defs)) = self
3305 .data_types
3306 .iter()
3307 .find(|(s, _)| discovery::same_loaded_spec(s, spec))
3308 {
3309 errors.extend(refresh_named_range_specs(
3310 self,
3311 spec,
3312 data_defs,
3313 &mut resolved_types,
3314 already_resolved,
3315 at,
3316 ));
3317 errors.extend(apply_deferred_named_range_constraints(
3318 spec,
3319 data_defs,
3320 &mut resolved_types.resolved,
3321 &mut resolved_types.declared_suggestions,
3322 &mut resolved_types.declared_values,
3323 &type_sources,
3324 &resolved_types.planning_unit_index,
3325 ));
3326 }
3327
3328 let (new_resolved, resolved_errors) = finalize_measure_magnitudes_in_resolved(
3329 std::mem::take(&mut resolved_types.resolved),
3330 &resolved_types.declared_suggestions,
3331 &type_sources,
3332 &spec.name,
3333 spec,
3334 );
3335 resolved_types.resolved = new_resolved;
3336
3337 resolved_types.unit_index = std::mem::take(&mut resolved_types.planning_unit_index)
3338 .bind(&resolved_types.resolved, &this_spec);
3339
3340 match build_signature_index(&spec.name, &resolved_types.unit_index, already_resolved) {
3341 Ok(built) => {
3342 resolved_types.signature_index = built.index;
3343 resolved_types.deferred_signature_collisions = built.deferred_cross_type;
3344 }
3345 Err(error) => errors.push(error),
3346 }
3347
3348 if !resolved_errors.is_empty() {
3349 errors.extend(resolved_errors);
3350 return Err(errors);
3351 }
3352
3353 if let Err(list_errors) = self.resolve_list_types(
3354 spec,
3355 at,
3356 already_resolved,
3357 &list_type_names,
3358 &mut resolved_types,
3359 ) {
3360 errors.extend(list_errors);
3361 return Err(errors);
3362 }
3363
3364 if let Err(rule_list_errors) =
3365 self.resolve_rule_list_types(spec, at, already_resolved, &mut resolved_types)
3366 {
3367 errors.extend(rule_list_errors);
3368 return Err(errors);
3369 }
3370
3371 let mut validated_in_unit_index: HashSet<String> = HashSet::new();
3372 for lemma_type in resolved_types.unit_index.values() {
3373 let Some(measure_family) = lemma_type.measure_family_name() else {
3374 continue;
3375 };
3376 if !lemma_type.is_measure()
3377 || !validated_in_unit_index.insert(measure_family.to_string())
3378 {
3379 continue;
3380 }
3381 let type_name = lemma_type
3382 .name
3383 .as_deref()
3384 .filter(|name| *name == measure_family)
3385 .unwrap_or(measure_family);
3386 let source = type_sources
3387 .get(type_name)
3388 .cloned()
3389 .or_else(|| {
3390 self.data_types
3391 .iter()
3392 .find_map(|(_, defs)| defs.get(type_name).map(|def| def.source.clone()))
3393 })
3394 .unwrap_or_else(|| {
3395 unreachable!(
3396 "BUG: measure type '{}' in unit_index has no DataTypeDef source",
3397 type_name
3398 )
3399 });
3400 errors.extend(validate_type_specifications(
3401 &lemma_type.specifications,
3402 resolved_types.declared_suggestions.get(type_name),
3403 "suggestion",
3404 type_name,
3405 &source,
3406 Some(spec),
3407 &resolved_types.unit_index,
3408 ));
3409 errors.extend(validate_type_specifications(
3410 &lemma_type.specifications,
3411 resolved_types.declared_values.get(type_name),
3412 "with",
3413 type_name,
3414 &source,
3415 Some(spec),
3416 &resolved_types.unit_index,
3417 ));
3418 }
3419
3420 for (type_name, lemma_type) in &resolved_types.resolved {
3421 let source = type_sources.get(type_name).cloned().unwrap_or_else(|| {
3422 unreachable!(
3423 "BUG: resolved type '{}' has no corresponding DataTypeDef in spec '{}'",
3424 type_name, spec.name
3425 )
3426 });
3427 let mut spec_errors = validate_type_specifications(
3428 &lemma_type.specifications,
3429 resolved_types.declared_suggestions.get(type_name),
3430 "suggestion",
3431 type_name,
3432 &source,
3433 Some(spec),
3434 &resolved_types.unit_index,
3435 );
3436 errors.append(&mut spec_errors);
3437 let mut fill_errors = validate_type_specifications(
3438 &lemma_type.specifications,
3439 resolved_types.declared_values.get(type_name),
3440 "with",
3441 type_name,
3442 &source,
3443 Some(spec),
3444 &resolved_types.unit_index,
3445 );
3446 errors.append(&mut fill_errors);
3447 errors.extend(validate_type_options(
3448 lemma_type,
3449 resolved_types.declared_suggestions.get(type_name),
3450 "suggestion",
3451 type_name,
3452 &source,
3453 Some(spec),
3454 &resolved_types.unit_index,
3455 ));
3456 errors.extend(validate_type_options(
3457 lemma_type,
3458 resolved_types.declared_values.get(type_name),
3459 "with",
3460 type_name,
3461 &source,
3462 Some(spec),
3463 &resolved_types.unit_index,
3464 ));
3465 }
3466
3467 for (type_name, lemma_type) in &resolved_types.resolved {
3468 let is_range = matches!(
3469 &lemma_type.specifications,
3470 TypeSpecification::NumberRange { .. }
3471 | TypeSpecification::DateRange { .. }
3472 | TypeSpecification::TimeRange { .. }
3473 | TypeSpecification::MeasureRange { .. }
3474 | TypeSpecification::RatioRange { .. }
3475 );
3476 if !is_range {
3477 continue;
3478 }
3479 let source = type_sources.get(type_name).cloned().unwrap_or_else(|| {
3480 unreachable!(
3481 "BUG: resolved type '{}' has no corresponding DataTypeDef in spec '{}'",
3482 type_name, spec.name
3483 )
3484 });
3485 if let Some(default_kind) = resolved_types.declared_suggestions.get(type_name.as_str())
3486 {
3487 let lit = TypedLiteral {
3488 value: default_kind.value.clone(),
3489 lemma_type: Arc::clone(lemma_type),
3490 };
3491 if let Err(message) = crate::planning::execution_plan::validate_value_against_type(
3492 lemma_type.as_ref(),
3493 &lit.to_literal(),
3494 &resolved_types.unit_index,
3495 None,
3496 ) {
3497 errors.push(Error::validation_with_context(
3498 format!("Type '{}' suggestion is invalid: {}", type_name, message),
3499 Some(source.clone()),
3500 None::<String>,
3501 Some(spec),
3502 None,
3503 ));
3504 }
3505 }
3506 if let Some(default_kind) = resolved_types.declared_values.get(type_name.as_str()) {
3507 let lit = TypedLiteral {
3508 value: default_kind.value.clone(),
3509 lemma_type: Arc::clone(lemma_type),
3510 };
3511 if let Err(message) = crate::planning::execution_plan::validate_value_against_type(
3512 lemma_type.as_ref(),
3513 &lit.to_literal(),
3514 &resolved_types.unit_index,
3515 None,
3516 ) {
3517 errors.push(Error::validation_with_context(
3518 format!("Type '{}' with is invalid: {}", type_name, message),
3519 Some(source),
3520 None::<String>,
3521 Some(spec),
3522 None,
3523 ));
3524 }
3525 }
3526 }
3527
3528 if errors.is_empty() {
3529 Ok(resolved_types)
3530 } else {
3531 Err(errors)
3532 }
3533 }
3534
3535 fn resolve_list_types(
3541 &self,
3542 spec: &LemmaSpec,
3543 at: &EffectiveDate,
3544 already_resolved: &ResolvedTypesMap,
3545 list_type_names: &[String],
3546 resolved_types: &mut ResolvedSpecTypes,
3547 ) -> Result<(), Vec<Error>> {
3548 if list_type_names.is_empty() {
3549 return Ok(());
3550 }
3551 let data_defs = self
3552 .data_types
3553 .iter()
3554 .find(|(s, _)| discovery::same_loaded_spec(s, spec))
3555 .map(|(_, defs)| defs)
3556 .expect("BUG: list type names come from this spec's type definitions");
3557 let mut local = local_type_entries(resolved_types);
3558 let mut errors = Vec::new();
3559 for type_name in list_type_names {
3560 let ftd = data_defs
3561 .get(type_name.as_str())
3562 .expect("BUG: topo-sorted list type missing from registry");
3563 let (lemma_type, suggestion, value) =
3564 self.resolve_type_def(spec, type_name, ftd, at, already_resolved, &local)?;
3565 let to_error = |message: String| {
3566 Error::validation_with_context(
3567 message,
3568 Some(ftd.source.clone()),
3569 None::<String>,
3570 Some(spec),
3571 None,
3572 )
3573 };
3574 if let Some(raw) = &suggestion {
3575 resolved_types
3576 .source_defaults
3577 .insert(type_name.clone(), raw.clone());
3578 match bound_value_kind_from_raw_suggestion(
3579 raw.clone(),
3580 &lemma_type.specifications,
3581 type_name,
3582 ) {
3583 Ok(bound) => {
3584 resolved_types
3585 .declared_suggestions
3586 .insert(type_name.clone(), bound);
3587 }
3588 Err(message) => errors.push(to_error(message)),
3589 }
3590 }
3591 if let Some(raw) = &value {
3592 resolved_types
3593 .source_value_defaults
3594 .insert(type_name.clone(), raw.clone());
3595 match bound_value_kind_from_raw_suggestion(
3596 raw.clone(),
3597 &lemma_type.specifications,
3598 type_name,
3599 ) {
3600 Ok(bound) => {
3601 resolved_types
3602 .declared_values
3603 .insert(type_name.clone(), bound);
3604 }
3605 Err(message) => errors.push(to_error(message)),
3606 }
3607 }
3608 resolved_types
3609 .resolved
3610 .insert(type_name.clone(), Arc::clone(&lemma_type));
3611 local.insert(type_name.clone(), (lemma_type, suggestion, value));
3612 }
3613 if errors.is_empty() {
3614 Ok(())
3615 } else {
3616 Err(errors)
3617 }
3618 }
3619
3620 fn resolve_rule_list_types<'s>(
3623 &self,
3624 spec: &'s LemmaSpec,
3625 at: &EffectiveDate,
3626 already_resolved: &ResolvedTypesMap,
3627 resolved_types: &mut ResolvedSpecTypes,
3628 ) -> Result<(), Vec<Error>> {
3629 let mut written: Vec<(&'s ParentType, &'s Source)> = Vec::new();
3630 for rule in &spec.rules {
3631 let trees = std::iter::once(&rule.expression).chain(
3632 rule.unless_clauses
3633 .iter()
3634 .flat_map(|clause| [&clause.condition, &clause.result]),
3635 );
3636 for tree in trees {
3637 let mut pending = vec![tree.root()];
3638 while let Some(node) = pending.pop() {
3639 if let ast::ExpressionKind::ListType { element } = node.kind() {
3640 if written.iter().all(|(seen, _)| *seen != element) {
3641 let source = node
3642 .source()
3643 .expect("BUG: AST expression missing source location");
3644 written.push((element, source));
3645 }
3646 }
3647 node.push_children(&mut pending);
3648 }
3649 }
3650 }
3651 if written.is_empty() {
3652 return Ok(());
3653 }
3654 let local = local_type_entries(resolved_types);
3655 let mut errors = Vec::new();
3656 for (element, source) in written {
3657 let scope = ParentLookup {
3658 resolver: self,
3659 spec,
3660 source,
3661 at,
3662 already_resolved,
3663 resolved: &local,
3664 site: TypeUseSite::RuleListType,
3665 };
3666 match lookup_parent_type(&scope, element) {
3667 Ok((.., lemma_type)) => {
3668 resolved_types
3669 .rule_list_types
3670 .push((element.clone(), lemma_type));
3671 }
3672 Err(lookup_errors) => {
3673 if let Some(list_type) =
3674 self.rule_spec_element_list_type(spec, element, source, at)
3675 {
3676 resolved_types
3677 .rule_list_types
3678 .push((element.clone(), list_type));
3679 } else {
3680 errors.extend(lookup_errors);
3681 }
3682 }
3683 }
3684 }
3685 if errors.is_empty() {
3686 Ok(())
3687 } else {
3688 Err(errors)
3689 }
3690 }
3691
3692 fn resolve_type_def(
3694 &self,
3695 spec: &LemmaSpec,
3696 type_name: &str,
3697 ftd: &DataTypeDef,
3698 at: &EffectiveDate,
3699 already_resolved: &ResolvedTypesMap,
3700 resolved: &HashMap<String, ResolvedTypeEntry>,
3701 ) -> Result<ResolvedTypeEntry, Vec<Error>> {
3702 let parent = &ftd.parent;
3703 let constraints = &ftd.constraints;
3704
3705 let scope = ParentLookup {
3706 resolver: self,
3707 spec,
3708 source: &ftd.source,
3709 at,
3710 already_resolved,
3711 resolved,
3712 site: TypeUseSite::DataDefinition,
3713 };
3714 let (parent_specs, parent_suggestion, parent_value, parent_type) =
3715 lookup_parent_type(&scope, parent)?;
3716
3717 let mut declared_suggestion = parent_suggestion;
3718 let mut declared_value = parent_value;
3719 let mut final_type = (*parent_type).clone();
3720 final_type.specifications = parent_specs;
3721 if !should_defer_ranged_constraints(parent) {
3722 if let Some(constraints) = &constraints {
3723 let resolve_unit = UnitTokenResolver::Search(
3724 already_resolved
3725 .iter()
3726 .map(|(_, _, rts)| (&rts.unit_index, &rts.resolved))
3727 .collect(),
3728 );
3729 apply_constraints_to_spec(
3730 spec,
3731 &constraint_application_type_name(parent, type_name),
3732 type_name,
3733 &mut final_type,
3734 constraints,
3735 &ftd.source,
3736 ConstraintSuggestionSink {
3737 suggestion: &mut declared_suggestion,
3738 value: &mut declared_value,
3739 resolve_unit: Some(&resolve_unit),
3740 },
3741 )?;
3742 }
3743 }
3744 let final_specs = final_type.specifications;
3745 let final_options = final_type.options;
3746
3747 if matches!(&final_specs, TypeSpecification::List { .. }) && declared_value.is_none() {
3748 declared_value = Some(RawSuggestion::List(Vec::new()));
3749 }
3750
3751 let this_spec = SpecVersionKey::of(self.registered_repository(spec), spec);
3752 let family = match (element_parent_type(parent), parent_type.family()) {
3753 (ParentType::Primitive { .. }, _) | (_, None) => {
3754 TypeFamily::declared_in(&this_spec, type_name)
3755 }
3756 (_, Some(parent_family)) => parent_family.clone(),
3757 };
3758 let defining_spec = match element_parent_type(parent) {
3759 ParentType::Qualified { .. } => TypeDefiningSpec::Import,
3760 _ => TypeDefiningSpec::Local,
3761 };
3762 let extends = TypeExtends::Custom {
3763 parent: parent.to_string(),
3764 family,
3765 defining_spec,
3766 };
3767
3768 let lemma_type = Arc::new(LemmaType {
3769 name: Some(type_name.to_string()),
3770 specifications: final_specs,
3771 extends,
3772 options: final_options,
3773 });
3774
3775 let declared_suggestion = match &ftd.bound_literal {
3776 Some(literal) => {
3777 let resolve_unit = UnitTokenResolver::Search(
3778 already_resolved
3779 .iter()
3780 .map(|(_, _, rts)| (&rts.unit_index, &rts.resolved))
3781 .collect(),
3782 );
3783 match semantics::parser_value_to_value_kind(
3784 literal,
3785 lemma_type.as_ref(),
3786 Some(&resolve_unit),
3787 ) {
3788 Ok(value_kind) => Some(RawSuggestion::Value(value_kind)),
3789 Err(message) => {
3790 return Err(vec![Error::validation_with_context(
3791 message,
3792 Some(ftd.source.clone()),
3793 None::<String>,
3794 Some(spec),
3795 None,
3796 )]);
3797 }
3798 }
3799 }
3800 None => declared_suggestion,
3801 };
3802
3803 Ok((lemma_type, declared_suggestion, declared_value))
3804 }
3805
3806 fn resolve_types_internal(
3809 &self,
3810 spec: &LemmaSpec,
3811 at: &EffectiveDate,
3812 already_resolved: &ResolvedTypesMap,
3813 ) -> Result<(ResolvedSpecTypes, Vec<String>), Vec<Error>> {
3814 let data_defs = self
3815 .data_types
3816 .iter()
3817 .find(|(s, _)| discovery::same_loaded_spec(s, spec))
3818 .map(|(_, defs)| defs)
3819 .cloned()
3820 .unwrap_or_default();
3821
3822 let (scalar_names, list_names) = partition_list_type_names(&data_defs);
3823 let sorted_type_names = topo_sort_type_names(spec, &data_defs, scalar_names)?;
3824 let list_type_names = topo_sort_type_names(spec, &data_defs, list_names)?;
3825
3826 let mut resolved: HashMap<String, ResolvedTypeEntry> = HashMap::new();
3827
3828 for type_name in &sorted_type_names {
3829 let ftd = data_defs
3830 .get(type_name.as_str())
3831 .expect("BUG: topo-sorted type missing from registry");
3832 let entry =
3833 self.resolve_type_def(spec, type_name, ftd, at, already_resolved, &resolved)?;
3834 resolved.insert(type_name.clone(), entry);
3835 }
3836
3837 let mut unit_index = PlanningUnitIndex::new();
3838 let mut errors = Vec::new();
3839 for unit in Self::extract_units_from_type(&semantics::primitive_ratio_arc().specifications)
3840 {
3841 unit_index.insert_owner(unit, PlanningOwner::PrimitiveRatio);
3842 }
3843
3844 let locals: HashMap<String, Arc<LemmaType>> = resolved
3845 .iter()
3846 .map(|(name, (type_arc, _, _))| (name.clone(), Arc::clone(type_arc)))
3847 .collect();
3848
3849 for type_name in &sorted_type_names {
3850 let (type_arc, _, _) = resolved
3851 .get(type_name)
3852 .expect("BUG: type was resolved but missing from map");
3853 let data_type_def = data_defs
3854 .get(type_name.as_str())
3855 .expect("BUG: type was resolved but not in registry");
3856 let merge_result = if type_arc.is_measure() {
3857 if let ParentType::Qualified { spec_alias, inner } = &data_type_def.parent {
3858 match Self::parent_units_for_qualified_extension(
3859 self,
3860 spec,
3861 data_type_def,
3862 already_resolved,
3863 at,
3864 spec_alias,
3865 inner,
3866 ) {
3867 Ok(None) => Ok(()),
3868 Ok(Some(parent_units)) => Self::merge_additive_measure_units(
3869 spec,
3870 &mut unit_index,
3871 type_arc,
3872 data_type_def,
3873 &[],
3874 &parent_units,
3875 &locals,
3876 ),
3877 Err(error) => Err(error),
3878 }
3879 } else {
3880 Self::add_measure_units_to_index(
3881 spec,
3882 &mut unit_index,
3883 type_arc,
3884 data_type_def,
3885 &[],
3886 &locals,
3887 )
3888 }
3889 } else if type_arc.is_ratio() {
3890 Self::add_ratio_units_to_index(
3891 spec,
3892 &mut unit_index,
3893 type_arc,
3894 data_type_def,
3895 &[],
3896 &locals,
3897 )
3898 } else {
3899 Ok(())
3900 };
3901 if let Err(error) = merge_result {
3902 errors.push(error);
3903 }
3904 }
3905
3906 for data_row in &spec.data {
3907 let import_alias = data_row.reference.name.clone();
3908 let mut merge_imported_units =
3909 |import_repository: &Arc<LemmaRepository>,
3910 imported_spec: &LemmaSpec,
3911 imported_resolved: &ResolvedSpecTypes,
3912 prefix_scale_hop: bool| {
3913 let scale_hop = prefix_scale_hop.then(|| {
3918 let identity = SpecIdentity::of(
3919 import_repository,
3920 imported_spec,
3921 imported_spec.effective_from.clone(),
3922 );
3923 PathSegment::import(&import_alias, &identity)
3924 });
3925 for (bare, owner) in imported_resolved.unit_index.iter_owners() {
3926 let planning_owner = match owner {
3927 UnitOwner::PrimitiveRatio => PlanningOwner::PrimitiveRatio,
3928 UnitOwner::Named {
3929 import_path,
3930 type_name,
3931 lemma_type,
3932 } => {
3933 let mut path = vec![import_alias.clone()];
3934 path.extend(import_path.iter().cloned());
3935 let lemma_type = match &scale_hop {
3936 Some(hop) => Arc::new(lemma_type.with_scale_import_hop(hop)),
3937 None => Arc::clone(lemma_type),
3938 };
3939 PlanningOwner::Imported {
3940 import_path: path,
3941 type_name: type_name.clone(),
3942 lemma_type,
3943 }
3944 }
3945 };
3946 unit_index.insert_owner(bare.to_string(), planning_owner);
3947 }
3948 };
3949 match &data_row.value {
3950 ParsedDataValue::Import {
3951 spec_ref,
3952 effective_ref: Some(_),
3953 ..
3954 } => {
3955 let resolved = match discovery::resolve_spec_ref_repository(
3957 self.context,
3958 spec_ref,
3959 Some(&data_row.source_location),
3960 spec.name.as_str(),
3961 Some(spec),
3962 ) {
3963 Ok(resolved) => resolved,
3964 Err(error) => {
3965 errors.push(error);
3966 continue;
3967 }
3968 };
3969 let repository = match &resolved.repository {
3970 Some(explicit) => Arc::clone(explicit),
3971 None => Arc::clone(self.registered_repository(spec)),
3972 };
3973 let dep_name =
3974 crate::parsing::ast::ascii_lowercase_logical_name(spec_ref.name.clone());
3975 for (repo, imported, imported_resolved) in already_resolved {
3976 if !Arc::ptr_eq(repo, &repository) {
3977 continue;
3978 }
3979 if crate::parsing::ast::ascii_lowercase_logical_name(imported.name.clone())
3980 != dep_name
3981 {
3982 continue;
3983 }
3984 if discovery::same_loaded_spec(imported, spec) {
3985 continue;
3986 }
3987 merge_imported_units(repo, imported, imported_resolved, true);
3988 }
3989 }
3990 ParsedDataValue::Import { .. } => {
3991 let Some(spec_ref) = data_row.value.uses_spec_ref() else {
3992 continue;
3993 };
3994 let (import_repository, imported_spec) = match self.resolve_spec_for_import(
3995 spec,
3996 spec_ref,
3997 &data_row.source_location,
3998 at,
3999 ) {
4000 Ok(pair) => pair,
4001 Err(error) => {
4002 errors.push(error);
4003 continue;
4004 }
4005 };
4006 if discovery::same_loaded_spec(imported_spec, spec) {
4007 continue;
4008 }
4009 let Some(imported_resolved) = already_resolved
4010 .iter()
4011 .find(|(_, imported, _)| {
4012 discovery::same_loaded_spec(imported, imported_spec)
4013 })
4014 .map(|(_, _, resolved_spec_types)| resolved_spec_types)
4015 else {
4016 continue;
4017 };
4018 merge_imported_units(
4019 &import_repository,
4020 imported_spec,
4021 imported_resolved,
4022 true,
4023 );
4024 }
4025 ParsedDataValue::Definition { .. } => {
4026 let Some(spec_ref) = discovery::data_instance_list_spec_ref(spec, data_row)
4027 else {
4028 continue;
4029 };
4030 let ParsedDataValue::Definition {
4031 base: Some(parent), ..
4032 } = &data_row.value
4033 else {
4034 continue;
4035 };
4036 if Self::list_of_spec_name(spec, parent).is_none() {
4037 continue;
4038 }
4039 let (import_repository, imported_spec) = match self
4040 .resolve_list_spec_for_import(
4041 spec,
4042 &spec_ref,
4043 &data_row.source_location,
4044 at,
4045 ) {
4046 Ok(pair) => pair,
4047 Err(error) => {
4048 errors.push(error);
4049 continue;
4050 }
4051 };
4052 if discovery::same_loaded_spec(imported_spec, spec) {
4053 continue;
4054 }
4055 let Some(imported_resolved) = already_resolved
4056 .iter()
4057 .find(|(_, imported, _)| {
4058 discovery::same_loaded_spec(imported, imported_spec)
4059 })
4060 .map(|(_, _, resolved_spec_types)| resolved_spec_types)
4061 else {
4062 continue;
4063 };
4064 merge_imported_units(
4065 &import_repository,
4066 imported_spec,
4067 imported_resolved,
4068 false,
4069 );
4070 }
4071 ParsedDataValue::ImportList { .. } => {
4072 panic!(
4073 "BUG: uses list is rejected at parse and must not reach type resolution"
4074 );
4075 }
4076 }
4077 }
4078
4079 if !errors.is_empty() {
4080 return Err(errors);
4081 }
4082
4083 let mut raw_suggestions = Vec::new();
4084 let mut raw_values = Vec::new();
4085 let mut resolved_types = IndexMap::new();
4086 for type_name in &sorted_type_names {
4087 let (lemma_type, default, fill) = resolved
4088 .remove(type_name)
4089 .expect("BUG: every sorted type name must exist in resolved");
4090 if let Some(raw_suggestion) = default {
4091 raw_suggestions.push((type_name.clone(), raw_suggestion));
4092 }
4093 if let Some(raw_value) = fill {
4094 raw_values.push((type_name.clone(), raw_value));
4095 }
4096 resolved_types.insert(type_name.clone(), lemma_type);
4097 }
4098 if !resolved.is_empty() {
4099 panic!(
4100 "BUG: resolved types remain after draining sorted_type_names: {:?}",
4101 resolved.keys().collect::<Vec<_>>()
4102 );
4103 }
4104
4105 Ok((
4106 ResolvedSpecTypes {
4107 resolved: resolved_types,
4108 declared_suggestions: IndexMap::new(),
4109 declared_values: IndexMap::new(),
4110 raw_suggestions,
4111 raw_values,
4112 source_defaults: IndexMap::new(),
4113 source_value_defaults: IndexMap::new(),
4114 rule_list_types: Vec::new(),
4115 unit_index: UnitIndex::new(),
4116 planning_unit_index: unit_index,
4117 signature_index: crate::computation::arithmetic::SignatureIndex::new(),
4118 deferred_signature_collisions: Vec::new(),
4119 },
4120 list_type_names,
4121 ))
4122 }
4123
4124 fn resolve_spec_for_import(
4125 &self,
4126 spec: &LemmaSpec,
4127 from: &crate::parsing::ast::SpecRef,
4128 import_site: &crate::parsing::source::Source,
4129 at: &EffectiveDate,
4130 ) -> Result<(Arc<LemmaRepository>, &'a LemmaSpec), Error> {
4131 discovery::resolve_spec_ref(
4132 self.context,
4133 from,
4134 self.registered_repository(spec),
4135 spec,
4136 at,
4137 Some(import_site.clone()),
4138 )
4139 }
4140
4141 fn resolve_list_spec_for_import(
4143 &self,
4144 spec: &LemmaSpec,
4145 from: &crate::parsing::ast::SpecRef,
4146 import_site: &crate::parsing::source::Source,
4147 at: &EffectiveDate,
4148 ) -> Result<(Arc<LemmaRepository>, &'a LemmaSpec), Error> {
4149 discovery::resolve_spec_ref_or_self(
4150 self.context,
4151 from,
4152 self.registered_repository(spec),
4153 spec,
4154 at,
4155 Some(import_site.clone()),
4156 )
4157 }
4158
4159 fn add_measure_units_to_index(
4164 spec: &LemmaSpec,
4165 unit_index: &mut PlanningUnitIndex,
4166 resolved_type: &Arc<LemmaType>,
4167 defined_by: &DataTypeDef,
4168 import_path: &[String],
4169 locals: &HashMap<String, Arc<LemmaType>>,
4170 ) -> Result<(), Error> {
4171 if matches!(defined_by.parent, ParentType::Qualified { .. }) {
4172 unreachable!(
4173 "BUG: Qualified parents must use parent_units_for_qualified_extension + merge_additive_measure_units"
4174 );
4175 }
4176 let measure_family = resolved_type
4177 .measure_family_name()
4178 .expect("BUG: add_measure_units_to_index requires measure type with family");
4179 for unit in Self::extract_units_from_type(&resolved_type.specifications) {
4180 unit_index
4181 .merge_measure_unit(
4182 unit,
4183 resolved_type,
4184 &defined_by.name,
4185 import_path,
4186 measure_family,
4187 locals,
4188 )
4189 .map_err(|conflict| {
4190 Self::unit_merge_conflict_to_error(conflict, defined_by, spec)
4191 })?;
4192 }
4193 Ok(())
4194 }
4195
4196 fn parent_units_for_qualified_extension(
4198 resolver: &TypeResolver<'_>,
4199 spec: &LemmaSpec,
4200 defined_by: &DataTypeDef,
4201 already_resolved: &ResolvedTypesMap<'_>,
4202 at: &EffectiveDate,
4203 spec_alias: &str,
4204 inner: &ParentType,
4205 ) -> Result<Option<BTreeSet<String>>, Error> {
4206 let ParentType::Custom {
4207 name: parent_type_name,
4208 ..
4209 } = inner
4210 else {
4211 return Ok(None);
4212 };
4213 let Some(spec_ref) = spec.data.iter().find_map(|row| {
4214 if row.reference.segments.is_empty() && row.reference.name == spec_alias {
4215 row.value.uses_spec_ref()
4216 } else {
4217 None
4218 }
4219 }) else {
4220 return Err(Error::validation_with_context(
4221 format!("'{spec_alias}' is not a `uses` import alias in this spec"),
4222 Some(defined_by.source.clone()),
4223 Some(format!(
4224 "Add `uses {spec_alias}: <target_spec>`, then write `{spec_alias}.<type>`."
4225 )),
4226 Some(spec),
4227 None,
4228 ));
4229 };
4230 let (_, target_spec) =
4231 resolver.resolve_spec_for_import(spec, spec_ref, &defined_by.source, at)?;
4232 let Some(imported_resolved) = already_resolved
4233 .iter()
4234 .find(|(_, imported, _)| discovery::same_loaded_spec(imported, target_spec))
4235 .map(|(_, _, types)| types)
4236 else {
4237 return Ok(None);
4238 };
4239 let Some(parent_type) = imported_resolved.resolved.get(parent_type_name.as_str()) else {
4240 return Ok(None);
4241 };
4242 Ok(Some(
4243 Self::extract_units_from_type(&parent_type.specifications)
4244 .into_iter()
4245 .collect(),
4246 ))
4247 }
4248
4249 fn merge_additive_measure_units(
4251 spec: &LemmaSpec,
4252 unit_index: &mut PlanningUnitIndex,
4253 resolved_type: &Arc<LemmaType>,
4254 defined_by: &DataTypeDef,
4255 import_path: &[String],
4256 parent_units: &BTreeSet<String>,
4257 locals: &HashMap<String, Arc<LemmaType>>,
4258 ) -> Result<(), Error> {
4259 let measure_family = resolved_type
4260 .measure_family_name()
4261 .expect("BUG: additive measure registration requires family");
4262 for unit in Self::extract_units_from_type(&resolved_type.specifications) {
4263 if parent_units.contains(&unit) {
4264 continue;
4265 }
4266 unit_index
4267 .merge_measure_unit(
4268 unit,
4269 resolved_type,
4270 &defined_by.name,
4271 import_path,
4272 measure_family,
4273 locals,
4274 )
4275 .map_err(|conflict| {
4276 Self::unit_merge_conflict_to_error(conflict, defined_by, spec)
4277 })?;
4278 }
4279 Ok(())
4280 }
4281
4282 fn add_ratio_units_to_index(
4283 spec: &LemmaSpec,
4284 unit_index: &mut PlanningUnitIndex,
4285 resolved_type: &Arc<LemmaType>,
4286 defined_by: &DataTypeDef,
4287 import_path: &[String],
4288 locals: &HashMap<String, Arc<LemmaType>>,
4289 ) -> Result<(), Error> {
4290 for unit in Self::extract_units_from_type(&resolved_type.specifications) {
4291 unit_index
4292 .merge_ratio_unit(unit, resolved_type, &defined_by.name, import_path, locals)
4293 .map_err(|conflict| {
4294 Self::unit_merge_conflict_to_error(conflict, defined_by, spec)
4295 })?;
4296 }
4297 Ok(())
4298 }
4299
4300 fn unit_merge_conflict_to_error(
4301 conflict: UnitMergeConflict,
4302 defined_by: &DataTypeDef,
4303 spec: &LemmaSpec,
4304 ) -> Error {
4305 let message = match conflict {
4306 UnitMergeConflict::ConflictingFactors { unit, family } => format!(
4307 "Unit '{unit}' has different values on related measure types in '{family}'. \
4308 Use the same unit value on each type, or use different unit names."
4309 ),
4310 };
4311 Error::validation_with_context(
4312 message,
4313 Some(defined_by.source.clone()),
4314 None::<String>,
4315 Some(spec),
4316 None,
4317 )
4318 }
4319
4320 fn extract_units_from_type(specs: &TypeSpecification) -> Vec<String> {
4321 match specs {
4322 TypeSpecification::Measure { units, .. } => {
4323 units.iter().map(|unit| unit.name.clone()).collect()
4324 }
4325 TypeSpecification::Ratio { units, .. } => {
4326 units.iter().map(|unit| unit.name.clone()).collect()
4327 }
4328 _ => Vec::new(),
4329 }
4330 }
4331}
4332
4333pub(crate) fn instance_list_option_errors(
4338 lemma_type: &LemmaType,
4339 raw: &RawSuggestion,
4340 source: &Source,
4341 spec: &LemmaSpec,
4342) -> Vec<Error> {
4343 let owner = match &lemma_type.specifications {
4344 TypeSpecification::List { element, .. } => element.as_ref(),
4345 _ => lemma_type,
4346 };
4347 let mut values = Vec::new();
4348 let mut errors = Vec::new();
4349 if let Err(message) = collect_raw_values(raw, &mut values) {
4350 errors.push(Error::validation_with_context(
4351 message,
4352 Some(source.clone()),
4353 None::<String>,
4354 Some(spec),
4355 None,
4356 ));
4357 return errors;
4358 }
4359 for value in values {
4360 if let Err(message) = owner.reject_disallowed(&value, None) {
4361 errors.push(Error::validation_with_context(
4362 message,
4363 Some(source.clone()),
4364 None::<String>,
4365 Some(spec),
4366 None,
4367 ));
4368 }
4369 }
4370 errors
4371}
4372
4373fn collect_raw_values(raw: &RawSuggestion, out: &mut Vec<ValueKind>) -> Result<(), String> {
4374 let mut pending = vec![raw];
4375 while let Some(current) = pending.pop() {
4376 match current {
4377 RawSuggestion::Value(value) => out.push(value.clone()),
4378 RawSuggestion::List(rows) => {
4379 for row in rows.iter().rev() {
4380 pending.push(row);
4381 }
4382 }
4383 RawSuggestion::Measure { .. } | RawSuggestion::UnitBound { .. } => {
4384 return Err(
4385 "an instance-list option row is a value, not a measure bound still awaiting a unit"
4386 .to_string(),
4387 );
4388 }
4389 }
4390 }
4391 Ok(())
4392}
4393
4394pub(crate) fn validate_type_options(
4395 lemma_type: &LemmaType,
4396 declared_default: Option<&BoundValueKind>,
4397 default_label: &str,
4398 type_name: &str,
4399 source: &Source,
4400 spec_context: Option<&LemmaSpec>,
4401 unit_index: &UnitIndex,
4402) -> Vec<Error> {
4403 let owner = match &lemma_type.specifications {
4404 TypeSpecification::List { element, .. } if !element.options.is_empty() => element.as_ref(),
4405 _ => lemma_type,
4406 };
4407 let mut errors = Vec::new();
4408 let mut bare = owner.clone();
4409 bare.options = TypeOptions::default();
4410 for option in &owner.options.values {
4411 let literal = LiteralValue {
4412 value: option.value.value.clone(),
4413 };
4414 if let Err(message) =
4415 execution_plan::validate_value_against_type(&bare, &literal, unit_index, None)
4416 {
4417 errors.push(Error::validation_with_context(
4418 format!(
4419 "Type '{type_name}' option '{}': {message}",
4420 owner.display_bound(&option.value)
4421 ),
4422 Some(source.clone()),
4423 None::<String>,
4424 spec_context,
4425 None,
4426 ));
4427 }
4428 }
4429 if let Some(default) = declared_default {
4430 if let Err(message) = execution_plan::validate_value_against_type(
4431 lemma_type,
4432 &default.to_literal(),
4433 unit_index,
4434 None,
4435 ) {
4436 errors.push(Error::validation_with_context(
4437 format!("Type '{type_name}' {default_label} value {message}"),
4438 Some(source.clone()),
4439 None::<String>,
4440 spec_context,
4441 None,
4442 ));
4443 }
4444 }
4445 errors
4446}
4447
4448pub fn validate_type_specifications(
4459 specs: &TypeSpecification,
4460 declared_default: Option<&BoundValueKind>,
4461 default_label: &str,
4462 type_name: &str,
4463 source: &Source,
4464 spec_context: Option<&LemmaSpec>,
4465 unit_index: &UnitIndex,
4466) -> Vec<Error> {
4467 let mut errors = Vec::new();
4468 let declared_default = declared_default.map(|bound| &bound.value);
4469
4470 match specs {
4471 TypeSpecification::Measure {
4472 minimum,
4473 maximum,
4474 units,
4475 ..
4476 } => {
4477 if let (Some(min), Some(max)) = (minimum, maximum) {
4479 match (
4480 semantics::measure_declared_bound_to_canonical(
4481 &min.0, &min.1, units, type_name, "minimum",
4482 ),
4483 semantics::measure_declared_bound_to_canonical(
4484 &max.0, &max.1, units, type_name, "maximum",
4485 ),
4486 ) {
4487 (Ok(min_canonical), Ok(max_canonical)) => {
4488 match min_canonical.try_cmp(&max_canonical) {
4489 Ok(std::cmp::Ordering::Greater) => {
4490 errors.push(Error::validation_with_context(
4491 format!(
4492 "Type '{}' has invalid range: minimum {} {} is greater than maximum {} {}",
4493 type_name,
4494 min.0,
4495 min.1,
4496 max.0,
4497 max.1
4498 ),
4499 Some(source.clone()),
4500 None::<String>,
4501 spec_context,
4502 None,
4503 ));
4504 }
4505 Ok(std::cmp::Ordering::Less | std::cmp::Ordering::Equal) => {}
4506 Err(failure) => {
4507 errors.push(Error::validation_with_context(
4508 format!(
4509 "Type '{}' has invalid measure bound: range compare failed: {failure}",
4510 type_name
4511 ),
4512 Some(source.clone()),
4513 None::<String>,
4514 spec_context,
4515 None,
4516 ));
4517 }
4518 }
4519 }
4520 (Err(message), _) | (_, Err(message)) => {
4521 errors.push(Error::validation_with_context(
4522 format!(
4523 "Type '{}' has invalid measure bound: {}",
4524 type_name, message
4525 ),
4526 Some(source.clone()),
4527 None::<String>,
4528 spec_context,
4529 None,
4530 ));
4531 }
4532 }
4533 }
4534
4535 if units.is_empty() {
4539 errors.push(Error::validation_with_context(
4540 format!(
4541 "Type '{}' is a measure type but has no units. Measure types must define at least one unit (e.g. -> unit eur: 1).",
4542 type_name
4543 ),
4544 Some(source.clone()),
4545 None::<String>,
4546 spec_context,
4547 None,
4548 ));
4549 }
4550
4551 if !units.is_empty() {
4553 let mut seen_names: Vec<String> = Vec::new();
4554 for unit in units.iter() {
4555 if unit.name.trim().is_empty() {
4557 errors.push(Error::validation_with_context(
4558 format!(
4559 "Type '{}' has a unit with empty name. Unit names cannot be empty.",
4560 type_name
4561 ),
4562 Some(source.clone()),
4563 None::<String>,
4564 spec_context,
4565 None,
4566 ));
4567 }
4568
4569 if seen_names.contains(&unit.name) {
4571 errors.push(Error::validation_with_context(
4572 format!("Type '{}' has duplicate unit name '{}'. Unit names must be unique within a type.", type_name, unit.name),
4573 Some(source.clone()),
4574 None::<String>,
4575 spec_context,
4576 None,
4577 ));
4578 } else {
4579 seen_names.push(unit.name.clone());
4580 }
4581
4582 if !unit.is_positive_factor() {
4583 let factor = unit.require_literal_factor();
4584 errors.push(Error::validation_with_context(
4585 format!("Type '{}' has unit '{}' with invalid value {}/{}. Unit values must be positive (conversion factor relative to type base).", type_name, unit.name, factor.numer_to_string(), factor.denom_to_string()),
4586 Some(source.clone()),
4587 None::<String>,
4588 spec_context,
4589 None,
4590 ));
4591 }
4592 }
4593 }
4594 }
4595 TypeSpecification::Number {
4596 minimum,
4597 maximum,
4598 decimals,
4599 ..
4600 } => {
4601 if let (Some(min), Some(max)) = (minimum, maximum) {
4603 match min.try_cmp(max) {
4604 Ok(std::cmp::Ordering::Greater) => {
4605 errors.push(Error::validation_with_context(
4606 format!(
4607 "Type '{}' has invalid range: minimum {} is greater than maximum {}",
4608 type_name, min, max
4609 ),
4610 Some(source.clone()),
4611 None::<String>,
4612 spec_context,
4613 None,
4614 ));
4615 }
4616 Ok(std::cmp::Ordering::Less | std::cmp::Ordering::Equal) => {}
4617 Err(failure) => {
4618 errors.push(Error::validation_with_context(
4619 format!(
4620 "Type '{}' has invalid range: bound compare failed: {failure}",
4621 type_name
4622 ),
4623 Some(source.clone()),
4624 None::<String>,
4625 spec_context,
4626 None,
4627 ));
4628 }
4629 }
4630 }
4631
4632 if let Some(d) = decimals {
4634 if *d > 28 {
4635 errors.push(Error::validation_with_context(
4636 format!(
4637 "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
4638 type_name, d
4639 ),
4640 Some(source.clone()),
4641 None::<String>,
4642 spec_context,
4643 None,
4644 ));
4645 }
4646 }
4647
4648 if let Some(ValueKind::Number(def)) = declared_default {
4649 if let Some(min) = minimum {
4650 match def.try_cmp(min) {
4651 Ok(std::cmp::Ordering::Less) => {
4652 errors.push(Error::validation_with_context(
4653 format!(
4654 "Type '{}' {default_label} value {} is less than minimum {}",
4655 type_name, def, min
4656 ),
4657 Some(source.clone()),
4658 None::<String>,
4659 spec_context,
4660 None,
4661 ));
4662 }
4663 Ok(std::cmp::Ordering::Equal | std::cmp::Ordering::Greater) => {}
4664 Err(failure) => {
4665 errors.push(Error::validation_with_context(
4666 format!(
4667 "Type '{}' {default_label} bound compare failed: {failure}",
4668 type_name
4669 ),
4670 Some(source.clone()),
4671 None::<String>,
4672 spec_context,
4673 None,
4674 ));
4675 }
4676 }
4677 }
4678 if let Some(max) = maximum {
4679 match def.try_cmp(max) {
4680 Ok(std::cmp::Ordering::Greater) => {
4681 errors.push(Error::validation_with_context(
4682 format!(
4683 "Type '{}' {default_label} value {} is greater than maximum {}",
4684 type_name, def, max
4685 ),
4686 Some(source.clone()),
4687 None::<String>,
4688 spec_context,
4689 None,
4690 ));
4691 }
4692 Ok(std::cmp::Ordering::Less | std::cmp::Ordering::Equal) => {}
4693 Err(failure) => {
4694 errors.push(Error::validation_with_context(
4695 format!(
4696 "Type '{}' {default_label} bound compare failed: {failure}",
4697 type_name
4698 ),
4699 Some(source.clone()),
4700 None::<String>,
4701 spec_context,
4702 None,
4703 ));
4704 }
4705 }
4706 }
4707 }
4708 }
4710
4711 TypeSpecification::Ratio {
4712 minimum,
4713 maximum,
4714 units,
4715 ..
4716 } => {
4717 if let (Some(min), Some(max)) = (minimum, maximum) {
4719 match min.try_cmp(max) {
4720 Ok(std::cmp::Ordering::Greater) => {
4721 errors.push(Error::validation_with_context(
4722 format!(
4723 "Type '{}' has invalid range: minimum {} is greater than maximum {}",
4724 type_name, min, max
4725 ),
4726 Some(source.clone()),
4727 None::<String>,
4728 spec_context,
4729 None,
4730 ));
4731 }
4732 Ok(std::cmp::Ordering::Less | std::cmp::Ordering::Equal) => {}
4733 Err(failure) => {
4734 errors.push(Error::validation_with_context(
4735 format!(
4736 "Type '{}' has invalid range: bound compare failed: {failure}",
4737 type_name
4738 ),
4739 Some(source.clone()),
4740 None::<String>,
4741 spec_context,
4742 None,
4743 ));
4744 }
4745 }
4746 }
4747
4748 if let Some(ValueKind::Ratio(def)) = declared_default {
4749 if let Some(min) = minimum {
4750 match def.try_cmp(min) {
4751 Ok(std::cmp::Ordering::Less) => {
4752 errors.push(Error::validation_with_context(
4753 format!(
4754 "Type '{}' {default_label} value {} is less than minimum {}",
4755 type_name, def, min
4756 ),
4757 Some(source.clone()),
4758 None::<String>,
4759 spec_context,
4760 None,
4761 ));
4762 }
4763 Ok(std::cmp::Ordering::Equal | std::cmp::Ordering::Greater) => {}
4764 Err(failure) => {
4765 errors.push(Error::validation_with_context(
4766 format!(
4767 "Type '{}' {default_label} bound compare failed: {failure}",
4768 type_name
4769 ),
4770 Some(source.clone()),
4771 None::<String>,
4772 spec_context,
4773 None,
4774 ));
4775 }
4776 }
4777 }
4778 if let Some(max) = maximum {
4779 match def.try_cmp(max) {
4780 Ok(std::cmp::Ordering::Greater) => {
4781 errors.push(Error::validation_with_context(
4782 format!(
4783 "Type '{}' {default_label} value {} is greater than maximum {}",
4784 type_name, def, max
4785 ),
4786 Some(source.clone()),
4787 None::<String>,
4788 spec_context,
4789 None,
4790 ));
4791 }
4792 Ok(std::cmp::Ordering::Less | std::cmp::Ordering::Equal) => {}
4793 Err(failure) => {
4794 errors.push(Error::validation_with_context(
4795 format!(
4796 "Type '{}' {default_label} bound compare failed: {failure}",
4797 type_name
4798 ),
4799 Some(source.clone()),
4800 None::<String>,
4801 spec_context,
4802 None,
4803 ));
4804 }
4805 }
4806 }
4807 }
4808
4809 if !units.is_empty() {
4813 let mut seen_names: Vec<String> = Vec::new();
4814 for unit in units.iter() {
4815 if unit.name.trim().is_empty() {
4817 errors.push(Error::validation_with_context(
4818 format!(
4819 "Type '{}' has a unit with empty name. Unit names cannot be empty.",
4820 type_name
4821 ),
4822 Some(source.clone()),
4823 None::<String>,
4824 spec_context,
4825 None,
4826 ));
4827 }
4828
4829 if seen_names.contains(&unit.name) {
4831 errors.push(Error::validation_with_context(
4832 format!("Type '{}' has duplicate unit name '{}'. Unit names must be unique within a type.", type_name, unit.name),
4833 Some(source.clone()),
4834 None::<String>,
4835 spec_context,
4836 None,
4837 ));
4838 } else {
4839 seen_names.push(unit.name.clone());
4840 }
4841
4842 if !unit.value.numer_is_positive() {
4843 errors.push(Error::validation_with_context(
4844 format!("Type '{}' has unit '{}' with invalid value {}/{}. Unit values must be positive (conversion factor relative to type base).", type_name, unit.name, unit.value.numer_to_string(), unit.value.denom_to_string()),
4845 Some(source.clone()),
4846 None::<String>,
4847 spec_context,
4848 None,
4849 ));
4850 }
4851 }
4852 }
4853 }
4854
4855 TypeSpecification::Text { length, .. } => {
4856 if let Some(ValueKind::Text(def)) = declared_default {
4857 let def_len = def.len();
4858
4859 if let Some(len) = length {
4860 if def_len != *len {
4861 errors.push(Error::validation_with_context(
4862 format!("Type '{}' {default_label} value length {} does not match required length {}", type_name, def_len, len),
4863 Some(source.clone()),
4864 None::<String>,
4865 spec_context,
4866 None,
4867 ));
4868 }
4869 }
4870 }
4871 }
4872
4873 TypeSpecification::Date {
4874 minimum, maximum, ..
4875 } => {
4876 if let (Some(min), Some(max)) = (minimum, maximum) {
4878 let min_sem = semantics::date_time_to_semantic(min);
4879 let max_sem = semantics::date_time_to_semantic(max);
4880 if semantics::compare_semantic_dates(&min_sem, &max_sem) == Ordering::Greater {
4881 errors.push(Error::validation_with_context(
4882 format!(
4883 "Type '{}' has invalid date range: minimum {} is after maximum {}",
4884 type_name, min, max
4885 ),
4886 Some(source.clone()),
4887 None::<String>,
4888 spec_context,
4889 None,
4890 ));
4891 }
4892 }
4893
4894 if let Some(ValueKind::Date(def)) = declared_default {
4895 if let Some(min) = minimum {
4896 let min_sem = semantics::date_time_to_semantic(min);
4897 if semantics::compare_semantic_dates(def, &min_sem) == Ordering::Less {
4898 errors.push(Error::validation_with_context(
4899 format!(
4900 "Type '{}' default date {} is before minimum {}",
4901 type_name, def, min
4902 ),
4903 Some(source.clone()),
4904 None::<String>,
4905 spec_context,
4906 None,
4907 ));
4908 }
4909 }
4910 if let Some(max) = maximum {
4911 let max_sem = semantics::date_time_to_semantic(max);
4912 if semantics::compare_semantic_dates(def, &max_sem) == Ordering::Greater {
4913 errors.push(Error::validation_with_context(
4914 format!(
4915 "Type '{}' default date {} is after maximum {}",
4916 type_name, def, max
4917 ),
4918 Some(source.clone()),
4919 None::<String>,
4920 spec_context,
4921 None,
4922 ));
4923 }
4924 }
4925 }
4926 }
4927
4928 TypeSpecification::Time {
4929 minimum, maximum, ..
4930 } => {
4931 if let (Some(min), Some(max)) = (minimum, maximum) {
4933 let min_sem = semantics::time_to_semantic(min);
4934 let max_sem = semantics::time_to_semantic(max);
4935 if semantics::compare_semantic_times(&min_sem, &max_sem) == Ordering::Greater {
4936 errors.push(Error::validation_with_context(
4937 format!(
4938 "Type '{}' has invalid time range: minimum {} is after maximum {}",
4939 type_name, min, max
4940 ),
4941 Some(source.clone()),
4942 None::<String>,
4943 spec_context,
4944 None,
4945 ));
4946 }
4947 }
4948
4949 if let Some(ValueKind::Time(def)) = declared_default {
4950 if let Some(min) = minimum {
4951 let min_sem = semantics::time_to_semantic(min);
4952 if semantics::compare_semantic_times(def, &min_sem) == Ordering::Less {
4953 errors.push(Error::validation_with_context(
4954 format!(
4955 "Type '{}' default time {} is before minimum {}",
4956 type_name, def, min
4957 ),
4958 Some(source.clone()),
4959 None::<String>,
4960 spec_context,
4961 None,
4962 ));
4963 }
4964 }
4965 if let Some(max) = maximum {
4966 let max_sem = semantics::time_to_semantic(max);
4967 if semantics::compare_semantic_times(def, &max_sem) == Ordering::Greater {
4968 errors.push(Error::validation_with_context(
4969 format!(
4970 "Type '{}' default time {} is after maximum {}",
4971 type_name, def, max
4972 ),
4973 Some(source.clone()),
4974 None::<String>,
4975 spec_context,
4976 None,
4977 ));
4978 }
4979 }
4980 }
4981 }
4982
4983 TypeSpecification::NumberRange { .. }
4984 | TypeSpecification::DateRange { .. }
4985 | TypeSpecification::TimeRange { .. }
4986 | TypeSpecification::MeasureRange { .. }
4987 | TypeSpecification::RatioRange { .. } => {
4988 if let Err(message) = semantics::check_range_bound_consistency(specs, unit_index) {
4989 errors.push(Error::validation_with_context(
4990 format!("Type '{}' has {message}", type_name),
4991 Some(source.clone()),
4992 None::<String>,
4993 spec_context,
4994 None,
4995 ));
4996 }
4997 }
4998 TypeSpecification::Boolean { .. } => {
4999 }
5001 TypeSpecification::Veto { .. } => {
5002 }
5005 TypeSpecification::Undetermined => unreachable!(
5006 "BUG: validate_type_specification_constraints called with Undetermined sentinel type; this type exists only during type inference"
5007 ),
5008 TypeSpecification::Spec { .. } | TypeSpecification::List { .. } => {}
5009 }
5010
5011 errors
5012}
5013
5014#[cfg(test)]
5015mod tests {
5016 use super::*;
5017
5018 mod type_resolution {
5019 use super::*;
5020 use crate::computation::rational::rational_new;
5021 use crate::parsing::ast::{
5022 CommandArg, LemmaSpec, ParentType, PrimitiveKind, TypeConstraintCommand,
5023 };
5024 use crate::parsing::parse;
5025 use crate::planning::typing::{find_unique_measure_type_in_unit_index, DecompositionMatch};
5026 use crate::ResourceLimits;
5027 use rust_decimal::Decimal;
5028 use std::sync::Arc;
5029
5030 fn test_context_and_effective(
5031 specs: &[&LemmaSpec],
5032 ) -> (&'static Context, &'static EffectiveDate) {
5033 use crate::engine::Context;
5034 let mut ctx = Context::new();
5035 let repository = ctx.workspace();
5036 for s in specs {
5037 ctx.insert_spec(Arc::clone(&repository), (*s).clone())
5038 .unwrap();
5039 }
5040 let ctx = Box::leak(Box::new(ctx));
5041 let eff = Box::leak(Box::new(EffectiveDate::Origin));
5042 (ctx, eff)
5043 }
5044
5045 fn context_with_test_spec() -> (&'static Context, &'static LemmaSpec) {
5046 use crate::engine::Context;
5047 let mut ctx = Context::new();
5048 let repository = ctx.workspace();
5049 let spec = LemmaSpec::new("test_spec".to_string());
5050 ctx.insert_spec(Arc::clone(&repository), spec).unwrap();
5051 let ctx: &'static Context = Box::leak(Box::new(ctx));
5052 let repository = ctx.workspace();
5053 let spec = ctx
5054 .spec_set(&repository, "test_spec")
5055 .and_then(|ss| ss.get_exact(None))
5056 .expect("inserted");
5057 (ctx, spec)
5058 }
5059
5060 fn resolver_for_code(code: &str) -> (TypeResolver<'static>, Vec<&'static LemmaSpec>) {
5061 use crate::engine::Context;
5062 let owned = parse(
5063 code,
5064 crate::parsing::source::SourceType::Volatile,
5065 &ResourceLimits::default(),
5066 )
5067 .unwrap()
5068 .into_flattened_specs();
5069 let mut ctx = Context::new();
5070 let repository = ctx.workspace();
5071 for s in owned {
5072 ctx.insert_spec(Arc::clone(&repository), s).unwrap();
5073 }
5074 let ctx = Box::leak(Box::new(ctx));
5075 let repository = ctx.workspace();
5076 let mut spec_refs: Vec<&'static LemmaSpec> = Vec::new();
5077 for ss in ctx.spec_sets_for(&repository) {
5078 for s in ss.iter_specs() {
5079 spec_refs.push(s);
5080 }
5081 }
5082 let mut resolver = TypeResolver::new(ctx, HashMap::new());
5083 for spec in &spec_refs {
5084 resolver.register_all(&repository, spec);
5085 }
5086 (resolver, spec_refs)
5087 }
5088
5089 fn resolver_single_spec(code: &str) -> (TypeResolver<'static>, &LemmaSpec) {
5090 let (resolver, spec_arcs) = resolver_for_code(code);
5091 let spec = spec_arcs.into_iter().next().expect("at least one spec");
5092 (resolver, spec)
5093 }
5094
5095 #[test]
5096 fn test_type_spec_for_primitive_covers_all_variants() {
5097 use crate::parsing::ast::PrimitiveKind;
5098 use crate::planning::semantics::type_spec_for_primitive;
5099
5100 for kind in [
5101 PrimitiveKind::Boolean,
5102 PrimitiveKind::Measure,
5103 PrimitiveKind::MeasureRange,
5104 PrimitiveKind::Number,
5105 PrimitiveKind::NumberRange,
5106 PrimitiveKind::Ratio,
5107 PrimitiveKind::RatioRange,
5108 PrimitiveKind::Text,
5109 PrimitiveKind::Date,
5110 PrimitiveKind::DateRange,
5111 PrimitiveKind::Time,
5112 PrimitiveKind::TimeRange,
5113 ] {
5114 let spec = type_spec_for_primitive(kind);
5115 assert!(
5116 !matches!(
5117 spec,
5118 crate::planning::semantics::TypeSpecification::Undetermined
5119 ),
5120 "type_spec_for_primitive({:?}) returned Undetermined",
5121 kind
5122 );
5123 }
5124 }
5125
5126 #[test]
5127 fn test_register_data_type_def() {
5128 let (ctx, spec) = context_with_test_spec();
5129 let mut resolver = TypeResolver::new(ctx, HashMap::new());
5130 let ftd = DataTypeDef {
5131 parent: ParentType::Primitive {
5132 primitive: PrimitiveKind::Number,
5133 },
5134 constraints: Some(vec![
5135 Constraint::new(
5136 TypeConstraintCommand::Minimum,
5137 vec![CommandArg::Literal(crate::literals::Value::Number(
5138 Decimal::ZERO,
5139 ))],
5140 crate::parsing::source::Source::new(
5141 crate::parsing::source::SourceType::Volatile,
5142 crate::parsing::ast::Span {
5143 start: 0,
5144 end: 0,
5145 line: 1,
5146 col: 0,
5147 },
5148 ),
5149 ),
5150 Constraint::new(
5151 TypeConstraintCommand::Maximum,
5152 vec![CommandArg::Literal(crate::literals::Value::Number(
5153 Decimal::from(150),
5154 ))],
5155 crate::parsing::source::Source::new(
5156 crate::parsing::source::SourceType::Volatile,
5157 crate::parsing::ast::Span {
5158 start: 0,
5159 end: 0,
5160 line: 1,
5161 col: 0,
5162 },
5163 ),
5164 ),
5165 ]),
5166 source: crate::parsing::source::Source::new(
5167 crate::parsing::source::SourceType::Volatile,
5168 crate::parsing::ast::Span {
5169 start: 0,
5170 end: 0,
5171 line: 1,
5172 col: 0,
5173 },
5174 ),
5175 name: "age".to_string(),
5176 bound_literal: None,
5177 };
5178
5179 let result = resolver.register_type(&ctx.workspace(), spec, ftd);
5180 assert!(result.is_ok());
5181 let resolved = resolver
5182 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5183 .unwrap();
5184 assert!(resolved.resolved.contains_key("age"));
5185 }
5186
5187 #[test]
5188 fn test_register_duplicate_type_fails() {
5189 let (ctx, spec) = context_with_test_spec();
5190 let mut resolver = TypeResolver::new(ctx, HashMap::new());
5191 let ftd = DataTypeDef {
5192 parent: ParentType::Primitive {
5193 primitive: PrimitiveKind::Number,
5194 },
5195 constraints: None,
5196 source: crate::parsing::source::Source::new(
5197 crate::parsing::source::SourceType::Volatile,
5198 crate::parsing::ast::Span {
5199 start: 0,
5200 end: 0,
5201 line: 1,
5202 col: 0,
5203 },
5204 ),
5205 name: "money".to_string(),
5206 bound_literal: None,
5207 };
5208 let repository = ctx.workspace();
5209 resolver
5210 .register_type(&repository, spec, ftd.clone())
5211 .unwrap();
5212 let result = resolver.register_type(&repository, spec, ftd);
5213 assert!(result.is_err());
5214 }
5215
5216 #[test]
5217 fn test_resolve_custom_type_from_primitive() {
5218 let (ctx, spec) = context_with_test_spec();
5219 let mut resolver = TypeResolver::new(ctx, HashMap::new());
5220 let ftd = DataTypeDef {
5221 parent: ParentType::Primitive {
5222 primitive: PrimitiveKind::Number,
5223 },
5224 constraints: None,
5225 source: crate::parsing::source::Source::new(
5226 crate::parsing::source::SourceType::Volatile,
5227 crate::parsing::ast::Span {
5228 start: 0,
5229 end: 0,
5230 line: 1,
5231 col: 0,
5232 },
5233 ),
5234 name: "money".to_string(),
5235 bound_literal: None,
5236 };
5237
5238 resolver.register_type(&ctx.workspace(), spec, ftd).unwrap();
5239 let resolved = resolver
5240 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5241 .unwrap();
5242
5243 assert!(resolved.resolved.contains_key("money"));
5244 let money_type = resolved.resolved.get("money").unwrap();
5245 assert_eq!(money_type.name, Some("money".to_string()));
5246 }
5247
5248 #[test]
5249 fn test_child_measure_type_keeps_declared_name_and_child_units() {
5250 let (resolver, spec) = resolver_single_spec(
5251 r#"spec test
5252 data length: measure
5253 -> unit meter: 1
5254 data road_length: length
5255 -> unit kilometer: 1000"#,
5256 );
5257
5258 let resolved_types = resolver
5259 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5260 .unwrap();
5261
5262 let road_length_type = resolved_types.resolved.get("road_length").unwrap();
5263 assert_eq!(road_length_type.name.as_deref(), Some("road_length"));
5264
5265 match &road_length_type.specifications {
5266 TypeSpecification::Measure { units, .. } => {
5267 assert!(units.iter().any(|unit| unit.name == "kilometer"));
5268 }
5269 _ => panic!("Expected Measure type specifications"),
5270 }
5271
5272 let kilometer_owner = resolved_types.unit_index.unique_owner("kilometer").unwrap();
5273 assert_eq!(kilometer_owner.name.as_deref(), Some("road_length"));
5274 assert!(
5275 Arc::ptr_eq(
5276 kilometer_owner,
5277 resolved_types.resolved.get("road_length").unwrap()
5278 ),
5279 "bound kilometer owner must be the resolved road_length Arc"
5280 );
5281 }
5282
5283 #[test]
5284 fn test_type_definition_resolution() {
5285 let (resolver, spec) = resolver_single_spec(
5286 r#"spec test
5287 data dice: number -> minimum 0 -> maximum 6"#,
5288 );
5289
5290 let resolved_types = resolver
5291 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5292 .unwrap();
5293 let dice_type = resolved_types.resolved.get("dice").unwrap();
5294
5295 match &dice_type.specifications {
5296 TypeSpecification::Number {
5297 minimum, maximum, ..
5298 } => {
5299 assert_eq!(minimum, &Some(rational_new(0, 1)));
5300 assert_eq!(maximum, &Some(rational_new(6, 1)));
5301 }
5302 _ => panic!("Expected Number type specifications"),
5303 }
5304 }
5305
5306 #[test]
5307 fn test_type_definition_with_multiple_commands() {
5308 let (resolver, spec) = resolver_single_spec(
5309 r#"spec test
5310 data money: measure -> unit eur: 1.0 -> unit usd: 1.18 -> minimum 0 eur"#,
5311 );
5312
5313 let resolved_types = resolver
5314 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5315 .unwrap();
5316 let money_type = resolved_types.resolved.get("money").unwrap();
5317
5318 match &money_type.specifications {
5319 TypeSpecification::Measure { minimum, units, .. } => {
5320 assert_eq!(minimum.as_ref().map(|(_, unit)| unit.as_str()), Some("eur"));
5321 assert_eq!(units.len(), 2);
5322 assert!(units.iter().any(|u| u.name == "eur"));
5323 assert!(units.iter().any(|u| u.name == "usd"));
5324 }
5325 _ => panic!("Expected Measure type specifications"),
5326 }
5327 }
5328
5329 #[test]
5330 fn test_number_type_with_decimals() {
5331 let (resolver, spec) = resolver_single_spec(
5332 r#"spec test
5333 data price: number -> decimals 2 -> minimum 0"#,
5334 );
5335
5336 let resolved_types = resolver
5337 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5338 .unwrap();
5339 let price_type = resolved_types.resolved.get("price").unwrap();
5340
5341 match &price_type.specifications {
5342 TypeSpecification::Number {
5343 decimals, minimum, ..
5344 } => {
5345 assert_eq!(*decimals, Some(2));
5346 assert_eq!(minimum, &Some(rational_new(0, 1)));
5347 }
5348 _ => panic!("Expected Number type specifications with decimals"),
5349 }
5350 }
5351
5352 #[test]
5353 fn test_number_type_decimals_only() {
5354 let (resolver, spec) = resolver_single_spec(
5355 r#"spec test
5356 data precise_number: number -> decimals 4"#,
5357 );
5358
5359 let resolved_types = resolver
5360 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5361 .unwrap();
5362 let precise_type = resolved_types.resolved.get("precise_number").unwrap();
5363
5364 match &precise_type.specifications {
5365 TypeSpecification::Number { decimals, .. } => {
5366 assert_eq!(*decimals, Some(4));
5367 }
5368 _ => panic!("Expected Number type with decimals 4"),
5369 }
5370 }
5371
5372 #[test]
5373 fn test_measure_type_rejects_decimals() {
5374 let (resolver, spec) = resolver_single_spec(
5375 r#"spec test
5376 data weight: measure -> unit kg: 1 -> decimals 3"#,
5377 );
5378
5379 let errors = resolver
5380 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5381 .expect_err("decimals on a measure must be a planning error");
5382 let messages: Vec<String> = errors.iter().map(|e| e.to_string()).collect();
5383 assert!(
5384 messages.iter().any(|m| m.contains(
5385 "'decimals' is not valid on measure type 'weight': precision depends on the unit. Round in a rule instead, for example 'weight -> round 0.001 kg'."
5386 )),
5387 "got: {messages:?}"
5388 );
5389 }
5390
5391 #[test]
5392 fn test_ratio_type_rejects_decimals() {
5393 let (resolver, spec) = resolver_single_spec(
5394 r#"spec test
5395 data ratio_type: ratio -> decimals 2"#,
5396 );
5397
5398 let errors = resolver
5399 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5400 .expect_err("decimals on a ratio must be a planning error");
5401 let messages: Vec<String> = errors.iter().map(|e| e.to_string()).collect();
5402 assert!(
5403 messages.iter().any(|m| m.contains(
5404 "'decimals' is not valid on ratio type 'ratio_type': precision depends on the unit. Round in a rule instead, for example 'ratio_type -> round 0.01 percent'."
5405 )),
5406 "got: {messages:?}"
5407 );
5408 }
5409
5410 #[test]
5411 fn typedef_default_inherits_through_extension_chain() {
5412 let (resolver, spec) = resolver_single_spec(
5413 r#"spec test
5414 data money: measure -> unit eur: 1 -> suggest 4 eur
5415 data price: money
5416 data final_price: price"#,
5417 );
5418
5419 let resolved = resolver
5420 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5421 .expect("extension chain must resolve");
5422
5423 assert!(
5424 resolved.declared_suggestions.contains_key("money"),
5425 "base typedef default must convert to ValueKind"
5426 );
5427 assert!(
5428 resolved.declared_suggestions.contains_key("final_price"),
5429 "suggestion must inherit through extension chain (money → price → final_price)"
5430 );
5431 }
5432
5433 #[test]
5434 fn test_ratio_type_with_default_command() {
5435 let (resolver, spec) = resolver_single_spec(
5436 r#"spec test
5437 data percentage: ratio -> minimum 0% -> maximum 100% -> suggest 50%"#,
5438 );
5439
5440 let resolved_types = resolver
5441 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5442 .unwrap();
5443 let percentage_type = resolved_types.resolved.get("percentage").unwrap();
5444
5445 match &percentage_type.specifications {
5446 TypeSpecification::Ratio {
5447 minimum, maximum, ..
5448 } => {
5449 assert_eq!(
5450 *minimum,
5451 Some(rational_new(0, 1)),
5452 "ratio type should have minimum 0"
5453 );
5454 assert_eq!(
5455 *maximum,
5456 Some(rational_new(1, 1)),
5457 "ratio type should have maximum 1"
5458 );
5459 }
5460 _ => panic!("Expected Ratio type with minimum and maximum"),
5461 }
5462
5463 let declared = resolved_types
5464 .declared_suggestions
5465 .get("percentage")
5466 .expect("declared default must be tracked for percentage");
5467 match &declared.value {
5468 ValueKind::Ratio(v) => {
5469 assert_eq!(v, &rational_new(1, 2));
5470 }
5471 other => panic!("expected Ratio declared default, got {:?}", other),
5472 }
5473 }
5474
5475 #[test]
5476 fn test_measure_extension_chain_same_family_units_allowed() {
5477 let (resolver, spec) = resolver_single_spec(
5478 r#"spec test
5479 data money: measure -> unit eur: 1
5480 data money2: money -> unit usd: 1.24"#,
5481 );
5482
5483 let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
5484 assert!(
5485 result.is_ok(),
5486 "Measure extension chain should resolve: {:?}",
5487 result.err()
5488 );
5489
5490 let resolved = result.unwrap();
5491 assert!(
5492 resolved.unit_index.unique_owner("eur").is_some(),
5493 "eur should be in unit_index"
5494 );
5495 assert!(
5496 resolved.unit_index.unique_owner("usd").is_some(),
5497 "usd should be in unit_index"
5498 );
5499 let eur_type = resolved.unit_index.unique_owner("eur").unwrap();
5500 let usd_type = resolved.unit_index.unique_owner("usd").unwrap();
5501 assert_eq!(
5502 eur_type.name.as_deref(),
5503 Some("money"),
5504 "declarer money must own inherited eur, not extension"
5505 );
5506 assert_eq!(
5507 usd_type.name.as_deref(),
5508 Some("money2"),
5509 "usd defined on money2 should be owned by money2"
5510 );
5511 assert!(
5512 Arc::ptr_eq(eur_type, resolved.resolved.get("money").unwrap()),
5513 "bound eur owner must be the resolved money Arc"
5514 );
5515 assert!(
5516 Arc::ptr_eq(usd_type, resolved.resolved.get("money2").unwrap()),
5517 "bound usd owner must be the resolved money2 Arc"
5518 );
5519 }
5520
5521 #[test]
5522 fn test_invalid_parent_type_in_named_type_should_error() {
5523 let (resolver, spec) = resolver_single_spec(
5524 r#"spec test
5525 data invalid: nonexistent_type -> minimum 0"#,
5526 );
5527
5528 let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
5529 assert!(result.is_err(), "Should reject invalid parent type");
5530
5531 let errs = result.unwrap_err();
5532 assert!(!errs.is_empty(), "expected at least one error");
5533 let error_msg = errs[0].to_string();
5534 assert!(
5535 error_msg.contains("Unknown parent") && error_msg.contains("nonexistent_type"),
5536 "Error should mention unknown type. Got: {}",
5537 error_msg
5538 );
5539 }
5540
5541 #[test]
5542 fn test_invalid_primitive_type_name_should_error() {
5543 let (resolver, spec) = resolver_single_spec(
5544 r#"spec test
5545 data invalid: choice -> option "a""#,
5546 );
5547
5548 let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
5549 assert!(result.is_err(), "Should reject invalid type base 'choice'");
5550
5551 let errs = result.unwrap_err();
5552 assert!(!errs.is_empty(), "expected at least one error");
5553 let error_msg = errs[0].to_string();
5554 assert!(
5555 error_msg.contains("Unknown parent") && error_msg.contains("choice"),
5556 "Error should mention unknown type 'choice'. Got: {}",
5557 error_msg
5558 );
5559 }
5560
5561 #[test]
5562 fn extension_unit_factor_override_errors() {
5563 let (resolver, spec) = resolver_single_spec(
5564 r#"spec test
5565 data money: measure
5566 -> unit eur: 1
5567 data money2: money
5568 -> unit eur: 1.10"#,
5569 );
5570
5571 let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
5572 assert!(
5573 result.is_err(),
5574 "extension child must not override inherited unit factor"
5575 );
5576 let error_msg = result
5577 .unwrap_err()
5578 .iter()
5579 .map(ToString::to_string)
5580 .collect::<Vec<_>>()
5581 .join("; ");
5582 assert!(
5583 error_msg.contains("eur"),
5584 "error must name unit eur, got: {error_msg}"
5585 );
5586 assert!(
5587 error_msg.contains("inherited") || error_msg.contains("cannot change"),
5588 "error must reject inherited unit redefinition, got: {error_msg}"
5589 );
5590 }
5591
5592 #[test]
5593 fn inherited_unit_idempotent_redeclare_ok() {
5594 let (resolver, spec) = resolver_single_spec(
5595 r#"spec test
5596 data money: measure
5597 -> unit eur: 1
5598 data money2: money
5599 -> unit eur: 1"#,
5600 );
5601
5602 let resolved = resolver
5603 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5604 .expect("idempotent inherited unit redeclare must resolve");
5605 let money2 = resolved.resolved.get("money2").expect("money2");
5606 match &money2.specifications {
5607 TypeSpecification::Measure { units, .. } => {
5608 let eur = units.iter().find(|u| u.name == "eur").expect("eur");
5609 assert_eq!(
5610 eur.require_literal_factor().try_to_decimal().unwrap(),
5611 Decimal::ONE,
5612 "eur factor must remain 1"
5613 );
5614 }
5615 other => panic!("expected Measure, got {other:?}"),
5616 }
5617 }
5618
5619 #[test]
5620 fn extension_additive_unit_registers_in_unit_index() {
5621 let (resolver, spec) = resolver_single_spec(
5622 r#"spec test
5623 data money: measure
5624 -> unit eur: 1
5625 data money2: money
5626 -> unit usd: 1.24"#,
5627 );
5628
5629 let resolved = resolver
5630 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5631 .expect("additive extension unit must resolve");
5632 let money = resolved
5633 .resolved
5634 .get("money")
5635 .expect("money resolved")
5636 .clone();
5637 let money2 = resolved
5638 .resolved
5639 .get("money2")
5640 .expect("money2 resolved")
5641 .clone();
5642 assert!(
5643 resolved.unit_index.unique_owner("eur").is_some(),
5644 "eur must be in unit_index"
5645 );
5646 assert!(
5647 resolved.unit_index.unique_owner("usd").is_some(),
5648 "usd must be in unit_index"
5649 );
5650 let eur_owner = resolved.unit_index.unique_owner("eur").expect("eur owner");
5651 let usd_owner = resolved.unit_index.unique_owner("usd").expect("usd owner");
5652 assert_eq!(
5653 eur_owner.name.as_deref(),
5654 Some("money"),
5655 "declarer money must own eur, not extension money2"
5656 );
5657 assert_eq!(
5658 usd_owner.name.as_deref(),
5659 Some("money2"),
5660 "money2 must own new unit usd"
5661 );
5662 assert_eq!(eur_owner.as_ref(), money.as_ref());
5663 assert_eq!(usd_owner.as_ref(), money2.as_ref());
5664 }
5665
5666 #[test]
5667 fn unit_index_maps_family_root_not_binding_alias() {
5668 let code = r#"spec units
5669 data money: measure
5670 -> unit eur: 1
5671 data mass: measure
5672 -> unit kg: 1
5673 data price_per_weight: measure
5674 -> unit eur_per_kg: eur/kg
5675
5676 spec consumer
5677 uses u: units
5678 data starch_levy_per_kg: u.price_per_weight
5679 -> suggest 0.1 eur_per_kg
5680 data amortization_per_kg: u.price_per_weight
5681 -> suggest 0.2 eur_per_kg
5682 "#;
5683 let (resolver, spec_arcs) = resolver_for_code(code);
5684 let units_arc = spec_arcs
5685 .iter()
5686 .find(|spec| spec.name == "units")
5687 .cloned()
5688 .expect("units spec");
5689 let consumer_arc = spec_arcs
5690 .iter()
5691 .find(|spec| spec.name == "consumer")
5692 .cloned()
5693 .expect("consumer spec");
5694 let (context, _) = test_context_and_effective(&spec_arcs);
5695 let repository = context.workspace();
5696
5697 let mut already_resolved = ResolvedTypesMap::new();
5698 let units_types = resolver
5699 .resolve_and_validate(units_arc, &EffectiveDate::Origin, &already_resolved)
5700 .expect("units spec resolves");
5701 let units_price_per_weight = units_types
5702 .resolved
5703 .get("price_per_weight")
5704 .expect("units defines price_per_weight")
5705 .clone();
5706 already_resolved.push((Arc::clone(&repository), units_arc, units_types));
5707
5708 let consumer_types = resolver
5709 .resolve_and_validate(consumer_arc, &EffectiveDate::Origin, &already_resolved)
5710 .expect("consumer spec resolves");
5711
5712 let eur_per_kg_owner = consumer_types
5713 .unit_index
5714 .unique_owner("eur_per_kg")
5715 .expect("eur_per_kg in unit_index");
5716 assert_eq!(
5717 eur_per_kg_owner.name.as_deref(),
5718 Some("price_per_weight"),
5719 "unit must belong to family root, not binding alias row name"
5720 );
5721 assert_eq!(
5722 eur_per_kg_owner.measure_family_name(),
5723 Some("price_per_weight")
5724 );
5725 assert_eq!(eur_per_kg_owner.as_ref(), units_price_per_weight.as_ref());
5726
5727 let starch_alias = consumer_types
5728 .resolved
5729 .get("starch_levy_per_kg")
5730 .expect("alias row in resolved");
5731 assert!(
5732 !std::ptr::eq(eur_per_kg_owner.as_ref(), starch_alias.as_ref()),
5733 "unit_index must not store binding alias arc"
5734 );
5735 }
5736
5737 #[test]
5738 fn same_named_local_root_does_not_hide_imported_units() {
5739 let code = r#"spec std_units
5740 data mass: measure
5741 -> unit kilogram: 1
5742 -> unit kilograms: 1
5743 -> unit gram: 0.001
5744
5745 spec s
5746 uses u: std_units
5747 data mass: measure
5748 -> unit kg: 1
5749 -> unit tonne: 1000
5750 rule smoke: true
5751 "#;
5752 let (resolver, spec_arcs) = resolver_for_code(code);
5753 let std_units_arc = spec_arcs
5754 .iter()
5755 .find(|spec| spec.name == "std_units")
5756 .cloned()
5757 .expect("std_units spec");
5758 let consumer_arc = spec_arcs
5759 .iter()
5760 .find(|spec| spec.name == "s")
5761 .cloned()
5762 .expect("consumer spec");
5763 let (context, _) = test_context_and_effective(&spec_arcs);
5764 let repository = context.workspace();
5765
5766 let mut already_resolved = ResolvedTypesMap::new();
5767 let std_units_types = resolver
5768 .resolve_and_validate(std_units_arc, &EffectiveDate::Origin, &already_resolved)
5769 .expect("std_units resolves");
5770 already_resolved.push((Arc::clone(&repository), std_units_arc, std_units_types));
5771
5772 let consumer_types = resolver
5773 .resolve_and_validate(consumer_arc, &EffectiveDate::Origin, &already_resolved)
5774 .expect("consumer resolves");
5775
5776 let local_mass = consumer_types
5777 .resolved
5778 .get("mass")
5779 .expect("consumer defines mass")
5780 .clone();
5781
5782 assert!(
5783 consumer_types.unit_index.unique_owner("kg").is_some(),
5784 "local kg must be indexed"
5785 );
5786 assert!(
5787 consumer_types.unit_index.unique_owner("tonne").is_some(),
5788 "local tonne must be indexed"
5789 );
5790 for imported_unit in ["kilogram", "kilograms", "gram"] {
5791 let owner = consumer_types
5792 .unit_index
5793 .unique_owner(imported_unit)
5794 .unwrap_or_else(|| panic!("imported {imported_unit} must stay in scope"));
5795 assert_eq!(
5796 owner.measure_family().map(|family| family.spec.as_str()),
5797 Some("std_units"),
5798 "{imported_unit} must belong to std_units.mass, not the local mass"
5799 );
5800 }
5801
5802 let kg_owner = consumer_types
5803 .unit_index
5804 .unique_owner("kg")
5805 .expect("kg owner");
5806 let tonne_owner = consumer_types
5807 .unit_index
5808 .unique_owner("tonne")
5809 .expect("tonne owner");
5810 assert_eq!(kg_owner.as_ref(), local_mass.as_ref());
5811 assert_eq!(tonne_owner.as_ref(), local_mass.as_ref());
5812 }
5813
5814 #[test]
5815 fn same_family_same_unit_same_factor_is_idempotent() {
5816 let code = r#"spec units_a
5817 data duration: measure
5818 -> unit hour: 1
5819
5820 spec consumer
5821 uses a: units_a
5822 uses b: units_a
5823 rule smoke: true
5824 "#;
5825 let (resolver, spec_arcs) = resolver_for_code(code);
5826 let units_a_arc = spec_arcs
5827 .iter()
5828 .find(|spec| spec.name == "units_a")
5829 .cloned()
5830 .expect("units_a spec");
5831 let consumer_arc = spec_arcs
5832 .iter()
5833 .find(|spec| spec.name == "consumer")
5834 .cloned()
5835 .expect("consumer spec");
5836 let (context, _) = test_context_and_effective(&spec_arcs);
5837 let repository = context.workspace();
5838
5839 let mut already_resolved = ResolvedTypesMap::new();
5840 let units_a_types = resolver
5841 .resolve_and_validate(units_a_arc, &EffectiveDate::Origin, &already_resolved)
5842 .expect("units_a resolves");
5843 already_resolved.push((Arc::clone(&repository), units_a_arc, units_a_types));
5844
5845 let consumer_types = resolver
5846 .resolve_and_validate(consumer_arc, &EffectiveDate::Origin, &already_resolved)
5847 .expect("double import of same duration must be idempotent");
5848
5849 assert_eq!(
5850 consumer_types.unit_index.owners_for("hour").len(),
5851 2,
5852 "each import path keeps its hour owner"
5853 );
5854 consumer_types
5855 .unit_index
5856 .resolve("hour")
5857 .expect("bare hour collapses across identical import paths");
5858 }
5859
5860 #[test]
5861 fn different_version_families_keep_separate_hour_owners() {
5862 let code = r#"spec units 2024-01-01
5865 data duration: measure
5866 -> unit hour: 1
5867
5868 spec units 2025-01-01
5869 data duration: measure
5870 -> unit hour: 60
5871
5872 spec consumer
5873 uses a: units 2024-01-01
5874 uses b: units 2025-01-01
5875 rule smoke: true
5876 "#;
5877 let (resolver, spec_arcs) = resolver_for_code(code);
5878 let consumer_arc = spec_arcs
5879 .iter()
5880 .find(|spec| spec.name == "consumer")
5881 .cloned()
5882 .expect("consumer spec");
5883 let (context, _) = test_context_and_effective(&spec_arcs);
5884 let repository = context.workspace();
5885
5886 let mut already_resolved = ResolvedTypesMap::new();
5887 for units_arc in spec_arcs.iter().filter(|spec| spec.name == "units") {
5888 let units_types = resolver
5889 .resolve_and_validate(units_arc, &units_arc.effective_from, &already_resolved)
5890 .expect("units version resolves");
5891 already_resolved.push((Arc::clone(&repository), units_arc, units_types));
5892 }
5893
5894 let consumer_types = resolver
5895 .resolve_and_validate(consumer_arc, &EffectiveDate::Origin, &already_resolved)
5896 .expect("different version families resolve without merge conflict");
5897 assert_eq!(
5898 consumer_types.unit_index.owners_for("hour").len(),
5899 2,
5900 "each version keeps its own hour owner"
5901 );
5902 }
5903
5904 #[test]
5905 fn test_spec_level_same_unit_names_keep_every_declarer() {
5906 let (resolver, spec) = resolver_single_spec(
5907 r#"spec test
5908 data money_a: measure
5909 -> unit eur: 1.00
5910 -> unit usd: 0.84
5911
5912 data money_b: measure
5913 -> unit eur: 1.00
5914 -> unit usd: 1.20
5915
5916 data length_a: measure
5917 -> unit meter: 1.0
5918
5919 data length_b: measure
5920 -> unit meter: 1.0"#,
5921 );
5922
5923 let resolved = resolver
5924 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5925 .expect("independent declarers of one unit name must load");
5926 assert_eq!(
5927 resolved.unit_index.owners_for("eur").len(),
5928 2,
5929 "every declarer of 'eur' stays in scope"
5930 );
5931 assert!(
5932 resolved.unit_index.resolve("eur").is_err(),
5933 "bare 'eur' with two declarers must be qualified"
5934 );
5935 assert_eq!(
5936 resolved.unit_index.owners_for("usd").len(),
5937 2,
5938 "every declarer of 'usd' stays in scope"
5939 );
5940 assert!(
5941 resolved.unit_index.resolve("usd").is_err(),
5942 "bare 'usd' with two declarers must be qualified"
5943 );
5944 assert_eq!(
5945 resolved.unit_index.owners_for("meter").len(),
5946 2,
5947 "every declarer of 'meter' stays in scope"
5948 );
5949 assert!(
5950 resolved.unit_index.resolve("meter").is_err(),
5951 "bare 'meter' with two declarers must be qualified"
5952 );
5953 let (_, owner) = resolved
5954 .unit_index
5955 .resolve("money_b.usd")
5956 .expect("qualified unit must resolve");
5957 assert_eq!(owner.name(), "money_b");
5958 }
5959
5960 #[test]
5961 fn test_measure_and_ratio_sharing_a_unit_name_keep_both_declarers() {
5962 let (resolver, spec) = resolver_single_spec(
5963 r#"spec test
5964 data q: measure
5965 -> unit foo: 1
5966
5967 data r: ratio
5968 -> unit foo: 100"#,
5969 );
5970
5971 let resolved = resolver
5972 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5973 .expect("independent declarers of one unit name must load");
5974 assert_eq!(
5975 resolved.unit_index.owners_for("foo").len(),
5976 2,
5977 "every declarer of 'foo' stays in scope"
5978 );
5979 assert!(
5980 resolved.unit_index.resolve("foo").is_err(),
5981 "bare 'foo' with two declarers must be qualified"
5982 );
5983 let (_, owner) = resolved
5984 .unit_index
5985 .resolve("r.foo")
5986 .expect("qualified unit must resolve");
5987 assert_eq!(owner.name(), "r");
5988 }
5989
5990 #[test]
5991 fn test_same_ratio_unit_across_types_keeps_both_declarers() {
5992 let (resolver, spec) = resolver_single_spec(
5993 r#"spec test
5994 data spread_a: ratio
5995 -> unit basis_points: 10000
5996
5997 data spread_b: ratio
5998 -> unit basis_points: 10000"#,
5999 );
6000
6001 let resolved = resolver
6002 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
6003 .expect("independent declarers of one unit name must load");
6004 assert_eq!(
6005 resolved.unit_index.owners_for("basis_points").len(),
6006 2,
6007 "every declarer of 'basis_points' stays in scope"
6008 );
6009 assert!(
6010 resolved.unit_index.resolve("basis_points").is_err(),
6011 "bare 'basis_points' with two declarers must be qualified"
6012 );
6013 let (_, owner) = resolved
6014 .unit_index
6015 .resolve("spread_a.basis_points")
6016 .expect("qualified unit must resolve");
6017 assert_eq!(owner.name(), "spread_a");
6018 }
6019
6020 #[test]
6021 fn test_different_ratio_unit_factor_across_types_keeps_both_declarers() {
6022 let (resolver, spec) = resolver_single_spec(
6023 r#"spec test
6024 data spread_a: ratio
6025 -> unit basis_points: 10000
6026
6027 data spread_b: ratio
6028 -> unit basis_points: 5000"#,
6029 );
6030
6031 let resolved = resolver
6032 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
6033 .expect("independent declarers of one unit name must load");
6034 assert_eq!(
6035 resolved.unit_index.owners_for("basis_points").len(),
6036 2,
6037 "every declarer of 'basis_points' stays in scope"
6038 );
6039 assert!(
6040 resolved.unit_index.resolve("basis_points").is_err(),
6041 "bare 'basis_points' with two declarers must be qualified"
6042 );
6043 let (_, owner) = resolved
6044 .unit_index
6045 .resolve("spread_b.basis_points")
6046 .expect("qualified unit must resolve");
6047 assert_eq!(owner.name(), "spread_b");
6048 }
6049
6050 #[test]
6051 fn test_multiple_builtin_ratio_types_share_percent_in_unit_index() {
6052 let (resolver, spec) = resolver_single_spec(
6053 r#"spec targets
6054 data standard_margin_pct: ratio
6055 -> minimum 0%
6056 -> suggest 15%
6057
6058 data default_credit_insurance_pct: ratio
6059 -> suggest 1.5%"#,
6060 );
6061
6062 resolver
6063 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
6064 .expect("multiple ratio types with built-in percent must not conflict");
6065 }
6066
6067 #[test]
6068 fn test_three_ratio_types_share_builtin_and_custom_unit() {
6069 let (resolver, spec) = resolver_single_spec(
6070 r#"spec test
6071 data margin: ratio -> suggest 10%
6072 data fee: ratio
6073 -> unit tenths: 10
6074 data tax: ratio -> suggest 1%"#,
6075 );
6076
6077 resolver
6078 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
6079 .expect("builtin percent/permille plus custom tenths on second type must load");
6080 }
6081
6082 #[test]
6083 fn test_ratio_unit_index_allows_builtin_after_two_named_types() {
6084 let (resolver, spec) = resolver_single_spec(
6085 r#"spec test
6086 data first_ratio: ratio -> suggest 5%
6087 data second_ratio: ratio -> suggest 10%"#,
6088 );
6089
6090 let resolved = resolver
6091 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
6092 .expect("two ratio types with only builtin units");
6093
6094 let (_, percent) = resolved
6095 .unit_index
6096 .resolve("percent")
6097 .expect("bare percent binds PrimitiveRatio");
6098 assert_eq!(percent.name(), "ratio");
6099 let (_, permille) = resolved
6100 .unit_index
6101 .resolve("permille")
6102 .expect("bare permille binds PrimitiveRatio");
6103 assert_eq!(permille.name(), "ratio");
6104 }
6105
6106 #[test]
6107 fn test_number_type_cannot_have_units() {
6108 let (resolver, spec) = resolver_single_spec(
6109 r#"spec test
6110 data price: number
6111 -> unit eur: 1.00"#,
6112 );
6113
6114 let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
6115 assert!(result.is_err(), "Number types must reject unit commands");
6116
6117 let errs = result.unwrap_err();
6118 assert!(!errs.is_empty(), "expected at least one error");
6119 let error_msg = errs[0].to_string();
6120 assert!(
6121 error_msg.contains("unit") && error_msg.contains("number"),
6122 "Error should mention units are invalid on number. Got: {}",
6123 error_msg
6124 );
6125 }
6126
6127 #[test]
6128 fn test_extending_type_inherits_units() {
6129 let (resolver, spec) = resolver_single_spec(
6130 r#"spec test
6131 data money: measure
6132 -> unit eur: 1.00
6133 -> unit usd: 0.84
6134
6135 data my_money: money
6136 -> unit gbp: 1.30"#,
6137 );
6138
6139 let resolved = resolver
6140 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
6141 .unwrap();
6142 let my_money_type = resolved.resolved.get("my_money").unwrap();
6143
6144 match &my_money_type.specifications {
6145 TypeSpecification::Measure { units, .. } => {
6146 assert_eq!(units.len(), 3);
6147 assert!(units.iter().any(|u| u.name == "eur"));
6148 assert!(units.iter().any(|u| u.name == "usd"));
6149 assert!(units.iter().any(|u| u.name == "gbp"));
6150 }
6151 other => panic!("Expected Measure type specifications, got {:?}", other),
6152 }
6153 }
6154
6155 #[test]
6156 fn binding_unit_factor_override_errors() {
6157 let (resolver, spec) = resolver_single_spec(
6158 r#"spec test
6159 data source_measure: measure
6160 -> unit usd: 1.00
6161 data z: source_measure
6162 -> unit usd: 0.84"#,
6163 );
6164
6165 let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
6166 assert!(
6167 result.is_err(),
6168 "binding row must not override inherited unit factor"
6169 );
6170 let error_msg = result
6171 .unwrap_err()
6172 .iter()
6173 .map(ToString::to_string)
6174 .collect::<Vec<_>>()
6175 .join("; ");
6176 assert!(
6177 error_msg.contains("usd"),
6178 "error must name unit usd, got: {error_msg}"
6179 );
6180 }
6181
6182 #[test]
6183 fn ratio_extension_unit_factor_override_errors() {
6184 let (resolver, spec) = resolver_single_spec(
6185 r#"spec test
6186 data parent: ratio
6187 -> unit basis: 1
6188 data child: parent
6189 -> unit basis: 100"#,
6190 );
6191
6192 let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
6193 assert!(
6194 result.is_err(),
6195 "ratio extension must not override inherited unit factor"
6196 );
6197 let error_msg = result
6198 .unwrap_err()
6199 .iter()
6200 .map(ToString::to_string)
6201 .collect::<Vec<_>>()
6202 .join("; ");
6203 assert!(
6204 error_msg.contains("basis"),
6205 "error must name unit basis, got: {error_msg}"
6206 );
6207 }
6208
6209 #[test]
6210 fn test_duplicate_unit_name_in_same_type_is_error() {
6211 let (resolver, spec) = resolver_single_spec(
6212 r#"spec test
6213 data money: measure
6214 -> unit eur: 1.00
6215 -> unit eur: 1.19"#,
6216 );
6217
6218 let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
6219 assert!(result.is_err(), "duplicate unit name must be rejected");
6220 let errs = result.unwrap_err();
6221 assert!(!errs.is_empty());
6222 let msg = errs[0].to_string();
6223 assert!(
6224 msg.contains("eur"),
6225 "error must name the duplicate unit; got: {msg}"
6226 );
6227 }
6228
6229 #[test]
6230 fn test_duplicate_unit_name_compound_is_error() {
6231 let (resolver, spec) = resolver_single_spec(
6232 r#"spec test
6233 data currency: measure
6234 -> unit usd: 1.00
6235 -> unit eur: 0.92
6236
6237 data time_period: measure
6238 -> unit year: 1
6239
6240 data run_rate: measure
6241 -> unit arr: usd/year
6242 -> unit arr: eur/year"#,
6243 );
6244
6245 let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
6246 assert!(
6247 result.is_err(),
6248 "duplicate compound unit name must be rejected"
6249 );
6250 let errs = result.unwrap_err();
6251 assert!(!errs.is_empty());
6252 let msg = errs[0].to_string();
6253 assert!(
6254 msg.contains("arr"),
6255 "error must name the duplicate unit; got: {msg}"
6256 );
6257 }
6258
6259 #[test]
6260 fn find_unique_reports_multiple_families_with_same_decomposition() {
6261 let code = r#"spec units
6262 data money: measure
6263 -> unit eur: 1
6264 -> unit usd: 0.86
6265 data mass: measure
6266 -> unit kg: 1
6267 data price_eur_per_kg: measure
6268 -> unit eur_per_kg: eur/kg
6269 data price_usd_per_kg: measure
6270 -> unit usd_per_kg: usd/kg
6271 "#;
6272 let (resolver, spec_arcs) = resolver_for_code(code);
6273 let units_arc = spec_arcs
6274 .iter()
6275 .find(|spec| spec.name == "units")
6276 .cloned()
6277 .expect("units spec");
6278 let (context, _) = test_context_and_effective(&spec_arcs);
6279 let repository = context.workspace();
6280 let resolved = resolver
6281 .resolve_and_validate(units_arc, &EffectiveDate::Origin, &ResolvedTypesMap::new())
6282 .expect("units resolves");
6283 let eur_decomp = resolved
6284 .unit_index
6285 .unique_owner("eur_per_kg")
6286 .expect("eur_per_kg")
6287 .measure_type_decomposition()
6288 .expect("eur_per_kg decomposition")
6289 .clone();
6290 let map = [(repository, units_arc, resolved)];
6291
6292 let units_types = map
6293 .iter()
6294 .find(|(_, s, _)| discovery::same_loaded_spec(s, units_arc))
6295 .map(|(_, _, t)| t)
6296 .expect("units in map");
6297 let unique =
6298 find_unique_measure_type_in_unit_index(&units_types.unit_index, &eur_decomp);
6299 match unique {
6300 DecompositionMatch::Multiple(families) => {
6301 assert_eq!(families.len(), 2);
6302 assert!(families.contains(&"price_eur_per_kg".to_string()));
6303 assert!(families.contains(&"price_usd_per_kg".to_string()));
6304 }
6305 other => panic!("expected Multiple families, got {other:?}"),
6306 }
6307 }
6308 }
6309
6310 mod type_validation {
6311 use super::*;
6312 use crate::computation::rational::rational_new;
6313 use crate::literals::UnitScale;
6314 use crate::parsing::ast::{CommandArg, TypeConstraintCommand};
6315 use crate::planning::semantics::TypeSpecification;
6316 use rust_decimal::Decimal;
6317
6318 fn test_source() -> Source {
6319 Source::new(
6320 crate::parsing::source::SourceType::Volatile,
6321 crate::parsing::ast::Span {
6322 start: 0,
6323 end: 0,
6324 line: 1,
6325 col: 0,
6326 },
6327 )
6328 }
6329
6330 fn apply(
6331 mut specs: TypeSpecification,
6332 command: TypeConstraintCommand,
6333 args: &[CommandArg],
6334 ) -> TypeSpecification {
6335 let mut suggestion: Option<RawSuggestion> = None;
6336 specs
6337 .apply_constraint(
6338 ConstraintNames {
6339 type_name: "test",
6340 data_name: "test",
6341 },
6342 command,
6343 args,
6344 &mut suggestion,
6345 &mut None,
6346 None,
6347 )
6348 .unwrap();
6349 specs
6350 }
6351
6352 fn number_arg(n: i64) -> CommandArg {
6353 CommandArg::Literal(crate::literals::Value::Number(Decimal::from(n)))
6354 }
6355
6356 fn date_arg(s: &str) -> CommandArg {
6357 let dt = s.parse::<crate::literals::DateTimeValue>().expect("date");
6358 CommandArg::Literal(crate::literals::Value::Date(dt))
6359 }
6360
6361 fn time_arg(s: &str) -> CommandArg {
6362 let t = s.parse::<crate::literals::TimeValue>().expect("time");
6363 CommandArg::Literal(crate::literals::Value::Time(t))
6364 }
6365
6366 #[test]
6367 fn validate_number_minimum_greater_than_maximum() {
6368 let mut specs = TypeSpecification::number();
6369 specs = apply(specs, TypeConstraintCommand::Minimum, &[number_arg(100)]);
6370 specs = apply(specs, TypeConstraintCommand::Maximum, &[number_arg(50)]);
6371
6372 let src = test_source();
6373 let errors = validate_type_specifications(
6374 &specs,
6375 None,
6376 "suggestion",
6377 "test",
6378 &src,
6379 None,
6380 &UnitIndex::new(),
6381 );
6382 assert_eq!(errors.len(), 1);
6383 assert!(errors[0]
6384 .to_string()
6385 .contains("minimum 100 is greater than maximum 50"));
6386 }
6387
6388 #[test]
6389 fn validate_number_default_below_minimum() {
6390 let specs = TypeSpecification::Number {
6391 minimum: Some(rational_new(10, 1)),
6392 maximum: None,
6393 decimals: None,
6394 help: String::new(),
6395 };
6396 let default = BoundValueKind::unbound(ValueKind::Number(rational_new(5, 1)));
6397
6398 let src = test_source();
6399 let errors = validate_type_specifications(
6400 &specs,
6401 Some(&default),
6402 "suggestion",
6403 "test",
6404 &src,
6405 None,
6406 &UnitIndex::new(),
6407 );
6408 assert_eq!(errors.len(), 1);
6409 assert!(errors[0]
6410 .to_string()
6411 .contains("suggestion value 5 is less than minimum 10"));
6412 }
6413
6414 #[test]
6415 fn validate_number_default_above_maximum() {
6416 let specs = TypeSpecification::Number {
6417 minimum: None,
6418 maximum: Some(rational_new(100, 1)),
6419 decimals: None,
6420 help: String::new(),
6421 };
6422 let default = BoundValueKind::unbound(ValueKind::Number(rational_new(150, 1)));
6423
6424 let src = test_source();
6425 let errors = validate_type_specifications(
6426 &specs,
6427 Some(&default),
6428 "suggestion",
6429 "test",
6430 &src,
6431 None,
6432 &UnitIndex::new(),
6433 );
6434 assert_eq!(errors.len(), 1);
6435 assert!(errors[0]
6436 .to_string()
6437 .contains("suggestion value 150 is greater than maximum 100"));
6438 }
6439
6440 #[test]
6441 fn validate_number_default_valid() {
6442 let specs = TypeSpecification::Number {
6443 minimum: Some(rational_new(0, 1)),
6444 maximum: Some(rational_new(100, 1)),
6445 decimals: None,
6446 help: String::new(),
6447 };
6448 let default = BoundValueKind::unbound(ValueKind::Number(rational_new(50, 1)));
6449
6450 let src = test_source();
6451 let errors = validate_type_specifications(
6452 &specs,
6453 Some(&default),
6454 "suggestion",
6455 "test",
6456 &src,
6457 None,
6458 &UnitIndex::new(),
6459 );
6460 assert!(errors.is_empty());
6461 }
6462
6463 #[test]
6464 fn validate_ratio_minimum_greater_than_maximum() {
6465 let specs = TypeSpecification::Ratio {
6466 minimum: Some(rational_new(2, 1)),
6467 maximum: Some(rational_new(1, 1)),
6468 units: crate::planning::semantics::RatioUnits::new(),
6469 unit: None,
6470 help: String::new(),
6471 };
6472
6473 let src = test_source();
6474 let errors = validate_type_specifications(
6475 &specs,
6476 None,
6477 "suggestion",
6478 "test",
6479 &src,
6480 None,
6481 &UnitIndex::new(),
6482 );
6483 assert_eq!(errors.len(), 1);
6484 assert!(errors[0]
6485 .to_string()
6486 .contains("minimum 2 is greater than maximum 1"));
6487 }
6488
6489 #[test]
6490 fn validate_date_minimum_after_maximum() {
6491 let mut specs = TypeSpecification::date();
6492 specs = apply(
6493 specs,
6494 TypeConstraintCommand::Minimum,
6495 &[date_arg("2024-12-31")],
6496 );
6497 specs = apply(
6498 specs,
6499 TypeConstraintCommand::Maximum,
6500 &[date_arg("2024-01-01")],
6501 );
6502
6503 let src = test_source();
6504 let errors = validate_type_specifications(
6505 &specs,
6506 None,
6507 "suggestion",
6508 "test",
6509 &src,
6510 None,
6511 &UnitIndex::new(),
6512 );
6513 assert_eq!(errors.len(), 1);
6514 assert!(
6515 errors[0].to_string().contains("minimum")
6516 && errors[0].to_string().contains("is after maximum")
6517 );
6518 }
6519
6520 #[test]
6521 fn validate_date_valid_range() {
6522 let mut specs = TypeSpecification::date();
6523 specs = apply(
6524 specs,
6525 TypeConstraintCommand::Minimum,
6526 &[date_arg("2024-01-01")],
6527 );
6528 specs = apply(
6529 specs,
6530 TypeConstraintCommand::Maximum,
6531 &[date_arg("2024-12-31")],
6532 );
6533
6534 let src = test_source();
6535 let errors = validate_type_specifications(
6536 &specs,
6537 None,
6538 "suggestion",
6539 "test",
6540 &src,
6541 None,
6542 &UnitIndex::new(),
6543 );
6544 assert!(errors.is_empty());
6545 }
6546
6547 #[test]
6548 fn validate_time_minimum_after_maximum() {
6549 let mut specs = TypeSpecification::time();
6550 specs = apply(
6551 specs,
6552 TypeConstraintCommand::Minimum,
6553 &[time_arg("23:00:00")],
6554 );
6555 specs = apply(
6556 specs,
6557 TypeConstraintCommand::Maximum,
6558 &[time_arg("10:00:00")],
6559 );
6560
6561 let src = test_source();
6562 let errors = validate_type_specifications(
6563 &specs,
6564 None,
6565 "suggestion",
6566 "test",
6567 &src,
6568 None,
6569 &UnitIndex::new(),
6570 );
6571 assert_eq!(errors.len(), 1);
6572 assert!(
6573 errors[0].to_string().contains("minimum")
6574 && errors[0].to_string().contains("is after maximum")
6575 );
6576 }
6577
6578 fn magnitude_beyond_decimal_max() -> crate::computation::rational::RationalInteger {
6579 use crate::computation::rational::{decimal_to_rational, rational_new, try_mul};
6580 use rust_decimal::Decimal;
6581 let max = Decimal::MAX.normalize();
6582 let max_rational = decimal_to_rational(max).expect("BUG: Decimal::MAX must lift to Q");
6583 try_mul(&max_rational, &rational_new(2, 1))
6584 .expect("BUG: test rational multiply must succeed")
6585 }
6586
6587 #[test]
6588 fn large_magnitude_minimum_does_not_fail_type_validation() {
6589 use crate::computation::rational::rational_one;
6590 use crate::literals::MeasureUnits;
6591 use crate::planning::semantics::TypeSpecification;
6592
6593 let too_large = magnitude_beyond_decimal_max();
6594 assert_eq!(
6595 too_large.try_to_decimal().unwrap_err(),
6596 crate::computation::rational::NumericFailure::Overflow,
6597 );
6598
6599 let spec = TypeSpecification::Measure {
6600 minimum: Some((too_large, "eur".to_string())),
6601 maximum: None,
6602 units: MeasureUnits(vec![crate::literals::MeasureUnit {
6603 name: "eur".to_string(),
6604 scale: UnitScale::Literal(rational_one()),
6605 derived_measure_factors: Default::default(),
6606 decomposition: Default::default(),
6607 minimum: None,
6608 maximum: None,
6609 suggestion_magnitude: None,
6610 }]),
6611 traits: vec![],
6612 decomposition: None,
6613 unit: None,
6614 help: String::new(),
6615 };
6616
6617 let src = test_source();
6618 let errors = validate_type_specifications(
6619 &spec,
6620 None,
6621 "suggestion",
6622 "money",
6623 &src,
6624 None,
6625 &UnitIndex::new(),
6626 );
6627 assert!(
6628 errors.is_empty(),
6629 "internal Q bounds must not fail type validation; API decimal rounding: {:?}",
6630 errors.iter().map(|e| e.to_string()).collect::<Vec<_>>()
6631 );
6632 }
6633
6634 #[test]
6635 fn large_magnitude_maximum_does_not_fail_type_validation() {
6636 use crate::computation::rational::rational_one;
6637 use crate::literals::MeasureUnits;
6638 use crate::planning::semantics::TypeSpecification;
6639
6640 let too_large = magnitude_beyond_decimal_max();
6641 assert_eq!(
6642 too_large.try_to_decimal().unwrap_err(),
6643 crate::computation::rational::NumericFailure::Overflow,
6644 );
6645
6646 let spec = TypeSpecification::Measure {
6647 minimum: None,
6648 maximum: Some((too_large, "eur".to_string())),
6649 units: MeasureUnits(vec![crate::literals::MeasureUnit {
6650 name: "eur".to_string(),
6651 scale: UnitScale::Literal(rational_one()),
6652 derived_measure_factors: Default::default(),
6653 decomposition: Default::default(),
6654 minimum: None,
6655 maximum: None,
6656 suggestion_magnitude: None,
6657 }]),
6658 traits: vec![],
6659 decomposition: None,
6660 unit: None,
6661 help: String::new(),
6662 };
6663
6664 let src = test_source();
6665 let errors = validate_type_specifications(
6666 &spec,
6667 None,
6668 "suggestion",
6669 "money",
6670 &src,
6671 None,
6672 &UnitIndex::new(),
6673 );
6674 assert!(errors.is_empty(), "got: {:?}", errors);
6675 }
6676
6677 #[test]
6678 fn large_magnitude_default_does_not_fail_type_validation() {
6679 use crate::computation::rational::rational_one;
6680 use crate::literals::MeasureUnits;
6681 use crate::planning::semantics::TypeSpecification;
6682
6683 let too_large = magnitude_beyond_decimal_max();
6684 assert_eq!(
6685 too_large.try_to_decimal().unwrap_err(),
6686 crate::computation::rational::NumericFailure::Overflow,
6687 );
6688
6689 let spec = TypeSpecification::Measure {
6690 minimum: None,
6691 maximum: None,
6692 units: MeasureUnits(vec![crate::literals::MeasureUnit {
6693 name: "eur".to_string(),
6694 scale: UnitScale::Literal(rational_one()),
6695 derived_measure_factors: Default::default(),
6696 decomposition: Default::default(),
6697 minimum: None,
6698 maximum: None,
6699 suggestion_magnitude: Some(too_large),
6700 }]),
6701 traits: vec![],
6702 decomposition: None,
6703 unit: None,
6704 help: String::new(),
6705 };
6706
6707 let src = test_source();
6708 let errors = validate_type_specifications(
6709 &spec,
6710 None,
6711 "suggestion",
6712 "money",
6713 &src,
6714 None,
6715 &UnitIndex::new(),
6716 );
6717 assert!(errors.is_empty(), "got: {:?}", errors);
6718 }
6719
6720 #[test]
6721 fn refresh_named_range_specs_missing_element_must_not_leave_unconverted_measure() {
6722 use crate::engine::Context;
6723 use crate::parsing::ast::{LemmaSpec, ParentType, Span};
6724 use crate::parsing::source::{Source, SourceType};
6725 use crate::planning::semantics::tests::primitive_measure_arc;
6726
6727 let mut ctx = Context::new();
6728 let repository = ctx.workspace();
6729 ctx.insert_spec(Arc::clone(&repository), LemmaSpec::new("t".to_string()))
6730 .unwrap();
6731 let ctx: &'static Context = Box::leak(Box::new(ctx));
6732 let mut resolver = TypeResolver::new(ctx, HashMap::new());
6733 let spec = ctx
6734 .spec_set(&ctx.workspace(), "t")
6735 .and_then(|ss| ss.get_exact(None))
6736 .expect("inserted");
6737 assert!(resolver.register_all(&ctx.workspace(), spec).is_empty());
6738 let source = Source::new(
6739 SourceType::Volatile,
6740 Span {
6741 start: 0,
6742 end: 0,
6743 line: 1,
6744 col: 0,
6745 },
6746 );
6747 let mut data_defs = HashMap::new();
6748 data_defs.insert(
6749 "band".to_string(),
6750 DataTypeDef {
6751 parent: ParentType::Ranged {
6752 inner: Box::new(ParentType::custom("ghost")),
6753 },
6754 constraints: None,
6755 source: source.clone(),
6756 name: "band".to_string(),
6757 bound_literal: None,
6758 },
6759 );
6760 let mut resolved_types = ResolvedSpecTypes::default();
6761 resolved_types
6762 .resolved
6763 .insert("band".to_string(), primitive_measure_arc().clone());
6764 let already_resolved = ResolvedTypesMap::new();
6765
6766 let errors = refresh_named_range_specs(
6767 &resolver,
6768 spec,
6769 &data_defs,
6770 &mut resolved_types,
6771 &already_resolved,
6772 &EffectiveDate::Origin,
6773 );
6774
6775 assert!(
6776 !errors.is_empty(),
6777 "planning must error when ranged inner type is missing"
6778 );
6779 assert!(
6780 errors[0]
6781 .to_string()
6782 .contains("Unknown parent 'ghost' for data definition"),
6783 "got: {}",
6784 errors[0]
6785 );
6786 }
6787 }
6788}