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