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_factors().is_empty() {
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 let factors = unit.resolved_scale_factors().to_vec();
2192 if factors.is_empty() {
2193 panic!(
2194 "BUG: inherited dynamic unit '{}' on '{type_name}' still has unresolved scale factors after the declaring type's resolution",
2195 unit.name
2196 );
2197 }
2198 resolved_unit_scales[unit_idx] = Some(ResolvedCompoundScale::Dynamic {
2199 factors,
2200 coefficient: unit.scale_coefficient(),
2201 });
2202 }
2203 continue;
2204 }
2205
2206 match resolve_compound_unit(
2207 spec_name,
2208 type_name,
2209 unit,
2210 &lookup,
2211 unit_index,
2212 &resolved,
2213 type_source.as_ref(),
2214 ) {
2215 Ok((unit_decomp, derived_scale)) => {
2216 if let [(referenced, 1)] = unit.derived_measure_factors.as_slice() {
2217 let (bare_name, owner) = unit_index
2218 .resolve_owner(referenced, &resolved)
2219 .unwrap_or_else(|err| {
2220 panic!(
2221 "BUG: factor '{referenced}' resolved for its conversion but not for its owner: {err}"
2222 )
2223 });
2224 let owning_type = owner.lemma_type(&resolved);
2225 let owner_family = owning_type
2226 .measure_family()
2227 .expect("BUG: resolved unit owner is not a named measure");
2228 let declaring_family = resolved[type_name]
2229 .measure_family()
2230 .expect("BUG: declaring measure has no family");
2231 if owner_family != declaring_family {
2232 unit_errors.push(Error::validation(
2233 format!(
2234 "In spec '{spec_name}': unit '{}' in measure type '{type_name}' cannot use a single unit from another measure. '{bare_name}' belongs to '{}'.",
2235 unit.name, owner_family.name
2236 ),
2237 type_source.clone(),
2238 None::<String>,
2239 ));
2240 }
2241 }
2242 if unit_decomp == declared_measure_decomposition(resolved[type_name].as_ref()) {
2245 resolve_to_simple[unit_idx] = true;
2246 }
2247 if let Some(existing) = &resolved_type_decomp {
2248 if existing != &unit_decomp {
2249 unit_errors.push(Error::validation(
2250 format!(
2251 "In spec '{}': measure type '{}' has inconsistent unit \
2252 decompositions. Unit '{}' resolved to {:?} but other units \
2253 resolved to {:?}. All units of a measure must measure the same \
2254 physical measure.",
2255 spec_name, type_name, unit.name, unit_decomp, existing
2256 ),
2257 type_source.clone(),
2258 None::<String>,
2259 ));
2260 }
2261 } else {
2262 resolved_type_decomp = Some(unit_decomp);
2263 }
2264
2265 resolved_unit_scales[unit_idx] = Some(derived_scale);
2266 }
2267 Err(err) => unit_errors.push(err),
2268 }
2269 }
2270
2271 if !unit_errors.is_empty() {
2272 errors.extend(unit_errors);
2273 continue;
2274 }
2275
2276 let type_decomp = match resolved_type_decomp {
2277 Some(d) => d,
2278 None => continue,
2279 };
2280
2281 let owned = unwrap_unique_resolved_type(
2282 resolved
2283 .shift_remove(type_name)
2284 .expect("BUG: type_name comes from resolved's own keys"),
2285 );
2286
2287 let updated = owned.map_measure(|units, _| {
2288 let units = MeasureUnits(
2289 units
2290 .0
2291 .into_iter()
2292 .enumerate()
2293 .map(|(idx, u)| {
2294 let u = u.with_decomposition(type_decomp.clone());
2295 let u = match resolved_unit_scales[idx].clone() {
2296 Some(ResolvedCompoundScale::Literal(factor)) => u.with_factor(factor),
2297 Some(ResolvedCompoundScale::Dynamic {
2298 factors,
2299 coefficient,
2300 }) => u.with_reference_scale(factors, coefficient),
2301 None => u,
2302 };
2303 if resolve_to_simple[idx] {
2304 u.with_derived_measure_factors(Vec::new())
2305 } else {
2306 u
2307 }
2308 })
2309 .collect(),
2310 );
2311 (units, Some(type_decomp.clone()))
2312 });
2313
2314 if let TypeSpecification::Measure { units, .. } = &updated.specifications {
2315 for unit in units.iter() {
2316 if body_already_resolved && !unit_owned_locally(&unit.name) {
2319 continue;
2320 }
2321 let Some(factor) = unit.literal_factor().cloned() else {
2322 continue;
2323 };
2324 lookup.insert(
2325 unit.name.clone(),
2326 (Some(type_name.clone()), type_decomp.clone(), factor),
2327 );
2328 }
2329 }
2330 resolved.insert(type_name.clone(), Arc::new(updated));
2331 }
2332
2333 let resolved = canonicalize_unit_signatures(resolved);
2334
2335 (resolved, errors)
2336}
2337
2338fn finalize_lemma_measure_magnitudes(
2343 lemma_type: LemmaType,
2344 declared_suggestion: Option<&BoundValueKind>,
2345 type_name: &str,
2346) -> Result<LemmaType, String> {
2347 let LemmaType {
2348 name,
2349 mut specifications,
2350 extends,
2351 options,
2352 } = lemma_type;
2353 semantics::finalize_measure_unit_constraint_magnitudes(
2354 &mut specifications,
2355 declared_suggestion.map(|bound| &bound.value),
2356 type_name,
2357 )?;
2358 Ok(LemmaType {
2359 name,
2360 specifications,
2361 extends,
2362 options,
2363 })
2364}
2365
2366fn finalize_measure_magnitudes_in_resolved(
2367 resolved: IndexMap<String, Arc<LemmaType>>,
2368 declared_suggestions: &IndexMap<String, BoundValueKind>,
2369 type_sources: &HashMap<String, Source>,
2370 spec_name: &str,
2371 spec: &LemmaSpec,
2372) -> (IndexMap<String, Arc<LemmaType>>, Vec<Error>) {
2373 resolved.into_iter().fold(
2374 (IndexMap::new(), Vec::new()),
2375 |(mut acc, mut errors), (type_name, arc)| {
2376 let source = type_sources.get(&type_name).cloned().unwrap_or_else(|| {
2377 unreachable!(
2378 "BUG: resolved type '{}' has no corresponding DataTypeDef in spec '{}'",
2379 type_name, spec_name
2380 )
2381 });
2382 let owned = unwrap_unique_resolved_type(arc);
2383 let lemma_type = match finalize_lemma_measure_magnitudes(
2384 owned.clone(),
2385 declared_suggestions.get(&type_name),
2386 &type_name,
2387 ) {
2388 Ok(lt) => lt,
2389 Err(message) => {
2390 errors.push(Error::validation_with_context(
2391 format!(
2392 "Type '{}' has invalid measure unit constraints: {}",
2393 type_name, message
2394 ),
2395 Some(source),
2396 None::<String>,
2397 Some(spec),
2398 None,
2399 ));
2400 owned
2401 }
2402 };
2403 acc.insert(type_name, Arc::new(lemma_type));
2404 (acc, errors)
2405 },
2406 )
2407}
2408
2409fn canonicalize_unit_signatures(types: TypeMap) -> TypeMap {
2417 types
2418 .into_iter()
2419 .map(|(name, arc)| {
2420 (
2421 name,
2422 Arc::new(canonicalize_lemma_unit_signatures(
2423 unwrap_unique_resolved_type(arc),
2424 )),
2425 )
2426 })
2427 .collect()
2428}
2429
2430fn canonicalize_lemma_unit_signatures(lemma_type: LemmaType) -> LemmaType {
2431 lemma_type.map_measure(|units, decomposition| {
2432 let units = units.map(canonicalize_unit_for_measure);
2433 (units, decomposition)
2434 })
2435}
2436
2437fn reserved_calendar_units_in_measure(lemma_type: &LemmaType) -> Vec<String> {
2438 let TypeSpecification::Measure { units, .. } = &lemma_type.specifications else {
2439 return Vec::new();
2440 };
2441 units
2442 .iter()
2443 .filter(|u| calendar_unit_factor(&u.name).is_some())
2444 .map(|u| u.name.clone())
2445 .collect()
2446}
2447
2448fn measure_unit_is_simple(unit: &MeasureUnit) -> bool {
2449 unit.derived_measure_factors.is_empty()
2450 || (unit.derived_measure_factors.len() == 1
2451 && unit.derived_measure_factors[0].0 == unit.name
2452 && unit.derived_measure_factors[0].1 == 1)
2453}
2454
2455fn canonicalize_unit_for_measure(unit: MeasureUnit) -> MeasureUnit {
2456 let factors = if measure_unit_is_simple(&unit) {
2457 vec![(unit.name.clone(), 1)]
2458 } else {
2459 canonicalize_signature(&unit.derived_measure_factors)
2460 };
2461 unit.with_derived_measure_factors(factors)
2462}
2463
2464#[derive(Clone, Debug)]
2466enum ResolvedCompoundScale {
2467 Literal(RationalInteger),
2468 Dynamic {
2471 factors: Vec<(crate::planning::semantics::ReferenceTarget, i32)>,
2472 coefficient: RationalInteger,
2473 },
2474}
2475
2476fn resolve_compound_unit(
2477 spec_name: &str,
2478 declaring_type_name: &str,
2479 unit: &MeasureUnit,
2480 lookup: &UnitDecompLookup,
2481 unit_index: &PlanningUnitIndex,
2482 resolved: &IndexMap<String, Arc<LemmaType>>,
2483 source: Option<&Source>,
2484) -> Result<(BaseMeasureVector, ResolvedCompoundScale), Error> {
2485 use crate::computation::bigint::BigInt;
2486 use crate::computation::rational::{checked_mul, checked_pow_integer, rational_one};
2487
2488 let mut result: BaseMeasureVector = BaseMeasureVector::new();
2489 let mut coefficient = unit.literal_factor().cloned().unwrap_or_else(rational_one);
2490 let mut dynamic_factors: Vec<(crate::planning::semantics::ReferenceTarget, i32)> = Vec::new();
2491
2492 for (measure_ref, exponent) in &unit.derived_measure_factors {
2493 let (owning_measure_name, owning_decomp, component) = if let Some(entry) =
2494 lookup.get(measure_ref.as_str())
2495 {
2496 (
2497 entry.0.clone(),
2498 entry.1.clone(),
2499 CompoundComponent::Literal(entry.2.clone()),
2500 )
2501 } else {
2502 let (bare, owner) = unit_index
2503 .resolve_owner(measure_ref, resolved)
2504 .map_err(|err| {
2505 Error::validation(
2506 format!(
2507 "In spec '{}': unit '{}' in measure type '{}' references '{}': {}. \
2508 Add `uses <spec>` (or declare the owning measure type in this spec) \
2509 so its units are in scope.",
2510 spec_name, unit.name, declaring_type_name, measure_ref, err
2511 ),
2512 source.cloned(),
2513 None::<String>,
2514 )
2515 })?;
2516 let owning_type = owner.lemma_type(resolved);
2517 let TypeSpecification::Measure {
2518 decomposition: Some(decomposition),
2519 units,
2520 ..
2521 } = &owning_type.specifications
2522 else {
2523 return Err(Error::validation(
2524 format!(
2525 "In spec '{}': unit '{}' in measure type '{}' references '{}' which did not resolve to a measure unit",
2526 spec_name, unit.name, declaring_type_name, measure_ref
2527 ),
2528 source.cloned(),
2529 None::<String>,
2530 ));
2531 };
2532 let referenced = units.iter().find(|u| u.name == bare).unwrap_or_else(|| {
2533 panic!(
2534 "BUG: resolved unit '{}' must be declared on type '{}'",
2535 bare,
2536 owning_type.name()
2537 )
2538 });
2539 let component = if let Some(factor) = referenced.literal_factor().cloned() {
2540 CompoundComponent::Literal(factor)
2541 } else {
2542 let factors = referenced.resolved_scale_factors();
2543 if factors.is_empty() {
2544 panic!(
2545 "BUG: dynamic unit '{bare}' on '{}' must be Reference-scaled before compounds that inherit it resolve",
2546 owning_type.name()
2547 );
2548 }
2549 CompoundComponent::Dynamic {
2550 bare: bare.to_string(),
2551 factors: factors.to_vec(),
2552 coefficient: referenced.scale_coefficient(),
2553 }
2554 };
2555 (local_owner_name(owner), decomposition.clone(), component)
2556 };
2557 let owning_decomp = &owning_decomp;
2558
2559 if owning_measure_name.as_deref() == Some(declaring_type_name) {
2560 return Err(Error::validation(
2561 format!(
2562 "In spec '{}': unit '{}' in measure type '{}' references unit '{}' which \
2563 belongs to the same measure type. A measure cannot reference its own units \
2564 in a compound expression.",
2565 spec_name, unit.name, declaring_type_name, measure_ref
2566 ),
2567 source.cloned(),
2568 None::<String>,
2569 ));
2570 }
2571
2572 for (dim, &dim_exp) in owning_decomp {
2573 let combined = dim_exp
2574 .checked_mul(*exponent)
2575 .and_then(|value| accumulate(&mut result, dim, value));
2576 if combined.is_none() {
2577 return Err(unit_exponent_limit_error(
2578 spec_name,
2579 &unit.name,
2580 declaring_type_name,
2581 source,
2582 ));
2583 }
2584 }
2585
2586 match component {
2587 CompoundComponent::Literal(unit_factor) => {
2588 let component_contribution =
2589 checked_pow_integer(&unit_factor, &BigInt::from_i64(i64::from(*exponent)))
2590 .map_err(|error| {
2591 overflow_to_validation_error(
2592 spec_name,
2593 &unit.name,
2594 declaring_type_name,
2595 measure_ref,
2596 error,
2597 source,
2598 )
2599 })?;
2600 coefficient =
2601 checked_mul(&coefficient, &component_contribution).map_err(|error| {
2602 overflow_to_validation_error(
2603 spec_name,
2604 &unit.name,
2605 declaring_type_name,
2606 measure_ref,
2607 error,
2608 source,
2609 )
2610 })?;
2611 }
2612 CompoundComponent::Dynamic {
2613 bare,
2614 factors: base_factors,
2615 coefficient: base_coefficient,
2616 } => {
2617 let raised_coefficient =
2618 checked_pow_integer(&base_coefficient, &BigInt::from_i64(i64::from(*exponent)))
2619 .map_err(|error| {
2620 overflow_to_validation_error(
2621 spec_name,
2622 &unit.name,
2623 declaring_type_name,
2624 measure_ref,
2625 error,
2626 source,
2627 )
2628 })?;
2629 coefficient = checked_mul(&coefficient, &raised_coefficient).map_err(|error| {
2630 overflow_to_validation_error(
2631 spec_name,
2632 &unit.name,
2633 declaring_type_name,
2634 measure_ref,
2635 error,
2636 source,
2637 )
2638 })?;
2639 for (target, base_exponent) in base_factors {
2640 let component_exponent = base_exponent.checked_mul(*exponent).ok_or_else(|| {
2641 Error::validation(
2642 format!(
2643 "In spec '{spec_name}': unit '{}' in measure type '{declaring_type_name}' overflowed combining dynamic unit '{bare}' exponents.",
2644 unit.name
2645 ),
2646 source.cloned(),
2647 None::<String>,
2648 )
2649 })?;
2650 dynamic_factors.push((target, component_exponent));
2651 }
2652 }
2653 }
2654 }
2655
2656 let factors = merge_compound_scale_factors(
2657 spec_name,
2658 declaring_type_name,
2659 &unit.name,
2660 &dynamic_factors,
2661 source,
2662 )?;
2663 let derived_scale = if factors.is_empty() {
2664 ResolvedCompoundScale::Literal(coefficient)
2665 } else {
2666 ResolvedCompoundScale::Dynamic {
2667 factors,
2668 coefficient,
2669 }
2670 };
2671 Ok((result, derived_scale))
2672}
2673
2674fn merge_compound_scale_factors(
2675 spec_name: &str,
2676 declaring_type_name: &str,
2677 unit_name: &str,
2678 factors: &[(crate::planning::semantics::ReferenceTarget, i32)],
2679 source: Option<&Source>,
2680) -> Result<Vec<(crate::planning::semantics::ReferenceTarget, i32)>, Error> {
2681 use std::collections::BTreeMap;
2682 let mut accumulator: BTreeMap<crate::planning::semantics::ReferenceTarget, i32> =
2683 BTreeMap::new();
2684 for (target, exponent) in factors {
2685 let entry = accumulator.entry(target.clone()).or_insert(0);
2686 *entry = entry.checked_add(*exponent).ok_or_else(|| {
2687 Error::validation(
2688 format!(
2689 "In spec '{spec_name}': unit '{unit_name}' in measure type '{declaring_type_name}' overflowed combining dynamic scale factor exponents."
2690 ),
2691 source.cloned(),
2692 None::<String>,
2693 )
2694 })?;
2695 }
2696 Ok(accumulator
2697 .into_iter()
2698 .filter(|(_, exponent)| *exponent != 0)
2699 .collect())
2700}
2701
2702enum CompoundComponent {
2703 Literal(RationalInteger),
2704 Dynamic {
2705 bare: String,
2706 factors: Vec<(crate::planning::semantics::ReferenceTarget, i32)>,
2707 coefficient: RationalInteger,
2708 },
2709}
2710
2711fn overflow_to_validation_error(
2712 spec_name: &str,
2713 unit_name: &str,
2714 declaring_type_name: &str,
2715 measure_ref: &str,
2716 failure: NumericFailure,
2717 source: Option<&Source>,
2718) -> Error {
2719 Error::validation(
2720 format!(
2721 "In spec '{}': unit '{}' in measure type '{}' overflowed while combining '{}': {}",
2722 spec_name, unit_name, declaring_type_name, measure_ref, failure
2723 ),
2724 source.cloned(),
2725 None::<String>,
2726 )
2727}
2728
2729fn unit_exponent_limit_error(
2730 spec_name: &str,
2731 unit_name: &str,
2732 declaring_type_name: &str,
2733 source: Option<&Source>,
2734) -> Error {
2735 Error::validation(
2736 format!(
2737 "In spec '{}': unit '{}' in measure type '{}' has a unit exponent exceeding the maximum of {}",
2738 spec_name,
2739 unit_name,
2740 declaring_type_name,
2741 MAX_UNIT_EXPONENT
2742 ),
2743 source.cloned(),
2744 None::<String>,
2745 )
2746}
2747
2748fn accumulate(
2750 result: &mut BaseMeasureVector,
2751 dim: &semantics::Dimension,
2752 value: i32,
2753) -> Option<()> {
2754 let entry = result.entry(dim.clone()).or_insert(0);
2755 *entry = entry.checked_add(value)?;
2756 if entry.unsigned_abs() > MAX_UNIT_EXPONENT.unsigned_abs() {
2757 return None;
2758 }
2759 if *entry == 0 {
2760 result.remove(dim);
2761 }
2762 Some(())
2763}
2764
2765#[derive(Debug, Clone, Default)]
2771pub struct ResolvedSpecTypes {
2772 pub resolved: IndexMap<String, Arc<LemmaType>>,
2775
2776 pub declared_suggestions: IndexMap<String, BoundValueKind>,
2782
2783 pub declared_values: IndexMap<String, BoundValueKind>,
2787
2788 pub(crate) raw_suggestions: Vec<(String, RawSuggestion)>,
2790
2791 pub(crate) raw_values: Vec<(String, RawSuggestion)>,
2793
2794 pub(crate) source_defaults: IndexMap<String, RawSuggestion>,
2796
2797 pub(crate) source_value_defaults: IndexMap<String, RawSuggestion>,
2799
2800 pub(crate) rule_list_types: Vec<(ParentType, Arc<LemmaType>)>,
2803
2804 pub unit_index: crate::planning::unit_index::UnitIndex,
2807
2808 pub(crate) deferred_signature_collisions:
2811 Vec<(String, crate::planning::semantics::BaseMeasureVector)>,
2812
2813 planning_unit_index: PlanningUnitIndex,
2815
2816 pub signature_index: crate::computation::arithmetic::SignatureIndex,
2819}
2820
2821#[derive(Debug, Clone, PartialEq)]
2823pub(crate) struct DataTypeDef {
2824 pub parent: ParentType,
2825 pub constraints: Option<Vec<Constraint>>,
2826 pub source: crate::parsing::source::Source,
2827 pub name: String,
2828 pub bound_literal: Option<ast::Value>,
2830}
2831
2832#[derive(Debug, Clone)]
2836pub(crate) struct TypeResolver<'a> {
2837 data_types: Vec<(&'a LemmaSpec, HashMap<String, DataTypeDef>)>,
2838 context: &'a Context,
2839 all_registered_specs: Vec<(Arc<LemmaRepository>, &'a LemmaSpec)>,
2840 specs_with_failed_registration: Vec<&'a LemmaSpec>,
2845 slices: HashMap<SpecVersionKey, Arc<instance_list::VersionSlices>>,
2847}
2848
2849fn element_parent_type(parent: &ParentType) -> &ParentType {
2851 let mut current = parent;
2852 while let ParentType::Ranged { inner } | ParentType::List { inner } = current {
2853 current = inner.as_ref();
2854 }
2855 current
2856}
2857
2858pub(crate) fn time_range_endpoints_share_timezone(
2859 left: &semantics::SemanticTime,
2860 right: &semantics::SemanticTime,
2861) -> bool {
2862 match (&left.timezone, &right.timezone) {
2863 (None, None) => true,
2864 (Some(left_tz), Some(right_tz)) => left_tz.offset_minutes == right_tz.offset_minutes,
2865 _ => false,
2866 }
2867}
2868
2869fn parser_value_base_type_display_name(value: &ast::Value) -> &'static str {
2873 match value {
2874 ast::Value::Number(_) => "number",
2875 ast::Value::Text(_) => "text",
2876 ast::Value::Boolean(_) => "boolean",
2877 ast::Value::Date(_) => "date",
2878 ast::Value::Time(_) => "time",
2879 ast::Value::NumberWithUnit(_, unit) => match unit.as_str() {
2880 "percent" | "permille" => "ratio",
2881 _ => "measure",
2882 },
2883 ast::Value::Range(_, _) => "range",
2884 }
2885}
2886
2887fn inferred_parent_type_from_literal(value: &ast::Value) -> Result<ParentType, String> {
2892 let parent_type = match value {
2893 ast::Value::Number(_) => ParentType::Primitive {
2894 primitive: PrimitiveKind::Number,
2895 },
2896 ast::Value::Text(_) => ParentType::Primitive {
2897 primitive: PrimitiveKind::Text,
2898 },
2899 ast::Value::Boolean(_) => ParentType::Primitive {
2900 primitive: PrimitiveKind::Boolean,
2901 },
2902 ast::Value::Date(_) => ParentType::Primitive {
2903 primitive: PrimitiveKind::Date,
2904 },
2905 ast::Value::Time(_) => ParentType::Primitive {
2906 primitive: PrimitiveKind::Time,
2907 },
2908 ast::Value::NumberWithUnit(_, _) => ParentType::Primitive {
2909 primitive: PrimitiveKind::Measure,
2910 },
2911 ast::Value::Range(left, right) => {
2912 let primitive = match (left.as_ref(), right.as_ref()) {
2913 (ast::Value::Number(_), ast::Value::Number(_)) => PrimitiveKind::NumberRange,
2914 (ast::Value::Date(_), ast::Value::Date(_)) => PrimitiveKind::DateRange,
2915 (ast::Value::Time(_), ast::Value::Time(_)) => PrimitiveKind::TimeRange,
2916 (ast::Value::NumberWithUnit(_, u1), ast::Value::NumberWithUnit(_, u2))
2917 if u1 == u2 && matches!(u1.as_str(), "percent" | "permille") =>
2918 {
2919 PrimitiveKind::RatioRange
2920 }
2921 (ast::Value::NumberWithUnit(_, _), ast::Value::NumberWithUnit(_, _)) => {
2922 PrimitiveKind::MeasureRange
2923 }
2924 (left_value, right_value) => {
2925 return Err(format!(
2926 "range endpoints must have the same supported base type, got {} and {}",
2927 parser_value_base_type_display_name(left_value),
2928 parser_value_base_type_display_name(right_value)
2929 ));
2930 }
2931 };
2932 ParentType::Primitive { primitive }
2933 }
2934 };
2935 Ok(parent_type)
2936}
2937
2938impl<'a> TypeResolver<'a> {
2939 pub fn new(
2940 context: &'a Context,
2941 slices: HashMap<SpecVersionKey, Arc<instance_list::VersionSlices>>,
2942 ) -> Self {
2943 TypeResolver {
2944 data_types: Vec::new(),
2945 context,
2946 all_registered_specs: Vec::new(),
2947 specs_with_failed_registration: Vec::new(),
2948 slices,
2949 }
2950 }
2951
2952 fn identity_at(
2954 &self,
2955 repository: &LemmaRepository,
2956 spec: &LemmaSpec,
2957 at: &EffectiveDate,
2958 ) -> SpecIdentity {
2959 self.slices
2960 .get(&SpecVersionKey::of(repository, spec))
2961 .expect("BUG: a spec type resolution reaches is in the graph's dependency order")
2962 .identity_at(repository, spec, at)
2963 }
2964
2965 fn rule_spec_element_list_type(
2968 &self,
2969 spec: &LemmaSpec,
2970 parent: &ParentType,
2971 source: &Source,
2972 at: &EffectiveDate,
2973 ) -> Option<Arc<LemmaType>> {
2974 let (name, spec_ref) = Self::list_of_spec_name(spec, parent)?;
2975 let (row_repository, row_spec) = match discovery::resolve_spec_ref_or_self(
2976 self.context,
2977 &spec_ref,
2978 self.registered_repository(spec),
2979 spec,
2980 at,
2981 Some(source.clone()),
2982 ) {
2983 Ok(pair) => pair,
2984 Err(_) => return None,
2985 };
2986 let identity = self.identity_at(&row_repository, row_spec, &spec_ref.at(at));
2987 let element = Arc::new(LemmaType::primitive(TypeSpecification::Spec {
2988 spec: identity,
2989 }));
2990 Some(Arc::new(LemmaType {
2991 name: Some(format!("{name} list")),
2992 specifications: TypeSpecification::List {
2993 element,
2994 key: ListKey::Numbered,
2995 help: String::new(),
2996 },
2997 extends: TypeExtends::Primitive,
2998 options: TypeOptions::default(),
2999 }))
3000 }
3001
3002 fn list_of_spec_name<'p>(
3006 spec: &LemmaSpec,
3007 parent: &'p ParentType,
3008 ) -> Option<(&'p str, ast::SpecRef)> {
3009 let ParentType::List { inner } = parent else {
3010 return None;
3011 };
3012 let ParentType::Custom { name } = inner.as_ref() else {
3013 return None;
3014 };
3015 discovery::list_element_spec_ref(spec, inner).map(|spec_ref| (name.as_str(), spec_ref))
3016 }
3017
3018 pub fn is_registered(&self, spec: &LemmaSpec) -> bool {
3019 self.all_registered_specs
3020 .iter()
3021 .any(|(_, s)| discovery::same_loaded_spec(s, spec))
3022 }
3023
3024 fn registered_repository(&self, spec: &LemmaSpec) -> &Arc<LemmaRepository> {
3026 self.all_registered_specs
3027 .iter()
3028 .find(|(_, registered)| discovery::same_loaded_spec(registered, spec))
3029 .map(|(repository, _)| repository)
3030 .expect("BUG: types are resolved only for specs registered with their repository")
3031 }
3032
3033 fn mark_registration_failure(&mut self, spec: &'a LemmaSpec) {
3035 if !self
3036 .specs_with_failed_registration
3037 .iter()
3038 .any(|s| discovery::same_loaded_spec(s, spec))
3039 {
3040 self.specs_with_failed_registration.push(spec);
3041 }
3042 }
3043
3044 pub fn registration_failed(&self, spec: &LemmaSpec) -> bool {
3049 self.specs_with_failed_registration
3050 .iter()
3051 .any(|s| discovery::same_loaded_spec(s, spec))
3052 }
3053
3054 pub fn register_all(
3056 &mut self,
3057 repository: &Arc<LemmaRepository>,
3058 spec: &'a LemmaSpec,
3059 ) -> Vec<Error> {
3060 self.record_repository(repository, spec);
3061
3062 let mut errors = Vec::new();
3063 for data in &spec.data {
3064 match &data.value {
3065 ParsedDataValue::Definition {
3066 base,
3067 constraints,
3068 value,
3069 list: _,
3070 } => {
3071 if matches!(
3072 (base.as_ref(), constraints.as_ref(), value.as_ref()),
3073 (None, None, Some(Value::NumberWithUnit(_, _)),)
3074 ) {
3075 continue;
3076 }
3077 let name = &data.reference.name;
3078 let parent = match (base.as_ref(), value.as_ref()) {
3079 (Some(b), _) => b.clone(),
3080 (None, Some(v)) => match inferred_parent_type_from_literal(v) {
3081 Ok(parent) => parent,
3082 Err(message) => {
3083 errors.push(Error::validation_with_context(
3084 message,
3085 Some(data.source_location.clone()),
3086 None::<String>,
3087 Some(spec),
3088 None,
3089 ));
3090 self.mark_registration_failure(spec);
3091 continue;
3092 }
3093 },
3094 (None, None) => {
3095 errors.push(Error::validation_with_context(
3096 format!(
3097 "Data '{name}' in spec '{}' must declare a type or a literal value",
3098 spec.name
3099 ),
3100 Some(data.source_location.clone()),
3101 None::<String>,
3102 Some(spec),
3103 None,
3104 ));
3105 self.mark_registration_failure(spec);
3106 continue;
3107 }
3108 };
3109 if Self::list_of_spec_name(spec, &parent).is_some() {
3112 continue;
3113 }
3114 let ftd = DataTypeDef {
3115 parent,
3116 constraints: constraints.clone(),
3117 source: data.source_location.clone(),
3118 name: name.clone(),
3119 bound_literal: value.clone(),
3120 };
3121 if let Err(e) = self.register_type(repository, spec, ftd) {
3122 errors.push(e);
3123 }
3124 }
3125 ParsedDataValue::Import { .. } | ParsedDataValue::ImportList { .. } => {}
3126 }
3127 }
3128 errors
3129 }
3130
3131 fn record_repository(&mut self, repository: &Arc<LemmaRepository>, spec: &'a LemmaSpec) {
3132 if !self
3133 .all_registered_specs
3134 .iter()
3135 .any(|(_, s)| discovery::same_loaded_spec(s, spec))
3136 {
3137 self.all_registered_specs
3138 .push((Arc::clone(repository), spec));
3139 }
3140 }
3141
3142 pub fn register_type(
3144 &mut self,
3145 repository: &Arc<LemmaRepository>,
3146 spec: &'a LemmaSpec,
3147 def: DataTypeDef,
3148 ) -> Result<(), Error> {
3149 self.record_repository(repository, spec);
3150 let spec_types = if let Some(pos) = self
3151 .data_types
3152 .iter()
3153 .position(|(s, _)| discovery::same_loaded_spec(s, spec))
3154 {
3155 &mut self.data_types[pos].1
3156 } else {
3157 self.data_types.push((spec, HashMap::new()));
3158 let last = self.data_types.len() - 1;
3159 &mut self.data_types[last].1
3160 };
3161 if spec_types.contains_key(&def.name) {
3162 return Err(Error::validation_with_context(
3163 format!(
3164 "The name '{}' is already used for data in this spec.",
3165 def.name
3166 ),
3167 Some(def.source.clone()),
3168 None::<String>,
3169 Some(spec),
3170 None,
3171 ));
3172 }
3173 spec_types.insert(def.name.clone(), def);
3174 Ok(())
3175 }
3176
3177 pub fn resolve_and_validate(
3180 &self,
3181 spec: &LemmaSpec,
3182 at: &EffectiveDate,
3183 already_resolved: &ResolvedTypesMap,
3184 ) -> Result<ResolvedSpecTypes, Vec<Error>> {
3185 let (mut resolved_types, list_type_names) =
3186 self.resolve_types_internal(spec, at, already_resolved)?;
3187 resolved_types.source_defaults = resolved_types
3188 .raw_suggestions
3189 .iter()
3190 .map(|(name, raw)| (name.clone(), raw.clone()))
3191 .collect();
3192 resolved_types.source_value_defaults = resolved_types
3193 .raw_values
3194 .iter()
3195 .map(|(name, raw)| (name.clone(), raw.clone()))
3196 .collect();
3197 let mut errors = Vec::new();
3198
3199 let type_sources: std::collections::HashMap<String, Source> = resolved_types
3201 .resolved
3202 .keys()
3203 .chain(list_type_names.iter())
3204 .filter_map(|type_name| {
3205 self.data_types
3206 .iter()
3207 .find(|(s, _)| discovery::same_loaded_spec(s, spec))
3208 .and_then(|(_, defs)| defs.get(type_name.as_str()))
3209 .map(|ftd| (type_name.clone(), ftd.source.clone()))
3210 })
3211 .collect();
3212
3213 errors.extend(rewrite_unit_path_scale_payloads(
3215 self,
3216 spec,
3217 at,
3218 already_resolved,
3219 &mut resolved_types.resolved,
3220 &resolved_types.planning_unit_index,
3221 &type_sources,
3222 ));
3223 let this_spec = SpecVersionKey::of(self.registered_repository(spec), spec);
3224 let (new_resolved, decomp_errors) = resolve_measure_decompositions(
3225 &spec.name,
3226 &this_spec,
3227 std::mem::take(&mut resolved_types.resolved),
3228 &resolved_types.planning_unit_index,
3229 &type_sources,
3230 );
3231 resolved_types.resolved = new_resolved;
3232 errors.extend(decomp_errors);
3233
3234 for (type_name, raw) in std::mem::take(&mut resolved_types.raw_suggestions) {
3235 let lemma_type = resolved_types
3236 .resolved
3237 .get(&type_name)
3238 .expect("BUG: raw default for type not in resolved");
3239 match bound_value_kind_from_raw_suggestion(raw, &lemma_type.specifications, &type_name)
3240 {
3241 Ok(value_kind) => {
3242 resolved_types
3243 .declared_suggestions
3244 .insert(type_name, value_kind);
3245 }
3246 Err(message) => {
3247 let source = type_sources
3248 .get(&type_name)
3249 .cloned()
3250 .unwrap_or_else(|| unreachable!("BUG: type '{}' has no source", type_name));
3251 errors.push(Error::validation_with_context(
3252 message,
3253 Some(source),
3254 None::<String>,
3255 Some(spec),
3256 None,
3257 ));
3258 }
3259 }
3260 }
3261
3262 for (type_name, raw) in std::mem::take(&mut resolved_types.raw_values) {
3263 let lemma_type = resolved_types
3264 .resolved
3265 .get(&type_name)
3266 .expect("BUG: raw fill for type not in resolved");
3267 match bound_value_kind_from_raw_suggestion(raw, &lemma_type.specifications, &type_name)
3268 {
3269 Ok(value_kind) => {
3270 resolved_types.declared_values.insert(type_name, value_kind);
3271 }
3272 Err(message) => {
3273 let source = type_sources
3274 .get(&type_name)
3275 .cloned()
3276 .unwrap_or_else(|| unreachable!("BUG: type '{}' has no source", type_name));
3277 errors.push(Error::validation_with_context(
3278 message,
3279 Some(source),
3280 None::<String>,
3281 Some(spec),
3282 None,
3283 ));
3284 }
3285 }
3286 }
3287
3288 if let Some((_, data_defs)) = self
3289 .data_types
3290 .iter()
3291 .find(|(s, _)| discovery::same_loaded_spec(s, spec))
3292 {
3293 errors.extend(refresh_named_range_specs(
3294 self,
3295 spec,
3296 data_defs,
3297 &mut resolved_types,
3298 already_resolved,
3299 at,
3300 ));
3301 errors.extend(apply_deferred_named_range_constraints(
3302 spec,
3303 data_defs,
3304 &mut resolved_types.resolved,
3305 &mut resolved_types.declared_suggestions,
3306 &mut resolved_types.declared_values,
3307 &type_sources,
3308 &resolved_types.planning_unit_index,
3309 ));
3310 }
3311
3312 let (new_resolved, resolved_errors) = finalize_measure_magnitudes_in_resolved(
3313 std::mem::take(&mut resolved_types.resolved),
3314 &resolved_types.declared_suggestions,
3315 &type_sources,
3316 &spec.name,
3317 spec,
3318 );
3319 resolved_types.resolved = new_resolved;
3320
3321 resolved_types.unit_index = std::mem::take(&mut resolved_types.planning_unit_index)
3322 .bind(&resolved_types.resolved, &this_spec);
3323
3324 match build_signature_index(&spec.name, &resolved_types.unit_index, already_resolved) {
3325 Ok(built) => {
3326 resolved_types.signature_index = built.index;
3327 resolved_types.deferred_signature_collisions = built.deferred_cross_type;
3328 }
3329 Err(error) => errors.push(error),
3330 }
3331
3332 if !resolved_errors.is_empty() {
3333 errors.extend(resolved_errors);
3334 return Err(errors);
3335 }
3336
3337 if let Err(list_errors) = self.resolve_list_types(
3338 spec,
3339 at,
3340 already_resolved,
3341 &list_type_names,
3342 &mut resolved_types,
3343 ) {
3344 errors.extend(list_errors);
3345 return Err(errors);
3346 }
3347
3348 if let Err(rule_list_errors) =
3349 self.resolve_rule_list_types(spec, at, already_resolved, &mut resolved_types)
3350 {
3351 errors.extend(rule_list_errors);
3352 return Err(errors);
3353 }
3354
3355 let mut validated_in_unit_index: HashSet<String> = HashSet::new();
3356 for lemma_type in resolved_types.unit_index.values() {
3357 let Some(measure_family) = lemma_type.measure_family_name() else {
3358 continue;
3359 };
3360 if !lemma_type.is_measure()
3361 || !validated_in_unit_index.insert(measure_family.to_string())
3362 {
3363 continue;
3364 }
3365 let type_name = lemma_type
3366 .name
3367 .as_deref()
3368 .filter(|name| *name == measure_family)
3369 .unwrap_or(measure_family);
3370 let source = type_sources
3371 .get(type_name)
3372 .cloned()
3373 .or_else(|| {
3374 self.data_types
3375 .iter()
3376 .find_map(|(_, defs)| defs.get(type_name).map(|def| def.source.clone()))
3377 })
3378 .unwrap_or_else(|| {
3379 unreachable!(
3380 "BUG: measure type '{}' in unit_index has no DataTypeDef source",
3381 type_name
3382 )
3383 });
3384 errors.extend(validate_type_specifications(
3385 &lemma_type.specifications,
3386 resolved_types.declared_suggestions.get(type_name),
3387 "suggestion",
3388 type_name,
3389 &source,
3390 Some(spec),
3391 &resolved_types.unit_index,
3392 ));
3393 errors.extend(validate_type_specifications(
3394 &lemma_type.specifications,
3395 resolved_types.declared_values.get(type_name),
3396 "with",
3397 type_name,
3398 &source,
3399 Some(spec),
3400 &resolved_types.unit_index,
3401 ));
3402 }
3403
3404 for (type_name, lemma_type) in &resolved_types.resolved {
3405 let source = type_sources.get(type_name).cloned().unwrap_or_else(|| {
3406 unreachable!(
3407 "BUG: resolved type '{}' has no corresponding DataTypeDef in spec '{}'",
3408 type_name, spec.name
3409 )
3410 });
3411 let mut spec_errors = validate_type_specifications(
3412 &lemma_type.specifications,
3413 resolved_types.declared_suggestions.get(type_name),
3414 "suggestion",
3415 type_name,
3416 &source,
3417 Some(spec),
3418 &resolved_types.unit_index,
3419 );
3420 errors.append(&mut spec_errors);
3421 let mut fill_errors = validate_type_specifications(
3422 &lemma_type.specifications,
3423 resolved_types.declared_values.get(type_name),
3424 "with",
3425 type_name,
3426 &source,
3427 Some(spec),
3428 &resolved_types.unit_index,
3429 );
3430 errors.append(&mut fill_errors);
3431 errors.extend(validate_type_options(
3432 lemma_type,
3433 resolved_types.declared_suggestions.get(type_name),
3434 "suggestion",
3435 type_name,
3436 &source,
3437 Some(spec),
3438 &resolved_types.unit_index,
3439 ));
3440 errors.extend(validate_type_options(
3441 lemma_type,
3442 resolved_types.declared_values.get(type_name),
3443 "with",
3444 type_name,
3445 &source,
3446 Some(spec),
3447 &resolved_types.unit_index,
3448 ));
3449 }
3450
3451 for (type_name, lemma_type) in &resolved_types.resolved {
3452 let is_range = matches!(
3453 &lemma_type.specifications,
3454 TypeSpecification::NumberRange { .. }
3455 | TypeSpecification::DateRange { .. }
3456 | TypeSpecification::TimeRange { .. }
3457 | TypeSpecification::MeasureRange { .. }
3458 | TypeSpecification::RatioRange { .. }
3459 );
3460 if !is_range {
3461 continue;
3462 }
3463 let source = type_sources.get(type_name).cloned().unwrap_or_else(|| {
3464 unreachable!(
3465 "BUG: resolved type '{}' has no corresponding DataTypeDef in spec '{}'",
3466 type_name, spec.name
3467 )
3468 });
3469 if let Some(default_kind) = resolved_types.declared_suggestions.get(type_name.as_str())
3470 {
3471 let lit = TypedLiteral {
3472 value: default_kind.value.clone(),
3473 lemma_type: Arc::clone(lemma_type),
3474 };
3475 if let Err(message) = crate::planning::execution_plan::validate_value_against_type(
3476 lemma_type.as_ref(),
3477 &lit.to_literal(),
3478 &resolved_types.unit_index,
3479 None,
3480 ) {
3481 errors.push(Error::validation_with_context(
3482 format!("Type '{}' suggestion is invalid: {}", type_name, message),
3483 Some(source.clone()),
3484 None::<String>,
3485 Some(spec),
3486 None,
3487 ));
3488 }
3489 }
3490 if let Some(default_kind) = resolved_types.declared_values.get(type_name.as_str()) {
3491 let lit = TypedLiteral {
3492 value: default_kind.value.clone(),
3493 lemma_type: Arc::clone(lemma_type),
3494 };
3495 if let Err(message) = crate::planning::execution_plan::validate_value_against_type(
3496 lemma_type.as_ref(),
3497 &lit.to_literal(),
3498 &resolved_types.unit_index,
3499 None,
3500 ) {
3501 errors.push(Error::validation_with_context(
3502 format!("Type '{}' with is invalid: {}", type_name, message),
3503 Some(source),
3504 None::<String>,
3505 Some(spec),
3506 None,
3507 ));
3508 }
3509 }
3510 }
3511
3512 if errors.is_empty() {
3513 Ok(resolved_types)
3514 } else {
3515 Err(errors)
3516 }
3517 }
3518
3519 fn resolve_list_types(
3525 &self,
3526 spec: &LemmaSpec,
3527 at: &EffectiveDate,
3528 already_resolved: &ResolvedTypesMap,
3529 list_type_names: &[String],
3530 resolved_types: &mut ResolvedSpecTypes,
3531 ) -> Result<(), Vec<Error>> {
3532 if list_type_names.is_empty() {
3533 return Ok(());
3534 }
3535 let data_defs = self
3536 .data_types
3537 .iter()
3538 .find(|(s, _)| discovery::same_loaded_spec(s, spec))
3539 .map(|(_, defs)| defs)
3540 .expect("BUG: list type names come from this spec's type definitions");
3541 let mut local = local_type_entries(resolved_types);
3542 let mut errors = Vec::new();
3543 for type_name in list_type_names {
3544 let ftd = data_defs
3545 .get(type_name.as_str())
3546 .expect("BUG: topo-sorted list type missing from registry");
3547 let (lemma_type, suggestion, value) =
3548 self.resolve_type_def(spec, type_name, ftd, at, already_resolved, &local)?;
3549 let to_error = |message: String| {
3550 Error::validation_with_context(
3551 message,
3552 Some(ftd.source.clone()),
3553 None::<String>,
3554 Some(spec),
3555 None,
3556 )
3557 };
3558 if let Some(raw) = &suggestion {
3559 resolved_types
3560 .source_defaults
3561 .insert(type_name.clone(), raw.clone());
3562 match bound_value_kind_from_raw_suggestion(
3563 raw.clone(),
3564 &lemma_type.specifications,
3565 type_name,
3566 ) {
3567 Ok(bound) => {
3568 resolved_types
3569 .declared_suggestions
3570 .insert(type_name.clone(), bound);
3571 }
3572 Err(message) => errors.push(to_error(message)),
3573 }
3574 }
3575 if let Some(raw) = &value {
3576 resolved_types
3577 .source_value_defaults
3578 .insert(type_name.clone(), raw.clone());
3579 match bound_value_kind_from_raw_suggestion(
3580 raw.clone(),
3581 &lemma_type.specifications,
3582 type_name,
3583 ) {
3584 Ok(bound) => {
3585 resolved_types
3586 .declared_values
3587 .insert(type_name.clone(), bound);
3588 }
3589 Err(message) => errors.push(to_error(message)),
3590 }
3591 }
3592 resolved_types
3593 .resolved
3594 .insert(type_name.clone(), Arc::clone(&lemma_type));
3595 local.insert(type_name.clone(), (lemma_type, suggestion, value));
3596 }
3597 if errors.is_empty() {
3598 Ok(())
3599 } else {
3600 Err(errors)
3601 }
3602 }
3603
3604 fn resolve_rule_list_types<'s>(
3607 &self,
3608 spec: &'s LemmaSpec,
3609 at: &EffectiveDate,
3610 already_resolved: &ResolvedTypesMap,
3611 resolved_types: &mut ResolvedSpecTypes,
3612 ) -> Result<(), Vec<Error>> {
3613 let mut written: Vec<(&'s ParentType, &'s Source)> = Vec::new();
3614 for rule in &spec.rules {
3615 let trees = std::iter::once(&rule.expression).chain(
3616 rule.unless_clauses
3617 .iter()
3618 .flat_map(|clause| [&clause.condition, &clause.result]),
3619 );
3620 for tree in trees {
3621 let mut pending = vec![tree.root()];
3622 while let Some(node) = pending.pop() {
3623 if let ast::ExpressionKind::ListType { element } = node.kind() {
3624 if written.iter().all(|(seen, _)| *seen != element) {
3625 let source = node
3626 .source()
3627 .expect("BUG: AST expression missing source location");
3628 written.push((element, source));
3629 }
3630 }
3631 node.push_children(&mut pending);
3632 }
3633 }
3634 }
3635 if written.is_empty() {
3636 return Ok(());
3637 }
3638 let local = local_type_entries(resolved_types);
3639 let mut errors = Vec::new();
3640 for (element, source) in written {
3641 let scope = ParentLookup {
3642 resolver: self,
3643 spec,
3644 source,
3645 at,
3646 already_resolved,
3647 resolved: &local,
3648 site: TypeUseSite::RuleListType,
3649 };
3650 match lookup_parent_type(&scope, element) {
3651 Ok((.., lemma_type)) => {
3652 resolved_types
3653 .rule_list_types
3654 .push((element.clone(), lemma_type));
3655 }
3656 Err(lookup_errors) => {
3657 if let Some(list_type) =
3658 self.rule_spec_element_list_type(spec, element, source, at)
3659 {
3660 resolved_types
3661 .rule_list_types
3662 .push((element.clone(), list_type));
3663 } else {
3664 errors.extend(lookup_errors);
3665 }
3666 }
3667 }
3668 }
3669 if errors.is_empty() {
3670 Ok(())
3671 } else {
3672 Err(errors)
3673 }
3674 }
3675
3676 fn resolve_type_def(
3678 &self,
3679 spec: &LemmaSpec,
3680 type_name: &str,
3681 ftd: &DataTypeDef,
3682 at: &EffectiveDate,
3683 already_resolved: &ResolvedTypesMap,
3684 resolved: &HashMap<String, ResolvedTypeEntry>,
3685 ) -> Result<ResolvedTypeEntry, Vec<Error>> {
3686 let parent = &ftd.parent;
3687 let constraints = &ftd.constraints;
3688
3689 let scope = ParentLookup {
3690 resolver: self,
3691 spec,
3692 source: &ftd.source,
3693 at,
3694 already_resolved,
3695 resolved,
3696 site: TypeUseSite::DataDefinition,
3697 };
3698 let (parent_specs, parent_suggestion, parent_value, parent_type) =
3699 lookup_parent_type(&scope, parent)?;
3700
3701 let mut declared_suggestion = parent_suggestion;
3702 let mut declared_value = parent_value;
3703 let mut final_type = (*parent_type).clone();
3704 final_type.specifications = parent_specs;
3705 if !should_defer_ranged_constraints(parent) {
3706 if let Some(constraints) = &constraints {
3707 let resolve_unit = UnitTokenResolver::Search(
3708 already_resolved
3709 .iter()
3710 .map(|(_, _, rts)| (&rts.unit_index, &rts.resolved))
3711 .collect(),
3712 );
3713 apply_constraints_to_spec(
3714 spec,
3715 &constraint_application_type_name(parent, type_name),
3716 type_name,
3717 &mut final_type,
3718 constraints,
3719 &ftd.source,
3720 ConstraintSuggestionSink {
3721 suggestion: &mut declared_suggestion,
3722 value: &mut declared_value,
3723 resolve_unit: Some(&resolve_unit),
3724 },
3725 )?;
3726 }
3727 }
3728 let final_specs = final_type.specifications;
3729 let final_options = final_type.options;
3730
3731 if matches!(&final_specs, TypeSpecification::List { .. }) && declared_value.is_none() {
3732 declared_value = Some(RawSuggestion::List(Vec::new()));
3733 }
3734
3735 let this_spec = SpecVersionKey::of(self.registered_repository(spec), spec);
3736 let family = match (element_parent_type(parent), parent_type.family()) {
3737 (ParentType::Primitive { .. }, _) | (_, None) => {
3738 TypeFamily::declared_in(&this_spec, type_name)
3739 }
3740 (_, Some(parent_family)) => parent_family.clone(),
3741 };
3742 let defining_spec = match element_parent_type(parent) {
3743 ParentType::Qualified { .. } => TypeDefiningSpec::Import,
3744 _ => TypeDefiningSpec::Local,
3745 };
3746 let extends = TypeExtends::Custom {
3747 parent: parent.to_string(),
3748 family,
3749 defining_spec,
3750 };
3751
3752 let lemma_type = Arc::new(LemmaType {
3753 name: Some(type_name.to_string()),
3754 specifications: final_specs,
3755 extends,
3756 options: final_options,
3757 });
3758
3759 let declared_suggestion = match &ftd.bound_literal {
3760 Some(literal) => {
3761 let resolve_unit = UnitTokenResolver::Search(
3762 already_resolved
3763 .iter()
3764 .map(|(_, _, rts)| (&rts.unit_index, &rts.resolved))
3765 .collect(),
3766 );
3767 match semantics::parser_value_to_value_kind(
3768 literal,
3769 lemma_type.as_ref(),
3770 Some(&resolve_unit),
3771 ) {
3772 Ok(value_kind) => Some(RawSuggestion::Value(value_kind)),
3773 Err(message) => {
3774 return Err(vec![Error::validation_with_context(
3775 message,
3776 Some(ftd.source.clone()),
3777 None::<String>,
3778 Some(spec),
3779 None,
3780 )]);
3781 }
3782 }
3783 }
3784 None => declared_suggestion,
3785 };
3786
3787 Ok((lemma_type, declared_suggestion, declared_value))
3788 }
3789
3790 fn resolve_types_internal(
3793 &self,
3794 spec: &LemmaSpec,
3795 at: &EffectiveDate,
3796 already_resolved: &ResolvedTypesMap,
3797 ) -> Result<(ResolvedSpecTypes, Vec<String>), Vec<Error>> {
3798 let data_defs = self
3799 .data_types
3800 .iter()
3801 .find(|(s, _)| discovery::same_loaded_spec(s, spec))
3802 .map(|(_, defs)| defs)
3803 .cloned()
3804 .unwrap_or_default();
3805
3806 let (scalar_names, list_names) = partition_list_type_names(&data_defs);
3807 let sorted_type_names = topo_sort_type_names(spec, &data_defs, scalar_names)?;
3808 let list_type_names = topo_sort_type_names(spec, &data_defs, list_names)?;
3809
3810 let mut resolved: HashMap<String, ResolvedTypeEntry> = HashMap::new();
3811
3812 for type_name in &sorted_type_names {
3813 let ftd = data_defs
3814 .get(type_name.as_str())
3815 .expect("BUG: topo-sorted type missing from registry");
3816 let entry =
3817 self.resolve_type_def(spec, type_name, ftd, at, already_resolved, &resolved)?;
3818 resolved.insert(type_name.clone(), entry);
3819 }
3820
3821 let mut unit_index = PlanningUnitIndex::new();
3822 let mut errors = Vec::new();
3823 for unit in Self::extract_units_from_type(&semantics::primitive_ratio_arc().specifications)
3824 {
3825 unit_index.insert_owner(unit, PlanningOwner::PrimitiveRatio);
3826 }
3827
3828 let locals: HashMap<String, Arc<LemmaType>> = resolved
3829 .iter()
3830 .map(|(name, (type_arc, _, _))| (name.clone(), Arc::clone(type_arc)))
3831 .collect();
3832
3833 for type_name in &sorted_type_names {
3834 let (type_arc, _, _) = resolved
3835 .get(type_name)
3836 .expect("BUG: type was resolved but missing from map");
3837 let data_type_def = data_defs
3838 .get(type_name.as_str())
3839 .expect("BUG: type was resolved but not in registry");
3840 let merge_result = if type_arc.is_measure() {
3841 if let ParentType::Qualified { spec_alias, inner } = &data_type_def.parent {
3842 match Self::parent_units_for_qualified_extension(
3843 self,
3844 spec,
3845 data_type_def,
3846 already_resolved,
3847 at,
3848 spec_alias,
3849 inner,
3850 ) {
3851 Ok(None) => Ok(()),
3852 Ok(Some(parent_units)) => Self::merge_additive_measure_units(
3853 spec,
3854 &mut unit_index,
3855 type_arc,
3856 data_type_def,
3857 &[],
3858 &parent_units,
3859 &locals,
3860 ),
3861 Err(error) => Err(error),
3862 }
3863 } else {
3864 Self::add_measure_units_to_index(
3865 spec,
3866 &mut unit_index,
3867 type_arc,
3868 data_type_def,
3869 &[],
3870 &locals,
3871 )
3872 }
3873 } else if type_arc.is_ratio() {
3874 Self::add_ratio_units_to_index(
3875 spec,
3876 &mut unit_index,
3877 type_arc,
3878 data_type_def,
3879 &[],
3880 &locals,
3881 )
3882 } else {
3883 Ok(())
3884 };
3885 if let Err(error) = merge_result {
3886 errors.push(error);
3887 }
3888 }
3889
3890 for data_row in &spec.data {
3891 let import_alias = data_row.reference.name.clone();
3892 let mut merge_imported_units =
3893 |import_repository: &Arc<LemmaRepository>,
3894 imported_spec: &LemmaSpec,
3895 imported_resolved: &ResolvedSpecTypes,
3896 prefix_scale_hop: bool| {
3897 let scale_hop = prefix_scale_hop.then(|| {
3902 let identity = SpecIdentity::of(
3903 import_repository,
3904 imported_spec,
3905 imported_spec.effective_from.clone(),
3906 );
3907 PathSegment::import(&import_alias, &identity)
3908 });
3909 for (bare, owner) in imported_resolved.unit_index.iter_owners() {
3910 let planning_owner = match owner {
3911 UnitOwner::PrimitiveRatio => PlanningOwner::PrimitiveRatio,
3912 UnitOwner::Named {
3913 import_path,
3914 type_name,
3915 lemma_type,
3916 } => {
3917 let mut path = vec![import_alias.clone()];
3918 path.extend(import_path.iter().cloned());
3919 let lemma_type = match &scale_hop {
3920 Some(hop) => Arc::new(lemma_type.with_scale_import_hop(hop)),
3921 None => Arc::clone(lemma_type),
3922 };
3923 PlanningOwner::Imported {
3924 import_path: path,
3925 type_name: type_name.clone(),
3926 lemma_type,
3927 }
3928 }
3929 };
3930 unit_index.insert_owner(bare.to_string(), planning_owner);
3931 }
3932 };
3933 match &data_row.value {
3934 ParsedDataValue::Import {
3935 spec_ref,
3936 effective_ref: Some(_),
3937 ..
3938 } => {
3939 let resolved = match discovery::resolve_spec_ref_repository(
3941 self.context,
3942 spec_ref,
3943 Some(&data_row.source_location),
3944 spec.name.as_str(),
3945 Some(spec),
3946 ) {
3947 Ok(resolved) => resolved,
3948 Err(error) => {
3949 errors.push(error);
3950 continue;
3951 }
3952 };
3953 let repository = match &resolved.repository {
3954 Some(explicit) => Arc::clone(explicit),
3955 None => Arc::clone(self.registered_repository(spec)),
3956 };
3957 let dep_name =
3958 crate::parsing::ast::ascii_lowercase_logical_name(spec_ref.name.clone());
3959 for (repo, imported, imported_resolved) in already_resolved {
3960 if !Arc::ptr_eq(repo, &repository) {
3961 continue;
3962 }
3963 if crate::parsing::ast::ascii_lowercase_logical_name(imported.name.clone())
3964 != dep_name
3965 {
3966 continue;
3967 }
3968 if discovery::same_loaded_spec(imported, spec) {
3969 continue;
3970 }
3971 merge_imported_units(repo, imported, imported_resolved, true);
3972 }
3973 }
3974 ParsedDataValue::Import { .. } => {
3975 let Some(spec_ref) = data_row.value.uses_spec_ref() else {
3976 continue;
3977 };
3978 let (import_repository, imported_spec) = match self.resolve_spec_for_import(
3979 spec,
3980 spec_ref,
3981 &data_row.source_location,
3982 at,
3983 ) {
3984 Ok(pair) => pair,
3985 Err(error) => {
3986 errors.push(error);
3987 continue;
3988 }
3989 };
3990 if discovery::same_loaded_spec(imported_spec, spec) {
3991 continue;
3992 }
3993 let Some(imported_resolved) = already_resolved
3994 .iter()
3995 .find(|(_, imported, _)| {
3996 discovery::same_loaded_spec(imported, imported_spec)
3997 })
3998 .map(|(_, _, resolved_spec_types)| resolved_spec_types)
3999 else {
4000 continue;
4001 };
4002 merge_imported_units(
4003 &import_repository,
4004 imported_spec,
4005 imported_resolved,
4006 true,
4007 );
4008 }
4009 ParsedDataValue::Definition { .. } => {
4010 let Some(spec_ref) = discovery::data_instance_list_spec_ref(spec, data_row)
4011 else {
4012 continue;
4013 };
4014 let ParsedDataValue::Definition {
4015 base: Some(parent), ..
4016 } = &data_row.value
4017 else {
4018 continue;
4019 };
4020 if Self::list_of_spec_name(spec, parent).is_none() {
4021 continue;
4022 }
4023 let (import_repository, imported_spec) = match self
4024 .resolve_list_spec_for_import(
4025 spec,
4026 &spec_ref,
4027 &data_row.source_location,
4028 at,
4029 ) {
4030 Ok(pair) => pair,
4031 Err(error) => {
4032 errors.push(error);
4033 continue;
4034 }
4035 };
4036 if discovery::same_loaded_spec(imported_spec, spec) {
4037 continue;
4038 }
4039 let Some(imported_resolved) = already_resolved
4040 .iter()
4041 .find(|(_, imported, _)| {
4042 discovery::same_loaded_spec(imported, imported_spec)
4043 })
4044 .map(|(_, _, resolved_spec_types)| resolved_spec_types)
4045 else {
4046 continue;
4047 };
4048 merge_imported_units(
4049 &import_repository,
4050 imported_spec,
4051 imported_resolved,
4052 false,
4053 );
4054 }
4055 ParsedDataValue::ImportList { .. } => {
4056 panic!(
4057 "BUG: uses list is rejected at parse and must not reach type resolution"
4058 );
4059 }
4060 }
4061 }
4062
4063 if !errors.is_empty() {
4064 return Err(errors);
4065 }
4066
4067 let mut raw_suggestions = Vec::new();
4068 let mut raw_values = Vec::new();
4069 let mut resolved_types = IndexMap::new();
4070 for type_name in &sorted_type_names {
4071 let (lemma_type, default, fill) = resolved
4072 .remove(type_name)
4073 .expect("BUG: every sorted type name must exist in resolved");
4074 if let Some(raw_suggestion) = default {
4075 raw_suggestions.push((type_name.clone(), raw_suggestion));
4076 }
4077 if let Some(raw_value) = fill {
4078 raw_values.push((type_name.clone(), raw_value));
4079 }
4080 resolved_types.insert(type_name.clone(), lemma_type);
4081 }
4082 if !resolved.is_empty() {
4083 panic!(
4084 "BUG: resolved types remain after draining sorted_type_names: {:?}",
4085 resolved.keys().collect::<Vec<_>>()
4086 );
4087 }
4088
4089 Ok((
4090 ResolvedSpecTypes {
4091 resolved: resolved_types,
4092 declared_suggestions: IndexMap::new(),
4093 declared_values: IndexMap::new(),
4094 raw_suggestions,
4095 raw_values,
4096 source_defaults: IndexMap::new(),
4097 source_value_defaults: IndexMap::new(),
4098 rule_list_types: Vec::new(),
4099 unit_index: UnitIndex::new(),
4100 planning_unit_index: unit_index,
4101 signature_index: crate::computation::arithmetic::SignatureIndex::new(),
4102 deferred_signature_collisions: Vec::new(),
4103 },
4104 list_type_names,
4105 ))
4106 }
4107
4108 fn resolve_spec_for_import(
4109 &self,
4110 spec: &LemmaSpec,
4111 from: &crate::parsing::ast::SpecRef,
4112 import_site: &crate::parsing::source::Source,
4113 at: &EffectiveDate,
4114 ) -> Result<(Arc<LemmaRepository>, &'a LemmaSpec), Error> {
4115 discovery::resolve_spec_ref(
4116 self.context,
4117 from,
4118 self.registered_repository(spec),
4119 spec,
4120 at,
4121 Some(import_site.clone()),
4122 )
4123 }
4124
4125 fn resolve_list_spec_for_import(
4127 &self,
4128 spec: &LemmaSpec,
4129 from: &crate::parsing::ast::SpecRef,
4130 import_site: &crate::parsing::source::Source,
4131 at: &EffectiveDate,
4132 ) -> Result<(Arc<LemmaRepository>, &'a LemmaSpec), Error> {
4133 discovery::resolve_spec_ref_or_self(
4134 self.context,
4135 from,
4136 self.registered_repository(spec),
4137 spec,
4138 at,
4139 Some(import_site.clone()),
4140 )
4141 }
4142
4143 fn add_measure_units_to_index(
4148 spec: &LemmaSpec,
4149 unit_index: &mut PlanningUnitIndex,
4150 resolved_type: &Arc<LemmaType>,
4151 defined_by: &DataTypeDef,
4152 import_path: &[String],
4153 locals: &HashMap<String, Arc<LemmaType>>,
4154 ) -> Result<(), Error> {
4155 if matches!(defined_by.parent, ParentType::Qualified { .. }) {
4156 unreachable!(
4157 "BUG: Qualified parents must use parent_units_for_qualified_extension + merge_additive_measure_units"
4158 );
4159 }
4160 let measure_family = resolved_type
4161 .measure_family_name()
4162 .expect("BUG: add_measure_units_to_index requires measure type with family");
4163 for unit in Self::extract_units_from_type(&resolved_type.specifications) {
4164 unit_index
4165 .merge_measure_unit(
4166 unit,
4167 resolved_type,
4168 &defined_by.name,
4169 import_path,
4170 measure_family,
4171 locals,
4172 )
4173 .map_err(|conflict| {
4174 Self::unit_merge_conflict_to_error(conflict, defined_by, spec)
4175 })?;
4176 }
4177 Ok(())
4178 }
4179
4180 fn parent_units_for_qualified_extension(
4182 resolver: &TypeResolver<'_>,
4183 spec: &LemmaSpec,
4184 defined_by: &DataTypeDef,
4185 already_resolved: &ResolvedTypesMap<'_>,
4186 at: &EffectiveDate,
4187 spec_alias: &str,
4188 inner: &ParentType,
4189 ) -> Result<Option<BTreeSet<String>>, Error> {
4190 let ParentType::Custom {
4191 name: parent_type_name,
4192 ..
4193 } = inner
4194 else {
4195 return Ok(None);
4196 };
4197 let Some(spec_ref) = spec.data.iter().find_map(|row| {
4198 if row.reference.segments.is_empty() && row.reference.name == spec_alias {
4199 row.value.uses_spec_ref()
4200 } else {
4201 None
4202 }
4203 }) else {
4204 return Err(Error::validation_with_context(
4205 format!("'{spec_alias}' is not a `uses` import alias in this spec"),
4206 Some(defined_by.source.clone()),
4207 Some(format!(
4208 "Add `uses {spec_alias}: <target_spec>`, then write `{spec_alias}.<type>`."
4209 )),
4210 Some(spec),
4211 None,
4212 ));
4213 };
4214 let (_, target_spec) =
4215 resolver.resolve_spec_for_import(spec, spec_ref, &defined_by.source, at)?;
4216 let Some(imported_resolved) = already_resolved
4217 .iter()
4218 .find(|(_, imported, _)| discovery::same_loaded_spec(imported, target_spec))
4219 .map(|(_, _, types)| types)
4220 else {
4221 return Ok(None);
4222 };
4223 let Some(parent_type) = imported_resolved.resolved.get(parent_type_name.as_str()) else {
4224 return Ok(None);
4225 };
4226 Ok(Some(
4227 Self::extract_units_from_type(&parent_type.specifications)
4228 .into_iter()
4229 .collect(),
4230 ))
4231 }
4232
4233 fn merge_additive_measure_units(
4235 spec: &LemmaSpec,
4236 unit_index: &mut PlanningUnitIndex,
4237 resolved_type: &Arc<LemmaType>,
4238 defined_by: &DataTypeDef,
4239 import_path: &[String],
4240 parent_units: &BTreeSet<String>,
4241 locals: &HashMap<String, Arc<LemmaType>>,
4242 ) -> Result<(), Error> {
4243 let measure_family = resolved_type
4244 .measure_family_name()
4245 .expect("BUG: additive measure registration requires family");
4246 for unit in Self::extract_units_from_type(&resolved_type.specifications) {
4247 if parent_units.contains(&unit) {
4248 continue;
4249 }
4250 unit_index
4251 .merge_measure_unit(
4252 unit,
4253 resolved_type,
4254 &defined_by.name,
4255 import_path,
4256 measure_family,
4257 locals,
4258 )
4259 .map_err(|conflict| {
4260 Self::unit_merge_conflict_to_error(conflict, defined_by, spec)
4261 })?;
4262 }
4263 Ok(())
4264 }
4265
4266 fn add_ratio_units_to_index(
4267 spec: &LemmaSpec,
4268 unit_index: &mut PlanningUnitIndex,
4269 resolved_type: &Arc<LemmaType>,
4270 defined_by: &DataTypeDef,
4271 import_path: &[String],
4272 locals: &HashMap<String, Arc<LemmaType>>,
4273 ) -> Result<(), Error> {
4274 for unit in Self::extract_units_from_type(&resolved_type.specifications) {
4275 unit_index
4276 .merge_ratio_unit(unit, resolved_type, &defined_by.name, import_path, locals)
4277 .map_err(|conflict| {
4278 Self::unit_merge_conflict_to_error(conflict, defined_by, spec)
4279 })?;
4280 }
4281 Ok(())
4282 }
4283
4284 fn unit_merge_conflict_to_error(
4285 conflict: UnitMergeConflict,
4286 defined_by: &DataTypeDef,
4287 spec: &LemmaSpec,
4288 ) -> Error {
4289 let message = match conflict {
4290 UnitMergeConflict::ConflictingFactors { unit, family } => format!(
4291 "Unit '{unit}' has different values on related measure types in '{family}'. \
4292 Use the same unit value on each type, or use different unit names."
4293 ),
4294 };
4295 Error::validation_with_context(
4296 message,
4297 Some(defined_by.source.clone()),
4298 None::<String>,
4299 Some(spec),
4300 None,
4301 )
4302 }
4303
4304 fn extract_units_from_type(specs: &TypeSpecification) -> Vec<String> {
4305 match specs {
4306 TypeSpecification::Measure { units, .. } => {
4307 units.iter().map(|unit| unit.name.clone()).collect()
4308 }
4309 TypeSpecification::Ratio { units, .. } => {
4310 units.iter().map(|unit| unit.name.clone()).collect()
4311 }
4312 _ => Vec::new(),
4313 }
4314 }
4315}
4316
4317pub(crate) fn instance_list_option_errors(
4322 lemma_type: &LemmaType,
4323 raw: &RawSuggestion,
4324 source: &Source,
4325 spec: &LemmaSpec,
4326) -> Vec<Error> {
4327 let owner = match &lemma_type.specifications {
4328 TypeSpecification::List { element, .. } => element.as_ref(),
4329 _ => lemma_type,
4330 };
4331 let mut values = Vec::new();
4332 let mut errors = Vec::new();
4333 if let Err(message) = collect_raw_values(raw, &mut values) {
4334 errors.push(Error::validation_with_context(
4335 message,
4336 Some(source.clone()),
4337 None::<String>,
4338 Some(spec),
4339 None,
4340 ));
4341 return errors;
4342 }
4343 for value in values {
4344 if let Err(message) = owner.reject_disallowed(&value, None) {
4345 errors.push(Error::validation_with_context(
4346 message,
4347 Some(source.clone()),
4348 None::<String>,
4349 Some(spec),
4350 None,
4351 ));
4352 }
4353 }
4354 errors
4355}
4356
4357fn collect_raw_values(raw: &RawSuggestion, out: &mut Vec<ValueKind>) -> Result<(), String> {
4358 let mut pending = vec![raw];
4359 while let Some(current) = pending.pop() {
4360 match current {
4361 RawSuggestion::Value(value) => out.push(value.clone()),
4362 RawSuggestion::List(rows) => {
4363 for row in rows.iter().rev() {
4364 pending.push(row);
4365 }
4366 }
4367 RawSuggestion::Measure { .. } | RawSuggestion::UnitBound { .. } => {
4368 return Err(
4369 "an instance-list option row is a value, not a measure bound still awaiting a unit"
4370 .to_string(),
4371 );
4372 }
4373 }
4374 }
4375 Ok(())
4376}
4377
4378pub(crate) fn validate_type_options(
4379 lemma_type: &LemmaType,
4380 declared_default: Option<&BoundValueKind>,
4381 default_label: &str,
4382 type_name: &str,
4383 source: &Source,
4384 spec_context: Option<&LemmaSpec>,
4385 unit_index: &UnitIndex,
4386) -> Vec<Error> {
4387 let owner = match &lemma_type.specifications {
4388 TypeSpecification::List { element, .. } if !element.options.is_empty() => element.as_ref(),
4389 _ => lemma_type,
4390 };
4391 let mut errors = Vec::new();
4392 let mut bare = owner.clone();
4393 bare.options = TypeOptions::default();
4394 for option in &owner.options.values {
4395 let literal = LiteralValue {
4396 value: option.value.value.clone(),
4397 };
4398 if let Err(message) =
4399 execution_plan::validate_value_against_type(&bare, &literal, unit_index, None)
4400 {
4401 errors.push(Error::validation_with_context(
4402 format!(
4403 "Type '{type_name}' option '{}': {message}",
4404 owner.display_bound(&option.value)
4405 ),
4406 Some(source.clone()),
4407 None::<String>,
4408 spec_context,
4409 None,
4410 ));
4411 }
4412 }
4413 if let Some(default) = declared_default {
4414 if let Err(message) = execution_plan::validate_value_against_type(
4415 lemma_type,
4416 &default.to_literal(),
4417 unit_index,
4418 None,
4419 ) {
4420 errors.push(Error::validation_with_context(
4421 format!("Type '{type_name}' {default_label} value {message}"),
4422 Some(source.clone()),
4423 None::<String>,
4424 spec_context,
4425 None,
4426 ));
4427 }
4428 }
4429 errors
4430}
4431
4432pub fn validate_type_specifications(
4443 specs: &TypeSpecification,
4444 declared_default: Option<&BoundValueKind>,
4445 default_label: &str,
4446 type_name: &str,
4447 source: &Source,
4448 spec_context: Option<&LemmaSpec>,
4449 unit_index: &UnitIndex,
4450) -> Vec<Error> {
4451 let mut errors = Vec::new();
4452 let declared_default = declared_default.map(|bound| &bound.value);
4453
4454 match specs {
4455 TypeSpecification::Measure {
4456 minimum,
4457 maximum,
4458 units,
4459 ..
4460 } => {
4461 if let (Some(min), Some(max)) = (minimum, maximum) {
4463 match (
4464 semantics::measure_declared_bound_to_canonical(
4465 &min.0, &min.1, units, type_name, "minimum",
4466 ),
4467 semantics::measure_declared_bound_to_canonical(
4468 &max.0, &max.1, units, type_name, "maximum",
4469 ),
4470 ) {
4471 (Ok(min_canonical), Ok(max_canonical)) => {
4472 match min_canonical.try_cmp(&max_canonical) {
4473 Ok(std::cmp::Ordering::Greater) => {
4474 errors.push(Error::validation_with_context(
4475 format!(
4476 "Type '{}' has invalid range: minimum {} {} is greater than maximum {} {}",
4477 type_name,
4478 min.0,
4479 min.1,
4480 max.0,
4481 max.1
4482 ),
4483 Some(source.clone()),
4484 None::<String>,
4485 spec_context,
4486 None,
4487 ));
4488 }
4489 Ok(std::cmp::Ordering::Less | std::cmp::Ordering::Equal) => {}
4490 Err(failure) => {
4491 errors.push(Error::validation_with_context(
4492 format!(
4493 "Type '{}' has invalid measure bound: range compare failed: {failure}",
4494 type_name
4495 ),
4496 Some(source.clone()),
4497 None::<String>,
4498 spec_context,
4499 None,
4500 ));
4501 }
4502 }
4503 }
4504 (Err(message), _) | (_, Err(message)) => {
4505 errors.push(Error::validation_with_context(
4506 format!(
4507 "Type '{}' has invalid measure bound: {}",
4508 type_name, message
4509 ),
4510 Some(source.clone()),
4511 None::<String>,
4512 spec_context,
4513 None,
4514 ));
4515 }
4516 }
4517 }
4518
4519 if units.is_empty() {
4523 errors.push(Error::validation_with_context(
4524 format!(
4525 "Type '{}' is a measure type but has no units. Measure types must define at least one unit (e.g. -> unit eur: 1).",
4526 type_name
4527 ),
4528 Some(source.clone()),
4529 None::<String>,
4530 spec_context,
4531 None,
4532 ));
4533 }
4534
4535 if !units.is_empty() {
4537 let mut seen_names: Vec<String> = Vec::new();
4538 for unit in units.iter() {
4539 if unit.name.trim().is_empty() {
4541 errors.push(Error::validation_with_context(
4542 format!(
4543 "Type '{}' has a unit with empty name. Unit names cannot be empty.",
4544 type_name
4545 ),
4546 Some(source.clone()),
4547 None::<String>,
4548 spec_context,
4549 None,
4550 ));
4551 }
4552
4553 if seen_names.contains(&unit.name) {
4555 errors.push(Error::validation_with_context(
4556 format!("Type '{}' has duplicate unit name '{}'. Unit names must be unique within a type.", type_name, unit.name),
4557 Some(source.clone()),
4558 None::<String>,
4559 spec_context,
4560 None,
4561 ));
4562 } else {
4563 seen_names.push(unit.name.clone());
4564 }
4565
4566 if !unit.is_positive_factor() {
4567 let factor = unit.require_literal_factor();
4568 errors.push(Error::validation_with_context(
4569 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()),
4570 Some(source.clone()),
4571 None::<String>,
4572 spec_context,
4573 None,
4574 ));
4575 }
4576 }
4577 }
4578 }
4579 TypeSpecification::Number {
4580 minimum,
4581 maximum,
4582 decimals,
4583 ..
4584 } => {
4585 if let (Some(min), Some(max)) = (minimum, maximum) {
4587 match min.try_cmp(max) {
4588 Ok(std::cmp::Ordering::Greater) => {
4589 errors.push(Error::validation_with_context(
4590 format!(
4591 "Type '{}' has invalid range: minimum {} is greater than maximum {}",
4592 type_name, min, max
4593 ),
4594 Some(source.clone()),
4595 None::<String>,
4596 spec_context,
4597 None,
4598 ));
4599 }
4600 Ok(std::cmp::Ordering::Less | std::cmp::Ordering::Equal) => {}
4601 Err(failure) => {
4602 errors.push(Error::validation_with_context(
4603 format!(
4604 "Type '{}' has invalid range: bound compare failed: {failure}",
4605 type_name
4606 ),
4607 Some(source.clone()),
4608 None::<String>,
4609 spec_context,
4610 None,
4611 ));
4612 }
4613 }
4614 }
4615
4616 if let Some(d) = decimals {
4618 if *d > 28 {
4619 errors.push(Error::validation_with_context(
4620 format!(
4621 "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
4622 type_name, d
4623 ),
4624 Some(source.clone()),
4625 None::<String>,
4626 spec_context,
4627 None,
4628 ));
4629 }
4630 }
4631
4632 if let Some(ValueKind::Number(def)) = declared_default {
4633 if let Some(min) = minimum {
4634 match def.try_cmp(min) {
4635 Ok(std::cmp::Ordering::Less) => {
4636 errors.push(Error::validation_with_context(
4637 format!(
4638 "Type '{}' {default_label} value {} is less than minimum {}",
4639 type_name, def, min
4640 ),
4641 Some(source.clone()),
4642 None::<String>,
4643 spec_context,
4644 None,
4645 ));
4646 }
4647 Ok(std::cmp::Ordering::Equal | std::cmp::Ordering::Greater) => {}
4648 Err(failure) => {
4649 errors.push(Error::validation_with_context(
4650 format!(
4651 "Type '{}' {default_label} bound compare failed: {failure}",
4652 type_name
4653 ),
4654 Some(source.clone()),
4655 None::<String>,
4656 spec_context,
4657 None,
4658 ));
4659 }
4660 }
4661 }
4662 if let Some(max) = maximum {
4663 match def.try_cmp(max) {
4664 Ok(std::cmp::Ordering::Greater) => {
4665 errors.push(Error::validation_with_context(
4666 format!(
4667 "Type '{}' {default_label} value {} is greater than maximum {}",
4668 type_name, def, max
4669 ),
4670 Some(source.clone()),
4671 None::<String>,
4672 spec_context,
4673 None,
4674 ));
4675 }
4676 Ok(std::cmp::Ordering::Less | std::cmp::Ordering::Equal) => {}
4677 Err(failure) => {
4678 errors.push(Error::validation_with_context(
4679 format!(
4680 "Type '{}' {default_label} bound compare failed: {failure}",
4681 type_name
4682 ),
4683 Some(source.clone()),
4684 None::<String>,
4685 spec_context,
4686 None,
4687 ));
4688 }
4689 }
4690 }
4691 }
4692 }
4694
4695 TypeSpecification::Ratio {
4696 minimum,
4697 maximum,
4698 units,
4699 ..
4700 } => {
4701 if let (Some(min), Some(max)) = (minimum, maximum) {
4703 match min.try_cmp(max) {
4704 Ok(std::cmp::Ordering::Greater) => {
4705 errors.push(Error::validation_with_context(
4706 format!(
4707 "Type '{}' has invalid range: minimum {} is greater than maximum {}",
4708 type_name, min, max
4709 ),
4710 Some(source.clone()),
4711 None::<String>,
4712 spec_context,
4713 None,
4714 ));
4715 }
4716 Ok(std::cmp::Ordering::Less | std::cmp::Ordering::Equal) => {}
4717 Err(failure) => {
4718 errors.push(Error::validation_with_context(
4719 format!(
4720 "Type '{}' has invalid range: bound compare failed: {failure}",
4721 type_name
4722 ),
4723 Some(source.clone()),
4724 None::<String>,
4725 spec_context,
4726 None,
4727 ));
4728 }
4729 }
4730 }
4731
4732 if let Some(ValueKind::Ratio(def)) = declared_default {
4733 if let Some(min) = minimum {
4734 match def.try_cmp(min) {
4735 Ok(std::cmp::Ordering::Less) => {
4736 errors.push(Error::validation_with_context(
4737 format!(
4738 "Type '{}' {default_label} value {} is less than minimum {}",
4739 type_name, def, min
4740 ),
4741 Some(source.clone()),
4742 None::<String>,
4743 spec_context,
4744 None,
4745 ));
4746 }
4747 Ok(std::cmp::Ordering::Equal | std::cmp::Ordering::Greater) => {}
4748 Err(failure) => {
4749 errors.push(Error::validation_with_context(
4750 format!(
4751 "Type '{}' {default_label} bound compare failed: {failure}",
4752 type_name
4753 ),
4754 Some(source.clone()),
4755 None::<String>,
4756 spec_context,
4757 None,
4758 ));
4759 }
4760 }
4761 }
4762 if let Some(max) = maximum {
4763 match def.try_cmp(max) {
4764 Ok(std::cmp::Ordering::Greater) => {
4765 errors.push(Error::validation_with_context(
4766 format!(
4767 "Type '{}' {default_label} value {} is greater than maximum {}",
4768 type_name, def, max
4769 ),
4770 Some(source.clone()),
4771 None::<String>,
4772 spec_context,
4773 None,
4774 ));
4775 }
4776 Ok(std::cmp::Ordering::Less | std::cmp::Ordering::Equal) => {}
4777 Err(failure) => {
4778 errors.push(Error::validation_with_context(
4779 format!(
4780 "Type '{}' {default_label} bound compare failed: {failure}",
4781 type_name
4782 ),
4783 Some(source.clone()),
4784 None::<String>,
4785 spec_context,
4786 None,
4787 ));
4788 }
4789 }
4790 }
4791 }
4792
4793 if !units.is_empty() {
4797 let mut seen_names: Vec<String> = Vec::new();
4798 for unit in units.iter() {
4799 if unit.name.trim().is_empty() {
4801 errors.push(Error::validation_with_context(
4802 format!(
4803 "Type '{}' has a unit with empty name. Unit names cannot be empty.",
4804 type_name
4805 ),
4806 Some(source.clone()),
4807 None::<String>,
4808 spec_context,
4809 None,
4810 ));
4811 }
4812
4813 if seen_names.contains(&unit.name) {
4815 errors.push(Error::validation_with_context(
4816 format!("Type '{}' has duplicate unit name '{}'. Unit names must be unique within a type.", type_name, unit.name),
4817 Some(source.clone()),
4818 None::<String>,
4819 spec_context,
4820 None,
4821 ));
4822 } else {
4823 seen_names.push(unit.name.clone());
4824 }
4825
4826 if !unit.value.numer_is_positive() {
4827 errors.push(Error::validation_with_context(
4828 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()),
4829 Some(source.clone()),
4830 None::<String>,
4831 spec_context,
4832 None,
4833 ));
4834 }
4835 }
4836 }
4837 }
4838
4839 TypeSpecification::Text { length, .. } => {
4840 if let Some(ValueKind::Text(def)) = declared_default {
4841 let def_len = def.len();
4842
4843 if let Some(len) = length {
4844 if def_len != *len {
4845 errors.push(Error::validation_with_context(
4846 format!("Type '{}' {default_label} value length {} does not match required length {}", type_name, def_len, len),
4847 Some(source.clone()),
4848 None::<String>,
4849 spec_context,
4850 None,
4851 ));
4852 }
4853 }
4854 }
4855 }
4856
4857 TypeSpecification::Date {
4858 minimum, maximum, ..
4859 } => {
4860 if let (Some(min), Some(max)) = (minimum, maximum) {
4862 let min_sem = semantics::date_time_to_semantic(min);
4863 let max_sem = semantics::date_time_to_semantic(max);
4864 if semantics::compare_semantic_dates(&min_sem, &max_sem) == Ordering::Greater {
4865 errors.push(Error::validation_with_context(
4866 format!(
4867 "Type '{}' has invalid date range: minimum {} is after maximum {}",
4868 type_name, min, max
4869 ),
4870 Some(source.clone()),
4871 None::<String>,
4872 spec_context,
4873 None,
4874 ));
4875 }
4876 }
4877
4878 if let Some(ValueKind::Date(def)) = declared_default {
4879 if let Some(min) = minimum {
4880 let min_sem = semantics::date_time_to_semantic(min);
4881 if semantics::compare_semantic_dates(def, &min_sem) == Ordering::Less {
4882 errors.push(Error::validation_with_context(
4883 format!(
4884 "Type '{}' default date {} is before minimum {}",
4885 type_name, def, min
4886 ),
4887 Some(source.clone()),
4888 None::<String>,
4889 spec_context,
4890 None,
4891 ));
4892 }
4893 }
4894 if let Some(max) = maximum {
4895 let max_sem = semantics::date_time_to_semantic(max);
4896 if semantics::compare_semantic_dates(def, &max_sem) == Ordering::Greater {
4897 errors.push(Error::validation_with_context(
4898 format!(
4899 "Type '{}' default date {} is after maximum {}",
4900 type_name, def, max
4901 ),
4902 Some(source.clone()),
4903 None::<String>,
4904 spec_context,
4905 None,
4906 ));
4907 }
4908 }
4909 }
4910 }
4911
4912 TypeSpecification::Time {
4913 minimum, maximum, ..
4914 } => {
4915 if let (Some(min), Some(max)) = (minimum, maximum) {
4917 let min_sem = semantics::time_to_semantic(min);
4918 let max_sem = semantics::time_to_semantic(max);
4919 if semantics::compare_semantic_times(&min_sem, &max_sem) == Ordering::Greater {
4920 errors.push(Error::validation_with_context(
4921 format!(
4922 "Type '{}' has invalid time range: minimum {} is after maximum {}",
4923 type_name, min, max
4924 ),
4925 Some(source.clone()),
4926 None::<String>,
4927 spec_context,
4928 None,
4929 ));
4930 }
4931 }
4932
4933 if let Some(ValueKind::Time(def)) = declared_default {
4934 if let Some(min) = minimum {
4935 let min_sem = semantics::time_to_semantic(min);
4936 if semantics::compare_semantic_times(def, &min_sem) == Ordering::Less {
4937 errors.push(Error::validation_with_context(
4938 format!(
4939 "Type '{}' default time {} is before minimum {}",
4940 type_name, def, min
4941 ),
4942 Some(source.clone()),
4943 None::<String>,
4944 spec_context,
4945 None,
4946 ));
4947 }
4948 }
4949 if let Some(max) = maximum {
4950 let max_sem = semantics::time_to_semantic(max);
4951 if semantics::compare_semantic_times(def, &max_sem) == Ordering::Greater {
4952 errors.push(Error::validation_with_context(
4953 format!(
4954 "Type '{}' default time {} is after maximum {}",
4955 type_name, def, max
4956 ),
4957 Some(source.clone()),
4958 None::<String>,
4959 spec_context,
4960 None,
4961 ));
4962 }
4963 }
4964 }
4965 }
4966
4967 TypeSpecification::NumberRange { .. }
4968 | TypeSpecification::DateRange { .. }
4969 | TypeSpecification::TimeRange { .. }
4970 | TypeSpecification::MeasureRange { .. }
4971 | TypeSpecification::RatioRange { .. } => {
4972 if let Err(message) = semantics::check_range_bound_consistency(specs, unit_index) {
4973 errors.push(Error::validation_with_context(
4974 format!("Type '{}' has {message}", type_name),
4975 Some(source.clone()),
4976 None::<String>,
4977 spec_context,
4978 None,
4979 ));
4980 }
4981 }
4982 TypeSpecification::Boolean { .. } => {
4983 }
4985 TypeSpecification::Veto { .. } => {
4986 }
4989 TypeSpecification::Undetermined => unreachable!(
4990 "BUG: validate_type_specification_constraints called with Undetermined sentinel type; this type exists only during type inference"
4991 ),
4992 TypeSpecification::Spec { .. } | TypeSpecification::List { .. } => {}
4993 }
4994
4995 errors
4996}
4997
4998#[cfg(test)]
4999mod tests {
5000 use super::*;
5001
5002 mod type_resolution {
5003 use super::*;
5004 use crate::computation::rational::rational_new;
5005 use crate::parsing::ast::{
5006 CommandArg, LemmaSpec, ParentType, PrimitiveKind, TypeConstraintCommand,
5007 };
5008 use crate::parsing::parse;
5009 use crate::planning::typing::{find_unique_measure_type_in_unit_index, DecompositionMatch};
5010 use crate::ResourceLimits;
5011 use rust_decimal::Decimal;
5012 use std::sync::Arc;
5013
5014 fn test_context_and_effective(
5015 specs: &[&LemmaSpec],
5016 ) -> (&'static Context, &'static EffectiveDate) {
5017 use crate::engine::Context;
5018 let mut ctx = Context::new();
5019 let repository = ctx.workspace();
5020 for s in specs {
5021 ctx.insert_spec(Arc::clone(&repository), (*s).clone())
5022 .unwrap();
5023 }
5024 let ctx = Box::leak(Box::new(ctx));
5025 let eff = Box::leak(Box::new(EffectiveDate::Origin));
5026 (ctx, eff)
5027 }
5028
5029 fn context_with_test_spec() -> (&'static Context, &'static LemmaSpec) {
5030 use crate::engine::Context;
5031 let mut ctx = Context::new();
5032 let repository = ctx.workspace();
5033 let spec = LemmaSpec::new("test_spec".to_string());
5034 ctx.insert_spec(Arc::clone(&repository), spec).unwrap();
5035 let ctx: &'static Context = Box::leak(Box::new(ctx));
5036 let repository = ctx.workspace();
5037 let spec = ctx
5038 .spec_set(&repository, "test_spec")
5039 .and_then(|ss| ss.get_exact(None))
5040 .expect("inserted");
5041 (ctx, spec)
5042 }
5043
5044 fn resolver_for_code(code: &str) -> (TypeResolver<'static>, Vec<&'static LemmaSpec>) {
5045 use crate::engine::Context;
5046 let owned = parse(
5047 code,
5048 crate::parsing::source::SourceType::Volatile,
5049 &ResourceLimits::default(),
5050 )
5051 .unwrap()
5052 .into_flattened_specs();
5053 let mut ctx = Context::new();
5054 let repository = ctx.workspace();
5055 for s in owned {
5056 ctx.insert_spec(Arc::clone(&repository), s).unwrap();
5057 }
5058 let ctx = Box::leak(Box::new(ctx));
5059 let repository = ctx.workspace();
5060 let mut spec_refs: Vec<&'static LemmaSpec> = Vec::new();
5061 for ss in ctx.spec_sets_for(&repository) {
5062 for s in ss.iter_specs() {
5063 spec_refs.push(s);
5064 }
5065 }
5066 let mut resolver = TypeResolver::new(ctx, HashMap::new());
5067 for spec in &spec_refs {
5068 resolver.register_all(&repository, spec);
5069 }
5070 (resolver, spec_refs)
5071 }
5072
5073 fn resolver_single_spec(code: &str) -> (TypeResolver<'static>, &LemmaSpec) {
5074 let (resolver, spec_arcs) = resolver_for_code(code);
5075 let spec = spec_arcs.into_iter().next().expect("at least one spec");
5076 (resolver, spec)
5077 }
5078
5079 #[test]
5080 fn test_type_spec_for_primitive_covers_all_variants() {
5081 use crate::parsing::ast::PrimitiveKind;
5082 use crate::planning::semantics::type_spec_for_primitive;
5083
5084 for kind in [
5085 PrimitiveKind::Boolean,
5086 PrimitiveKind::Measure,
5087 PrimitiveKind::MeasureRange,
5088 PrimitiveKind::Number,
5089 PrimitiveKind::NumberRange,
5090 PrimitiveKind::Ratio,
5091 PrimitiveKind::RatioRange,
5092 PrimitiveKind::Text,
5093 PrimitiveKind::Date,
5094 PrimitiveKind::DateRange,
5095 PrimitiveKind::Time,
5096 PrimitiveKind::TimeRange,
5097 ] {
5098 let spec = type_spec_for_primitive(kind);
5099 assert!(
5100 !matches!(
5101 spec,
5102 crate::planning::semantics::TypeSpecification::Undetermined
5103 ),
5104 "type_spec_for_primitive({:?}) returned Undetermined",
5105 kind
5106 );
5107 }
5108 }
5109
5110 #[test]
5111 fn test_register_data_type_def() {
5112 let (ctx, spec) = context_with_test_spec();
5113 let mut resolver = TypeResolver::new(ctx, HashMap::new());
5114 let ftd = DataTypeDef {
5115 parent: ParentType::Primitive {
5116 primitive: PrimitiveKind::Number,
5117 },
5118 constraints: Some(vec![
5119 Constraint::new(
5120 TypeConstraintCommand::Minimum,
5121 vec![CommandArg::Literal(crate::literals::Value::Number(
5122 Decimal::ZERO,
5123 ))],
5124 crate::parsing::source::Source::new(
5125 crate::parsing::source::SourceType::Volatile,
5126 crate::parsing::ast::Span {
5127 start: 0,
5128 end: 0,
5129 line: 1,
5130 col: 0,
5131 },
5132 ),
5133 ),
5134 Constraint::new(
5135 TypeConstraintCommand::Maximum,
5136 vec![CommandArg::Literal(crate::literals::Value::Number(
5137 Decimal::from(150),
5138 ))],
5139 crate::parsing::source::Source::new(
5140 crate::parsing::source::SourceType::Volatile,
5141 crate::parsing::ast::Span {
5142 start: 0,
5143 end: 0,
5144 line: 1,
5145 col: 0,
5146 },
5147 ),
5148 ),
5149 ]),
5150 source: crate::parsing::source::Source::new(
5151 crate::parsing::source::SourceType::Volatile,
5152 crate::parsing::ast::Span {
5153 start: 0,
5154 end: 0,
5155 line: 1,
5156 col: 0,
5157 },
5158 ),
5159 name: "age".to_string(),
5160 bound_literal: None,
5161 };
5162
5163 let result = resolver.register_type(&ctx.workspace(), spec, ftd);
5164 assert!(result.is_ok());
5165 let resolved = resolver
5166 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5167 .unwrap();
5168 assert!(resolved.resolved.contains_key("age"));
5169 }
5170
5171 #[test]
5172 fn test_register_duplicate_type_fails() {
5173 let (ctx, spec) = context_with_test_spec();
5174 let mut resolver = TypeResolver::new(ctx, HashMap::new());
5175 let ftd = DataTypeDef {
5176 parent: ParentType::Primitive {
5177 primitive: PrimitiveKind::Number,
5178 },
5179 constraints: None,
5180 source: crate::parsing::source::Source::new(
5181 crate::parsing::source::SourceType::Volatile,
5182 crate::parsing::ast::Span {
5183 start: 0,
5184 end: 0,
5185 line: 1,
5186 col: 0,
5187 },
5188 ),
5189 name: "money".to_string(),
5190 bound_literal: None,
5191 };
5192 let repository = ctx.workspace();
5193 resolver
5194 .register_type(&repository, spec, ftd.clone())
5195 .unwrap();
5196 let result = resolver.register_type(&repository, spec, ftd);
5197 assert!(result.is_err());
5198 }
5199
5200 #[test]
5201 fn test_resolve_custom_type_from_primitive() {
5202 let (ctx, spec) = context_with_test_spec();
5203 let mut resolver = TypeResolver::new(ctx, HashMap::new());
5204 let ftd = DataTypeDef {
5205 parent: ParentType::Primitive {
5206 primitive: PrimitiveKind::Number,
5207 },
5208 constraints: None,
5209 source: crate::parsing::source::Source::new(
5210 crate::parsing::source::SourceType::Volatile,
5211 crate::parsing::ast::Span {
5212 start: 0,
5213 end: 0,
5214 line: 1,
5215 col: 0,
5216 },
5217 ),
5218 name: "money".to_string(),
5219 bound_literal: None,
5220 };
5221
5222 resolver.register_type(&ctx.workspace(), spec, ftd).unwrap();
5223 let resolved = resolver
5224 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5225 .unwrap();
5226
5227 assert!(resolved.resolved.contains_key("money"));
5228 let money_type = resolved.resolved.get("money").unwrap();
5229 assert_eq!(money_type.name, Some("money".to_string()));
5230 }
5231
5232 #[test]
5233 fn test_child_measure_type_keeps_declared_name_and_child_units() {
5234 let (resolver, spec) = resolver_single_spec(
5235 r#"spec test
5236 data length: measure
5237 -> unit meter: 1
5238 data road_length: length
5239 -> unit kilometer: 1000"#,
5240 );
5241
5242 let resolved_types = resolver
5243 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5244 .unwrap();
5245
5246 let road_length_type = resolved_types.resolved.get("road_length").unwrap();
5247 assert_eq!(road_length_type.name.as_deref(), Some("road_length"));
5248
5249 match &road_length_type.specifications {
5250 TypeSpecification::Measure { units, .. } => {
5251 assert!(units.iter().any(|unit| unit.name == "kilometer"));
5252 }
5253 _ => panic!("Expected Measure type specifications"),
5254 }
5255
5256 let kilometer_owner = resolved_types.unit_index.unique_owner("kilometer").unwrap();
5257 assert_eq!(kilometer_owner.name.as_deref(), Some("road_length"));
5258 assert!(
5259 Arc::ptr_eq(
5260 kilometer_owner,
5261 resolved_types.resolved.get("road_length").unwrap()
5262 ),
5263 "bound kilometer owner must be the resolved road_length Arc"
5264 );
5265 }
5266
5267 #[test]
5268 fn test_type_definition_resolution() {
5269 let (resolver, spec) = resolver_single_spec(
5270 r#"spec test
5271 data dice: number -> minimum 0 -> maximum 6"#,
5272 );
5273
5274 let resolved_types = resolver
5275 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5276 .unwrap();
5277 let dice_type = resolved_types.resolved.get("dice").unwrap();
5278
5279 match &dice_type.specifications {
5280 TypeSpecification::Number {
5281 minimum, maximum, ..
5282 } => {
5283 assert_eq!(minimum, &Some(rational_new(0, 1)));
5284 assert_eq!(maximum, &Some(rational_new(6, 1)));
5285 }
5286 _ => panic!("Expected Number type specifications"),
5287 }
5288 }
5289
5290 #[test]
5291 fn test_type_definition_with_multiple_commands() {
5292 let (resolver, spec) = resolver_single_spec(
5293 r#"spec test
5294 data money: measure -> unit eur: 1.0 -> unit usd: 1.18 -> minimum 0 eur"#,
5295 );
5296
5297 let resolved_types = resolver
5298 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5299 .unwrap();
5300 let money_type = resolved_types.resolved.get("money").unwrap();
5301
5302 match &money_type.specifications {
5303 TypeSpecification::Measure { minimum, units, .. } => {
5304 assert_eq!(minimum.as_ref().map(|(_, unit)| unit.as_str()), Some("eur"));
5305 assert_eq!(units.len(), 2);
5306 assert!(units.iter().any(|u| u.name == "eur"));
5307 assert!(units.iter().any(|u| u.name == "usd"));
5308 }
5309 _ => panic!("Expected Measure type specifications"),
5310 }
5311 }
5312
5313 #[test]
5314 fn test_number_type_with_decimals() {
5315 let (resolver, spec) = resolver_single_spec(
5316 r#"spec test
5317 data price: number -> decimals 2 -> minimum 0"#,
5318 );
5319
5320 let resolved_types = resolver
5321 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5322 .unwrap();
5323 let price_type = resolved_types.resolved.get("price").unwrap();
5324
5325 match &price_type.specifications {
5326 TypeSpecification::Number {
5327 decimals, minimum, ..
5328 } => {
5329 assert_eq!(*decimals, Some(2));
5330 assert_eq!(minimum, &Some(rational_new(0, 1)));
5331 }
5332 _ => panic!("Expected Number type specifications with decimals"),
5333 }
5334 }
5335
5336 #[test]
5337 fn test_number_type_decimals_only() {
5338 let (resolver, spec) = resolver_single_spec(
5339 r#"spec test
5340 data precise_number: number -> decimals 4"#,
5341 );
5342
5343 let resolved_types = resolver
5344 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5345 .unwrap();
5346 let precise_type = resolved_types.resolved.get("precise_number").unwrap();
5347
5348 match &precise_type.specifications {
5349 TypeSpecification::Number { decimals, .. } => {
5350 assert_eq!(*decimals, Some(4));
5351 }
5352 _ => panic!("Expected Number type with decimals 4"),
5353 }
5354 }
5355
5356 #[test]
5357 fn test_measure_type_rejects_decimals() {
5358 let (resolver, spec) = resolver_single_spec(
5359 r#"spec test
5360 data weight: measure -> unit kg: 1 -> decimals 3"#,
5361 );
5362
5363 let errors = resolver
5364 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5365 .expect_err("decimals on a measure must be a planning error");
5366 let messages: Vec<String> = errors.iter().map(|e| e.to_string()).collect();
5367 assert!(
5368 messages.iter().any(|m| m.contains(
5369 "'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'."
5370 )),
5371 "got: {messages:?}"
5372 );
5373 }
5374
5375 #[test]
5376 fn test_ratio_type_rejects_decimals() {
5377 let (resolver, spec) = resolver_single_spec(
5378 r#"spec test
5379 data ratio_type: ratio -> decimals 2"#,
5380 );
5381
5382 let errors = resolver
5383 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5384 .expect_err("decimals on a ratio must be a planning error");
5385 let messages: Vec<String> = errors.iter().map(|e| e.to_string()).collect();
5386 assert!(
5387 messages.iter().any(|m| m.contains(
5388 "'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'."
5389 )),
5390 "got: {messages:?}"
5391 );
5392 }
5393
5394 #[test]
5395 fn typedef_default_inherits_through_extension_chain() {
5396 let (resolver, spec) = resolver_single_spec(
5397 r#"spec test
5398 data money: measure -> unit eur: 1 -> suggest 4 eur
5399 data price: money
5400 data final_price: price"#,
5401 );
5402
5403 let resolved = resolver
5404 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5405 .expect("extension chain must resolve");
5406
5407 assert!(
5408 resolved.declared_suggestions.contains_key("money"),
5409 "base typedef default must convert to ValueKind"
5410 );
5411 assert!(
5412 resolved.declared_suggestions.contains_key("final_price"),
5413 "suggestion must inherit through extension chain (money → price → final_price)"
5414 );
5415 }
5416
5417 #[test]
5418 fn test_ratio_type_with_default_command() {
5419 let (resolver, spec) = resolver_single_spec(
5420 r#"spec test
5421 data percentage: ratio -> minimum 0% -> maximum 100% -> suggest 50%"#,
5422 );
5423
5424 let resolved_types = resolver
5425 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5426 .unwrap();
5427 let percentage_type = resolved_types.resolved.get("percentage").unwrap();
5428
5429 match &percentage_type.specifications {
5430 TypeSpecification::Ratio {
5431 minimum, maximum, ..
5432 } => {
5433 assert_eq!(
5434 *minimum,
5435 Some(rational_new(0, 1)),
5436 "ratio type should have minimum 0"
5437 );
5438 assert_eq!(
5439 *maximum,
5440 Some(rational_new(1, 1)),
5441 "ratio type should have maximum 1"
5442 );
5443 }
5444 _ => panic!("Expected Ratio type with minimum and maximum"),
5445 }
5446
5447 let declared = resolved_types
5448 .declared_suggestions
5449 .get("percentage")
5450 .expect("declared default must be tracked for percentage");
5451 match &declared.value {
5452 ValueKind::Ratio(v) => {
5453 assert_eq!(v, &rational_new(1, 2));
5454 }
5455 other => panic!("expected Ratio declared default, got {:?}", other),
5456 }
5457 }
5458
5459 #[test]
5460 fn test_measure_extension_chain_same_family_units_allowed() {
5461 let (resolver, spec) = resolver_single_spec(
5462 r#"spec test
5463 data money: measure -> unit eur: 1
5464 data money2: money -> unit usd: 1.24"#,
5465 );
5466
5467 let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
5468 assert!(
5469 result.is_ok(),
5470 "Measure extension chain should resolve: {:?}",
5471 result.err()
5472 );
5473
5474 let resolved = result.unwrap();
5475 assert!(
5476 resolved.unit_index.unique_owner("eur").is_some(),
5477 "eur should be in unit_index"
5478 );
5479 assert!(
5480 resolved.unit_index.unique_owner("usd").is_some(),
5481 "usd should be in unit_index"
5482 );
5483 let eur_type = resolved.unit_index.unique_owner("eur").unwrap();
5484 let usd_type = resolved.unit_index.unique_owner("usd").unwrap();
5485 assert_eq!(
5486 eur_type.name.as_deref(),
5487 Some("money"),
5488 "declarer money must own inherited eur, not extension"
5489 );
5490 assert_eq!(
5491 usd_type.name.as_deref(),
5492 Some("money2"),
5493 "usd defined on money2 should be owned by money2"
5494 );
5495 assert!(
5496 Arc::ptr_eq(eur_type, resolved.resolved.get("money").unwrap()),
5497 "bound eur owner must be the resolved money Arc"
5498 );
5499 assert!(
5500 Arc::ptr_eq(usd_type, resolved.resolved.get("money2").unwrap()),
5501 "bound usd owner must be the resolved money2 Arc"
5502 );
5503 }
5504
5505 #[test]
5506 fn test_invalid_parent_type_in_named_type_should_error() {
5507 let (resolver, spec) = resolver_single_spec(
5508 r#"spec test
5509 data invalid: nonexistent_type -> minimum 0"#,
5510 );
5511
5512 let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
5513 assert!(result.is_err(), "Should reject invalid parent type");
5514
5515 let errs = result.unwrap_err();
5516 assert!(!errs.is_empty(), "expected at least one error");
5517 let error_msg = errs[0].to_string();
5518 assert!(
5519 error_msg.contains("Unknown parent") && error_msg.contains("nonexistent_type"),
5520 "Error should mention unknown type. Got: {}",
5521 error_msg
5522 );
5523 }
5524
5525 #[test]
5526 fn test_invalid_primitive_type_name_should_error() {
5527 let (resolver, spec) = resolver_single_spec(
5528 r#"spec test
5529 data invalid: choice -> option "a""#,
5530 );
5531
5532 let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
5533 assert!(result.is_err(), "Should reject invalid type base 'choice'");
5534
5535 let errs = result.unwrap_err();
5536 assert!(!errs.is_empty(), "expected at least one error");
5537 let error_msg = errs[0].to_string();
5538 assert!(
5539 error_msg.contains("Unknown parent") && error_msg.contains("choice"),
5540 "Error should mention unknown type 'choice'. Got: {}",
5541 error_msg
5542 );
5543 }
5544
5545 #[test]
5546 fn extension_unit_factor_override_errors() {
5547 let (resolver, spec) = resolver_single_spec(
5548 r#"spec test
5549 data money: measure
5550 -> unit eur: 1
5551 data money2: money
5552 -> unit eur: 1.10"#,
5553 );
5554
5555 let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
5556 assert!(
5557 result.is_err(),
5558 "extension child must not override inherited unit factor"
5559 );
5560 let error_msg = result
5561 .unwrap_err()
5562 .iter()
5563 .map(ToString::to_string)
5564 .collect::<Vec<_>>()
5565 .join("; ");
5566 assert!(
5567 error_msg.contains("eur"),
5568 "error must name unit eur, got: {error_msg}"
5569 );
5570 assert!(
5571 error_msg.contains("inherited") || error_msg.contains("cannot change"),
5572 "error must reject inherited unit redefinition, got: {error_msg}"
5573 );
5574 }
5575
5576 #[test]
5577 fn inherited_unit_idempotent_redeclare_ok() {
5578 let (resolver, spec) = resolver_single_spec(
5579 r#"spec test
5580 data money: measure
5581 -> unit eur: 1
5582 data money2: money
5583 -> unit eur: 1"#,
5584 );
5585
5586 let resolved = resolver
5587 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5588 .expect("idempotent inherited unit redeclare must resolve");
5589 let money2 = resolved.resolved.get("money2").expect("money2");
5590 match &money2.specifications {
5591 TypeSpecification::Measure { units, .. } => {
5592 let eur = units.iter().find(|u| u.name == "eur").expect("eur");
5593 assert_eq!(
5594 eur.require_literal_factor().try_to_decimal().unwrap(),
5595 Decimal::ONE,
5596 "eur factor must remain 1"
5597 );
5598 }
5599 other => panic!("expected Measure, got {other:?}"),
5600 }
5601 }
5602
5603 #[test]
5604 fn extension_additive_unit_registers_in_unit_index() {
5605 let (resolver, spec) = resolver_single_spec(
5606 r#"spec test
5607 data money: measure
5608 -> unit eur: 1
5609 data money2: money
5610 -> unit usd: 1.24"#,
5611 );
5612
5613 let resolved = resolver
5614 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5615 .expect("additive extension unit must resolve");
5616 let money = resolved
5617 .resolved
5618 .get("money")
5619 .expect("money resolved")
5620 .clone();
5621 let money2 = resolved
5622 .resolved
5623 .get("money2")
5624 .expect("money2 resolved")
5625 .clone();
5626 assert!(
5627 resolved.unit_index.unique_owner("eur").is_some(),
5628 "eur must be in unit_index"
5629 );
5630 assert!(
5631 resolved.unit_index.unique_owner("usd").is_some(),
5632 "usd must be in unit_index"
5633 );
5634 let eur_owner = resolved.unit_index.unique_owner("eur").expect("eur owner");
5635 let usd_owner = resolved.unit_index.unique_owner("usd").expect("usd owner");
5636 assert_eq!(
5637 eur_owner.name.as_deref(),
5638 Some("money"),
5639 "declarer money must own eur, not extension money2"
5640 );
5641 assert_eq!(
5642 usd_owner.name.as_deref(),
5643 Some("money2"),
5644 "money2 must own new unit usd"
5645 );
5646 assert_eq!(eur_owner.as_ref(), money.as_ref());
5647 assert_eq!(usd_owner.as_ref(), money2.as_ref());
5648 }
5649
5650 #[test]
5651 fn unit_index_maps_family_root_not_binding_alias() {
5652 let code = r#"spec units
5653 data money: measure
5654 -> unit eur: 1
5655 data mass: measure
5656 -> unit kg: 1
5657 data price_per_weight: measure
5658 -> unit eur_per_kg: eur/kg
5659
5660 spec consumer
5661 uses u: units
5662 data starch_levy_per_kg: u.price_per_weight
5663 -> suggest 0.1 eur_per_kg
5664 data amortization_per_kg: u.price_per_weight
5665 -> suggest 0.2 eur_per_kg
5666 "#;
5667 let (resolver, spec_arcs) = resolver_for_code(code);
5668 let units_arc = spec_arcs
5669 .iter()
5670 .find(|spec| spec.name == "units")
5671 .cloned()
5672 .expect("units spec");
5673 let consumer_arc = spec_arcs
5674 .iter()
5675 .find(|spec| spec.name == "consumer")
5676 .cloned()
5677 .expect("consumer spec");
5678 let (context, _) = test_context_and_effective(&spec_arcs);
5679 let repository = context.workspace();
5680
5681 let mut already_resolved = ResolvedTypesMap::new();
5682 let units_types = resolver
5683 .resolve_and_validate(units_arc, &EffectiveDate::Origin, &already_resolved)
5684 .expect("units spec resolves");
5685 let units_price_per_weight = units_types
5686 .resolved
5687 .get("price_per_weight")
5688 .expect("units defines price_per_weight")
5689 .clone();
5690 already_resolved.push((Arc::clone(&repository), units_arc, units_types));
5691
5692 let consumer_types = resolver
5693 .resolve_and_validate(consumer_arc, &EffectiveDate::Origin, &already_resolved)
5694 .expect("consumer spec resolves");
5695
5696 let eur_per_kg_owner = consumer_types
5697 .unit_index
5698 .unique_owner("eur_per_kg")
5699 .expect("eur_per_kg in unit_index");
5700 assert_eq!(
5701 eur_per_kg_owner.name.as_deref(),
5702 Some("price_per_weight"),
5703 "unit must belong to family root, not binding alias row name"
5704 );
5705 assert_eq!(
5706 eur_per_kg_owner.measure_family_name(),
5707 Some("price_per_weight")
5708 );
5709 assert_eq!(eur_per_kg_owner.as_ref(), units_price_per_weight.as_ref());
5710
5711 let starch_alias = consumer_types
5712 .resolved
5713 .get("starch_levy_per_kg")
5714 .expect("alias row in resolved");
5715 assert!(
5716 !std::ptr::eq(eur_per_kg_owner.as_ref(), starch_alias.as_ref()),
5717 "unit_index must not store binding alias arc"
5718 );
5719 }
5720
5721 #[test]
5722 fn same_named_local_root_does_not_hide_imported_units() {
5723 let code = r#"spec std_units
5724 data mass: measure
5725 -> unit kilogram: 1
5726 -> unit kilograms: 1
5727 -> unit gram: 0.001
5728
5729 spec s
5730 uses u: std_units
5731 data mass: measure
5732 -> unit kg: 1
5733 -> unit tonne: 1000
5734 rule smoke: true
5735 "#;
5736 let (resolver, spec_arcs) = resolver_for_code(code);
5737 let std_units_arc = spec_arcs
5738 .iter()
5739 .find(|spec| spec.name == "std_units")
5740 .cloned()
5741 .expect("std_units spec");
5742 let consumer_arc = spec_arcs
5743 .iter()
5744 .find(|spec| spec.name == "s")
5745 .cloned()
5746 .expect("consumer spec");
5747 let (context, _) = test_context_and_effective(&spec_arcs);
5748 let repository = context.workspace();
5749
5750 let mut already_resolved = ResolvedTypesMap::new();
5751 let std_units_types = resolver
5752 .resolve_and_validate(std_units_arc, &EffectiveDate::Origin, &already_resolved)
5753 .expect("std_units resolves");
5754 already_resolved.push((Arc::clone(&repository), std_units_arc, std_units_types));
5755
5756 let consumer_types = resolver
5757 .resolve_and_validate(consumer_arc, &EffectiveDate::Origin, &already_resolved)
5758 .expect("consumer resolves");
5759
5760 let local_mass = consumer_types
5761 .resolved
5762 .get("mass")
5763 .expect("consumer defines mass")
5764 .clone();
5765
5766 assert!(
5767 consumer_types.unit_index.unique_owner("kg").is_some(),
5768 "local kg must be indexed"
5769 );
5770 assert!(
5771 consumer_types.unit_index.unique_owner("tonne").is_some(),
5772 "local tonne must be indexed"
5773 );
5774 for imported_unit in ["kilogram", "kilograms", "gram"] {
5775 let owner = consumer_types
5776 .unit_index
5777 .unique_owner(imported_unit)
5778 .unwrap_or_else(|| panic!("imported {imported_unit} must stay in scope"));
5779 assert_eq!(
5780 owner.measure_family().map(|family| family.spec.as_str()),
5781 Some("std_units"),
5782 "{imported_unit} must belong to std_units.mass, not the local mass"
5783 );
5784 }
5785
5786 let kg_owner = consumer_types
5787 .unit_index
5788 .unique_owner("kg")
5789 .expect("kg owner");
5790 let tonne_owner = consumer_types
5791 .unit_index
5792 .unique_owner("tonne")
5793 .expect("tonne owner");
5794 assert_eq!(kg_owner.as_ref(), local_mass.as_ref());
5795 assert_eq!(tonne_owner.as_ref(), local_mass.as_ref());
5796 }
5797
5798 #[test]
5799 fn same_family_same_unit_same_factor_is_idempotent() {
5800 let code = r#"spec units_a
5801 data duration: measure
5802 -> unit hour: 1
5803
5804 spec consumer
5805 uses a: units_a
5806 uses b: units_a
5807 rule smoke: true
5808 "#;
5809 let (resolver, spec_arcs) = resolver_for_code(code);
5810 let units_a_arc = spec_arcs
5811 .iter()
5812 .find(|spec| spec.name == "units_a")
5813 .cloned()
5814 .expect("units_a spec");
5815 let consumer_arc = spec_arcs
5816 .iter()
5817 .find(|spec| spec.name == "consumer")
5818 .cloned()
5819 .expect("consumer spec");
5820 let (context, _) = test_context_and_effective(&spec_arcs);
5821 let repository = context.workspace();
5822
5823 let mut already_resolved = ResolvedTypesMap::new();
5824 let units_a_types = resolver
5825 .resolve_and_validate(units_a_arc, &EffectiveDate::Origin, &already_resolved)
5826 .expect("units_a resolves");
5827 already_resolved.push((Arc::clone(&repository), units_a_arc, units_a_types));
5828
5829 let consumer_types = resolver
5830 .resolve_and_validate(consumer_arc, &EffectiveDate::Origin, &already_resolved)
5831 .expect("double import of same duration must be idempotent");
5832
5833 assert_eq!(
5834 consumer_types.unit_index.owners_for("hour").len(),
5835 2,
5836 "each import path keeps its hour owner"
5837 );
5838 consumer_types
5839 .unit_index
5840 .resolve("hour")
5841 .expect("bare hour collapses across identical import paths");
5842 }
5843
5844 #[test]
5845 fn different_version_families_keep_separate_hour_owners() {
5846 let code = r#"spec units 2024-01-01
5849 data duration: measure
5850 -> unit hour: 1
5851
5852 spec units 2025-01-01
5853 data duration: measure
5854 -> unit hour: 60
5855
5856 spec consumer
5857 uses a: units 2024-01-01
5858 uses b: units 2025-01-01
5859 rule smoke: true
5860 "#;
5861 let (resolver, spec_arcs) = resolver_for_code(code);
5862 let consumer_arc = spec_arcs
5863 .iter()
5864 .find(|spec| spec.name == "consumer")
5865 .cloned()
5866 .expect("consumer spec");
5867 let (context, _) = test_context_and_effective(&spec_arcs);
5868 let repository = context.workspace();
5869
5870 let mut already_resolved = ResolvedTypesMap::new();
5871 for units_arc in spec_arcs.iter().filter(|spec| spec.name == "units") {
5872 let units_types = resolver
5873 .resolve_and_validate(units_arc, &units_arc.effective_from, &already_resolved)
5874 .expect("units version resolves");
5875 already_resolved.push((Arc::clone(&repository), units_arc, units_types));
5876 }
5877
5878 let consumer_types = resolver
5879 .resolve_and_validate(consumer_arc, &EffectiveDate::Origin, &already_resolved)
5880 .expect("different version families resolve without merge conflict");
5881 assert_eq!(
5882 consumer_types.unit_index.owners_for("hour").len(),
5883 2,
5884 "each version keeps its own hour owner"
5885 );
5886 }
5887
5888 #[test]
5889 fn test_spec_level_same_unit_names_keep_every_declarer() {
5890 let (resolver, spec) = resolver_single_spec(
5891 r#"spec test
5892 data money_a: measure
5893 -> unit eur: 1.00
5894 -> unit usd: 0.84
5895
5896 data money_b: measure
5897 -> unit eur: 1.00
5898 -> unit usd: 1.20
5899
5900 data length_a: measure
5901 -> unit meter: 1.0
5902
5903 data length_b: measure
5904 -> unit meter: 1.0"#,
5905 );
5906
5907 let resolved = resolver
5908 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5909 .expect("independent declarers of one unit name must load");
5910 assert_eq!(
5911 resolved.unit_index.owners_for("eur").len(),
5912 2,
5913 "every declarer of 'eur' stays in scope"
5914 );
5915 assert!(
5916 resolved.unit_index.resolve("eur").is_err(),
5917 "bare 'eur' with two declarers must be qualified"
5918 );
5919 assert_eq!(
5920 resolved.unit_index.owners_for("usd").len(),
5921 2,
5922 "every declarer of 'usd' stays in scope"
5923 );
5924 assert!(
5925 resolved.unit_index.resolve("usd").is_err(),
5926 "bare 'usd' with two declarers must be qualified"
5927 );
5928 assert_eq!(
5929 resolved.unit_index.owners_for("meter").len(),
5930 2,
5931 "every declarer of 'meter' stays in scope"
5932 );
5933 assert!(
5934 resolved.unit_index.resolve("meter").is_err(),
5935 "bare 'meter' with two declarers must be qualified"
5936 );
5937 let (_, owner) = resolved
5938 .unit_index
5939 .resolve("money_b.usd")
5940 .expect("qualified unit must resolve");
5941 assert_eq!(owner.name(), "money_b");
5942 }
5943
5944 #[test]
5945 fn test_measure_and_ratio_sharing_a_unit_name_keep_both_declarers() {
5946 let (resolver, spec) = resolver_single_spec(
5947 r#"spec test
5948 data q: measure
5949 -> unit foo: 1
5950
5951 data r: ratio
5952 -> unit foo: 100"#,
5953 );
5954
5955 let resolved = resolver
5956 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5957 .expect("independent declarers of one unit name must load");
5958 assert_eq!(
5959 resolved.unit_index.owners_for("foo").len(),
5960 2,
5961 "every declarer of 'foo' stays in scope"
5962 );
5963 assert!(
5964 resolved.unit_index.resolve("foo").is_err(),
5965 "bare 'foo' with two declarers must be qualified"
5966 );
5967 let (_, owner) = resolved
5968 .unit_index
5969 .resolve("r.foo")
5970 .expect("qualified unit must resolve");
5971 assert_eq!(owner.name(), "r");
5972 }
5973
5974 #[test]
5975 fn test_same_ratio_unit_across_types_keeps_both_declarers() {
5976 let (resolver, spec) = resolver_single_spec(
5977 r#"spec test
5978 data spread_a: ratio
5979 -> unit basis_points: 10000
5980
5981 data spread_b: ratio
5982 -> unit basis_points: 10000"#,
5983 );
5984
5985 let resolved = resolver
5986 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
5987 .expect("independent declarers of one unit name must load");
5988 assert_eq!(
5989 resolved.unit_index.owners_for("basis_points").len(),
5990 2,
5991 "every declarer of 'basis_points' stays in scope"
5992 );
5993 assert!(
5994 resolved.unit_index.resolve("basis_points").is_err(),
5995 "bare 'basis_points' with two declarers must be qualified"
5996 );
5997 let (_, owner) = resolved
5998 .unit_index
5999 .resolve("spread_a.basis_points")
6000 .expect("qualified unit must resolve");
6001 assert_eq!(owner.name(), "spread_a");
6002 }
6003
6004 #[test]
6005 fn test_different_ratio_unit_factor_across_types_keeps_both_declarers() {
6006 let (resolver, spec) = resolver_single_spec(
6007 r#"spec test
6008 data spread_a: ratio
6009 -> unit basis_points: 10000
6010
6011 data spread_b: ratio
6012 -> unit basis_points: 5000"#,
6013 );
6014
6015 let resolved = resolver
6016 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
6017 .expect("independent declarers of one unit name must load");
6018 assert_eq!(
6019 resolved.unit_index.owners_for("basis_points").len(),
6020 2,
6021 "every declarer of 'basis_points' stays in scope"
6022 );
6023 assert!(
6024 resolved.unit_index.resolve("basis_points").is_err(),
6025 "bare 'basis_points' with two declarers must be qualified"
6026 );
6027 let (_, owner) = resolved
6028 .unit_index
6029 .resolve("spread_b.basis_points")
6030 .expect("qualified unit must resolve");
6031 assert_eq!(owner.name(), "spread_b");
6032 }
6033
6034 #[test]
6035 fn test_multiple_builtin_ratio_types_share_percent_in_unit_index() {
6036 let (resolver, spec) = resolver_single_spec(
6037 r#"spec targets
6038 data standard_margin_pct: ratio
6039 -> minimum 0%
6040 -> suggest 15%
6041
6042 data default_credit_insurance_pct: ratio
6043 -> suggest 1.5%"#,
6044 );
6045
6046 resolver
6047 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
6048 .expect("multiple ratio types with built-in percent must not conflict");
6049 }
6050
6051 #[test]
6052 fn test_three_ratio_types_share_builtin_and_custom_unit() {
6053 let (resolver, spec) = resolver_single_spec(
6054 r#"spec test
6055 data margin: ratio -> suggest 10%
6056 data fee: ratio
6057 -> unit tenths: 10
6058 data tax: ratio -> suggest 1%"#,
6059 );
6060
6061 resolver
6062 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
6063 .expect("builtin percent/permille plus custom tenths on second type must load");
6064 }
6065
6066 #[test]
6067 fn test_ratio_unit_index_allows_builtin_after_two_named_types() {
6068 let (resolver, spec) = resolver_single_spec(
6069 r#"spec test
6070 data first_ratio: ratio -> suggest 5%
6071 data second_ratio: ratio -> suggest 10%"#,
6072 );
6073
6074 let resolved = resolver
6075 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
6076 .expect("two ratio types with only builtin units");
6077
6078 let (_, percent) = resolved
6079 .unit_index
6080 .resolve("percent")
6081 .expect("bare percent binds PrimitiveRatio");
6082 assert_eq!(percent.name(), "ratio");
6083 let (_, permille) = resolved
6084 .unit_index
6085 .resolve("permille")
6086 .expect("bare permille binds PrimitiveRatio");
6087 assert_eq!(permille.name(), "ratio");
6088 }
6089
6090 #[test]
6091 fn test_number_type_cannot_have_units() {
6092 let (resolver, spec) = resolver_single_spec(
6093 r#"spec test
6094 data price: number
6095 -> unit eur: 1.00"#,
6096 );
6097
6098 let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
6099 assert!(result.is_err(), "Number types must reject unit commands");
6100
6101 let errs = result.unwrap_err();
6102 assert!(!errs.is_empty(), "expected at least one error");
6103 let error_msg = errs[0].to_string();
6104 assert!(
6105 error_msg.contains("unit") && error_msg.contains("number"),
6106 "Error should mention units are invalid on number. Got: {}",
6107 error_msg
6108 );
6109 }
6110
6111 #[test]
6112 fn test_extending_type_inherits_units() {
6113 let (resolver, spec) = resolver_single_spec(
6114 r#"spec test
6115 data money: measure
6116 -> unit eur: 1.00
6117 -> unit usd: 0.84
6118
6119 data my_money: money
6120 -> unit gbp: 1.30"#,
6121 );
6122
6123 let resolved = resolver
6124 .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
6125 .unwrap();
6126 let my_money_type = resolved.resolved.get("my_money").unwrap();
6127
6128 match &my_money_type.specifications {
6129 TypeSpecification::Measure { units, .. } => {
6130 assert_eq!(units.len(), 3);
6131 assert!(units.iter().any(|u| u.name == "eur"));
6132 assert!(units.iter().any(|u| u.name == "usd"));
6133 assert!(units.iter().any(|u| u.name == "gbp"));
6134 }
6135 other => panic!("Expected Measure type specifications, got {:?}", other),
6136 }
6137 }
6138
6139 #[test]
6140 fn binding_unit_factor_override_errors() {
6141 let (resolver, spec) = resolver_single_spec(
6142 r#"spec test
6143 data source_measure: measure
6144 -> unit usd: 1.00
6145 data z: source_measure
6146 -> unit usd: 0.84"#,
6147 );
6148
6149 let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
6150 assert!(
6151 result.is_err(),
6152 "binding row must not override inherited unit factor"
6153 );
6154 let error_msg = result
6155 .unwrap_err()
6156 .iter()
6157 .map(ToString::to_string)
6158 .collect::<Vec<_>>()
6159 .join("; ");
6160 assert!(
6161 error_msg.contains("usd"),
6162 "error must name unit usd, got: {error_msg}"
6163 );
6164 }
6165
6166 #[test]
6167 fn ratio_extension_unit_factor_override_errors() {
6168 let (resolver, spec) = resolver_single_spec(
6169 r#"spec test
6170 data parent: ratio
6171 -> unit basis: 1
6172 data child: parent
6173 -> unit basis: 100"#,
6174 );
6175
6176 let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
6177 assert!(
6178 result.is_err(),
6179 "ratio extension must not override inherited unit factor"
6180 );
6181 let error_msg = result
6182 .unwrap_err()
6183 .iter()
6184 .map(ToString::to_string)
6185 .collect::<Vec<_>>()
6186 .join("; ");
6187 assert!(
6188 error_msg.contains("basis"),
6189 "error must name unit basis, got: {error_msg}"
6190 );
6191 }
6192
6193 #[test]
6194 fn test_duplicate_unit_name_in_same_type_is_error() {
6195 let (resolver, spec) = resolver_single_spec(
6196 r#"spec test
6197 data money: measure
6198 -> unit eur: 1.00
6199 -> unit eur: 1.19"#,
6200 );
6201
6202 let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
6203 assert!(result.is_err(), "duplicate unit name must be rejected");
6204 let errs = result.unwrap_err();
6205 assert!(!errs.is_empty());
6206 let msg = errs[0].to_string();
6207 assert!(
6208 msg.contains("eur"),
6209 "error must name the duplicate unit; got: {msg}"
6210 );
6211 }
6212
6213 #[test]
6214 fn test_duplicate_unit_name_compound_is_error() {
6215 let (resolver, spec) = resolver_single_spec(
6216 r#"spec test
6217 data currency: measure
6218 -> unit usd: 1.00
6219 -> unit eur: 0.92
6220
6221 data time_period: measure
6222 -> unit year: 1
6223
6224 data run_rate: measure
6225 -> unit arr: usd/year
6226 -> unit arr: eur/year"#,
6227 );
6228
6229 let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
6230 assert!(
6231 result.is_err(),
6232 "duplicate compound unit name must be rejected"
6233 );
6234 let errs = result.unwrap_err();
6235 assert!(!errs.is_empty());
6236 let msg = errs[0].to_string();
6237 assert!(
6238 msg.contains("arr"),
6239 "error must name the duplicate unit; got: {msg}"
6240 );
6241 }
6242
6243 #[test]
6244 fn find_unique_reports_multiple_families_with_same_decomposition() {
6245 let code = r#"spec units
6246 data money: measure
6247 -> unit eur: 1
6248 -> unit usd: 0.86
6249 data mass: measure
6250 -> unit kg: 1
6251 data price_eur_per_kg: measure
6252 -> unit eur_per_kg: eur/kg
6253 data price_usd_per_kg: measure
6254 -> unit usd_per_kg: usd/kg
6255 "#;
6256 let (resolver, spec_arcs) = resolver_for_code(code);
6257 let units_arc = spec_arcs
6258 .iter()
6259 .find(|spec| spec.name == "units")
6260 .cloned()
6261 .expect("units spec");
6262 let (context, _) = test_context_and_effective(&spec_arcs);
6263 let repository = context.workspace();
6264 let resolved = resolver
6265 .resolve_and_validate(units_arc, &EffectiveDate::Origin, &ResolvedTypesMap::new())
6266 .expect("units resolves");
6267 let eur_decomp = resolved
6268 .unit_index
6269 .unique_owner("eur_per_kg")
6270 .expect("eur_per_kg")
6271 .measure_type_decomposition()
6272 .expect("eur_per_kg decomposition")
6273 .clone();
6274 let map = [(repository, units_arc, resolved)];
6275
6276 let units_types = map
6277 .iter()
6278 .find(|(_, s, _)| discovery::same_loaded_spec(s, units_arc))
6279 .map(|(_, _, t)| t)
6280 .expect("units in map");
6281 let unique =
6282 find_unique_measure_type_in_unit_index(&units_types.unit_index, &eur_decomp);
6283 match unique {
6284 DecompositionMatch::Multiple(families) => {
6285 assert_eq!(families.len(), 2);
6286 assert!(families.contains(&"price_eur_per_kg".to_string()));
6287 assert!(families.contains(&"price_usd_per_kg".to_string()));
6288 }
6289 other => panic!("expected Multiple families, got {other:?}"),
6290 }
6291 }
6292 }
6293
6294 mod type_validation {
6295 use super::*;
6296 use crate::computation::rational::rational_new;
6297 use crate::literals::UnitScale;
6298 use crate::parsing::ast::{CommandArg, TypeConstraintCommand};
6299 use crate::planning::semantics::TypeSpecification;
6300 use rust_decimal::Decimal;
6301
6302 fn test_source() -> Source {
6303 Source::new(
6304 crate::parsing::source::SourceType::Volatile,
6305 crate::parsing::ast::Span {
6306 start: 0,
6307 end: 0,
6308 line: 1,
6309 col: 0,
6310 },
6311 )
6312 }
6313
6314 fn apply(
6315 mut specs: TypeSpecification,
6316 command: TypeConstraintCommand,
6317 args: &[CommandArg],
6318 ) -> TypeSpecification {
6319 let mut suggestion: Option<RawSuggestion> = None;
6320 specs
6321 .apply_constraint(
6322 ConstraintNames {
6323 type_name: "test",
6324 data_name: "test",
6325 },
6326 command,
6327 args,
6328 &mut suggestion,
6329 &mut None,
6330 None,
6331 )
6332 .unwrap();
6333 specs
6334 }
6335
6336 fn number_arg(n: i64) -> CommandArg {
6337 CommandArg::Literal(crate::literals::Value::Number(Decimal::from(n)))
6338 }
6339
6340 fn date_arg(s: &str) -> CommandArg {
6341 let dt = s.parse::<crate::literals::DateTimeValue>().expect("date");
6342 CommandArg::Literal(crate::literals::Value::Date(dt))
6343 }
6344
6345 fn time_arg(s: &str) -> CommandArg {
6346 let t = s.parse::<crate::literals::TimeValue>().expect("time");
6347 CommandArg::Literal(crate::literals::Value::Time(t))
6348 }
6349
6350 #[test]
6351 fn validate_number_minimum_greater_than_maximum() {
6352 let mut specs = TypeSpecification::number();
6353 specs = apply(specs, TypeConstraintCommand::Minimum, &[number_arg(100)]);
6354 specs = apply(specs, TypeConstraintCommand::Maximum, &[number_arg(50)]);
6355
6356 let src = test_source();
6357 let errors = validate_type_specifications(
6358 &specs,
6359 None,
6360 "suggestion",
6361 "test",
6362 &src,
6363 None,
6364 &UnitIndex::new(),
6365 );
6366 assert_eq!(errors.len(), 1);
6367 assert!(errors[0]
6368 .to_string()
6369 .contains("minimum 100 is greater than maximum 50"));
6370 }
6371
6372 #[test]
6373 fn validate_number_default_below_minimum() {
6374 let specs = TypeSpecification::Number {
6375 minimum: Some(rational_new(10, 1)),
6376 maximum: None,
6377 decimals: None,
6378 help: String::new(),
6379 };
6380 let default = BoundValueKind::unbound(ValueKind::Number(rational_new(5, 1)));
6381
6382 let src = test_source();
6383 let errors = validate_type_specifications(
6384 &specs,
6385 Some(&default),
6386 "suggestion",
6387 "test",
6388 &src,
6389 None,
6390 &UnitIndex::new(),
6391 );
6392 assert_eq!(errors.len(), 1);
6393 assert!(errors[0]
6394 .to_string()
6395 .contains("suggestion value 5 is less than minimum 10"));
6396 }
6397
6398 #[test]
6399 fn validate_number_default_above_maximum() {
6400 let specs = TypeSpecification::Number {
6401 minimum: None,
6402 maximum: Some(rational_new(100, 1)),
6403 decimals: None,
6404 help: String::new(),
6405 };
6406 let default = BoundValueKind::unbound(ValueKind::Number(rational_new(150, 1)));
6407
6408 let src = test_source();
6409 let errors = validate_type_specifications(
6410 &specs,
6411 Some(&default),
6412 "suggestion",
6413 "test",
6414 &src,
6415 None,
6416 &UnitIndex::new(),
6417 );
6418 assert_eq!(errors.len(), 1);
6419 assert!(errors[0]
6420 .to_string()
6421 .contains("suggestion value 150 is greater than maximum 100"));
6422 }
6423
6424 #[test]
6425 fn validate_number_default_valid() {
6426 let specs = TypeSpecification::Number {
6427 minimum: Some(rational_new(0, 1)),
6428 maximum: Some(rational_new(100, 1)),
6429 decimals: None,
6430 help: String::new(),
6431 };
6432 let default = BoundValueKind::unbound(ValueKind::Number(rational_new(50, 1)));
6433
6434 let src = test_source();
6435 let errors = validate_type_specifications(
6436 &specs,
6437 Some(&default),
6438 "suggestion",
6439 "test",
6440 &src,
6441 None,
6442 &UnitIndex::new(),
6443 );
6444 assert!(errors.is_empty());
6445 }
6446
6447 #[test]
6448 fn validate_ratio_minimum_greater_than_maximum() {
6449 let specs = TypeSpecification::Ratio {
6450 minimum: Some(rational_new(2, 1)),
6451 maximum: Some(rational_new(1, 1)),
6452 units: crate::planning::semantics::RatioUnits::new(),
6453 unit: None,
6454 help: String::new(),
6455 };
6456
6457 let src = test_source();
6458 let errors = validate_type_specifications(
6459 &specs,
6460 None,
6461 "suggestion",
6462 "test",
6463 &src,
6464 None,
6465 &UnitIndex::new(),
6466 );
6467 assert_eq!(errors.len(), 1);
6468 assert!(errors[0]
6469 .to_string()
6470 .contains("minimum 2 is greater than maximum 1"));
6471 }
6472
6473 #[test]
6474 fn validate_date_minimum_after_maximum() {
6475 let mut specs = TypeSpecification::date();
6476 specs = apply(
6477 specs,
6478 TypeConstraintCommand::Minimum,
6479 &[date_arg("2024-12-31")],
6480 );
6481 specs = apply(
6482 specs,
6483 TypeConstraintCommand::Maximum,
6484 &[date_arg("2024-01-01")],
6485 );
6486
6487 let src = test_source();
6488 let errors = validate_type_specifications(
6489 &specs,
6490 None,
6491 "suggestion",
6492 "test",
6493 &src,
6494 None,
6495 &UnitIndex::new(),
6496 );
6497 assert_eq!(errors.len(), 1);
6498 assert!(
6499 errors[0].to_string().contains("minimum")
6500 && errors[0].to_string().contains("is after maximum")
6501 );
6502 }
6503
6504 #[test]
6505 fn validate_date_valid_range() {
6506 let mut specs = TypeSpecification::date();
6507 specs = apply(
6508 specs,
6509 TypeConstraintCommand::Minimum,
6510 &[date_arg("2024-01-01")],
6511 );
6512 specs = apply(
6513 specs,
6514 TypeConstraintCommand::Maximum,
6515 &[date_arg("2024-12-31")],
6516 );
6517
6518 let src = test_source();
6519 let errors = validate_type_specifications(
6520 &specs,
6521 None,
6522 "suggestion",
6523 "test",
6524 &src,
6525 None,
6526 &UnitIndex::new(),
6527 );
6528 assert!(errors.is_empty());
6529 }
6530
6531 #[test]
6532 fn validate_time_minimum_after_maximum() {
6533 let mut specs = TypeSpecification::time();
6534 specs = apply(
6535 specs,
6536 TypeConstraintCommand::Minimum,
6537 &[time_arg("23:00:00")],
6538 );
6539 specs = apply(
6540 specs,
6541 TypeConstraintCommand::Maximum,
6542 &[time_arg("10:00:00")],
6543 );
6544
6545 let src = test_source();
6546 let errors = validate_type_specifications(
6547 &specs,
6548 None,
6549 "suggestion",
6550 "test",
6551 &src,
6552 None,
6553 &UnitIndex::new(),
6554 );
6555 assert_eq!(errors.len(), 1);
6556 assert!(
6557 errors[0].to_string().contains("minimum")
6558 && errors[0].to_string().contains("is after maximum")
6559 );
6560 }
6561
6562 fn magnitude_beyond_decimal_max() -> crate::computation::rational::RationalInteger {
6563 use crate::computation::rational::{decimal_to_rational, rational_new, try_mul};
6564 use rust_decimal::Decimal;
6565 let max = Decimal::MAX.normalize();
6566 let max_rational = decimal_to_rational(max).expect("BUG: Decimal::MAX must lift to Q");
6567 try_mul(&max_rational, &rational_new(2, 1))
6568 .expect("BUG: test rational multiply must succeed")
6569 }
6570
6571 #[test]
6572 fn large_magnitude_minimum_does_not_fail_type_validation() {
6573 use crate::computation::rational::rational_one;
6574 use crate::literals::MeasureUnits;
6575 use crate::planning::semantics::TypeSpecification;
6576
6577 let too_large = magnitude_beyond_decimal_max();
6578 assert_eq!(
6579 too_large.try_to_decimal().unwrap_err(),
6580 crate::computation::rational::NumericFailure::Overflow,
6581 );
6582
6583 let spec = TypeSpecification::Measure {
6584 minimum: Some((too_large, "eur".to_string())),
6585 maximum: None,
6586 units: MeasureUnits(vec![crate::literals::MeasureUnit {
6587 name: "eur".to_string(),
6588 scale: UnitScale::Literal(rational_one()),
6589 derived_measure_factors: Default::default(),
6590 decomposition: Default::default(),
6591 minimum: None,
6592 maximum: None,
6593 suggestion_magnitude: None,
6594 }]),
6595 traits: vec![],
6596 decomposition: None,
6597 unit: None,
6598 help: String::new(),
6599 };
6600
6601 let src = test_source();
6602 let errors = validate_type_specifications(
6603 &spec,
6604 None,
6605 "suggestion",
6606 "money",
6607 &src,
6608 None,
6609 &UnitIndex::new(),
6610 );
6611 assert!(
6612 errors.is_empty(),
6613 "internal Q bounds must not fail type validation; API decimal rounding: {:?}",
6614 errors.iter().map(|e| e.to_string()).collect::<Vec<_>>()
6615 );
6616 }
6617
6618 #[test]
6619 fn large_magnitude_maximum_does_not_fail_type_validation() {
6620 use crate::computation::rational::rational_one;
6621 use crate::literals::MeasureUnits;
6622 use crate::planning::semantics::TypeSpecification;
6623
6624 let too_large = magnitude_beyond_decimal_max();
6625 assert_eq!(
6626 too_large.try_to_decimal().unwrap_err(),
6627 crate::computation::rational::NumericFailure::Overflow,
6628 );
6629
6630 let spec = TypeSpecification::Measure {
6631 minimum: None,
6632 maximum: Some((too_large, "eur".to_string())),
6633 units: MeasureUnits(vec![crate::literals::MeasureUnit {
6634 name: "eur".to_string(),
6635 scale: UnitScale::Literal(rational_one()),
6636 derived_measure_factors: Default::default(),
6637 decomposition: Default::default(),
6638 minimum: None,
6639 maximum: None,
6640 suggestion_magnitude: None,
6641 }]),
6642 traits: vec![],
6643 decomposition: None,
6644 unit: None,
6645 help: String::new(),
6646 };
6647
6648 let src = test_source();
6649 let errors = validate_type_specifications(
6650 &spec,
6651 None,
6652 "suggestion",
6653 "money",
6654 &src,
6655 None,
6656 &UnitIndex::new(),
6657 );
6658 assert!(errors.is_empty(), "got: {:?}", errors);
6659 }
6660
6661 #[test]
6662 fn large_magnitude_default_does_not_fail_type_validation() {
6663 use crate::computation::rational::rational_one;
6664 use crate::literals::MeasureUnits;
6665 use crate::planning::semantics::TypeSpecification;
6666
6667 let too_large = magnitude_beyond_decimal_max();
6668 assert_eq!(
6669 too_large.try_to_decimal().unwrap_err(),
6670 crate::computation::rational::NumericFailure::Overflow,
6671 );
6672
6673 let spec = TypeSpecification::Measure {
6674 minimum: None,
6675 maximum: None,
6676 units: MeasureUnits(vec![crate::literals::MeasureUnit {
6677 name: "eur".to_string(),
6678 scale: UnitScale::Literal(rational_one()),
6679 derived_measure_factors: Default::default(),
6680 decomposition: Default::default(),
6681 minimum: None,
6682 maximum: None,
6683 suggestion_magnitude: Some(too_large),
6684 }]),
6685 traits: vec![],
6686 decomposition: None,
6687 unit: None,
6688 help: String::new(),
6689 };
6690
6691 let src = test_source();
6692 let errors = validate_type_specifications(
6693 &spec,
6694 None,
6695 "suggestion",
6696 "money",
6697 &src,
6698 None,
6699 &UnitIndex::new(),
6700 );
6701 assert!(errors.is_empty(), "got: {:?}", errors);
6702 }
6703
6704 #[test]
6705 fn refresh_named_range_specs_missing_element_must_not_leave_unconverted_measure() {
6706 use crate::engine::Context;
6707 use crate::parsing::ast::{LemmaSpec, ParentType, Span};
6708 use crate::parsing::source::{Source, SourceType};
6709 use crate::planning::semantics::tests::primitive_measure_arc;
6710
6711 let mut ctx = Context::new();
6712 let repository = ctx.workspace();
6713 ctx.insert_spec(Arc::clone(&repository), LemmaSpec::new("t".to_string()))
6714 .unwrap();
6715 let ctx: &'static Context = Box::leak(Box::new(ctx));
6716 let mut resolver = TypeResolver::new(ctx, HashMap::new());
6717 let spec = ctx
6718 .spec_set(&ctx.workspace(), "t")
6719 .and_then(|ss| ss.get_exact(None))
6720 .expect("inserted");
6721 assert!(resolver.register_all(&ctx.workspace(), spec).is_empty());
6722 let source = Source::new(
6723 SourceType::Volatile,
6724 Span {
6725 start: 0,
6726 end: 0,
6727 line: 1,
6728 col: 0,
6729 },
6730 );
6731 let mut data_defs = HashMap::new();
6732 data_defs.insert(
6733 "band".to_string(),
6734 DataTypeDef {
6735 parent: ParentType::Ranged {
6736 inner: Box::new(ParentType::custom("ghost")),
6737 },
6738 constraints: None,
6739 source: source.clone(),
6740 name: "band".to_string(),
6741 bound_literal: None,
6742 },
6743 );
6744 let mut resolved_types = ResolvedSpecTypes::default();
6745 resolved_types
6746 .resolved
6747 .insert("band".to_string(), primitive_measure_arc().clone());
6748 let already_resolved = ResolvedTypesMap::new();
6749
6750 let errors = refresh_named_range_specs(
6751 &resolver,
6752 spec,
6753 &data_defs,
6754 &mut resolved_types,
6755 &already_resolved,
6756 &EffectiveDate::Origin,
6757 );
6758
6759 assert!(
6760 !errors.is_empty(),
6761 "planning must error when ranged inner type is missing"
6762 );
6763 assert!(
6764 errors[0]
6765 .to_string()
6766 .contains("Unknown parent 'ghost' for data definition"),
6767 "got: {}",
6768 errors[0]
6769 );
6770 }
6771 }
6772}