Skip to main content

lemma/planning/
graph.rs

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