Skip to main content

lemma/planning/
types.rs

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