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