Skip to main content

lemma/planning/
graph.rs

1use crate::computation::rational::{rational_is_zero, NumericFailure, RationalInteger};
2use crate::computation::{mathematical_computation_applies_to_magnitude, OperationResult};
3use crate::engine::Context;
4use crate::literals::{MeasureUnit, MeasureUnits};
5use crate::parsing::arena::{ExprId, ExprKind, ExprRef, ExprTable, ExprTree};
6use crate::parsing::ast::{
7    self as ast, CommandArg, Constraint, ConversionTypeKind, EffectiveDate, LemmaData,
8    LemmaRepository, LemmaRule, LemmaSpec, ParentType, PrimitiveKind, TypeConstraintCommand, Value,
9    WithRhs,
10};
11use crate::parsing::source::Source;
12use crate::planning::discovery;
13use crate::planning::execution_plan::{self, ExecutionPlan};
14use crate::planning::instance_list::{
15    self, InstanceListSource, InstanceLists, KeyIndex, RowHop, RowStore, RowTemplate, StoreKeyField,
16};
17use crate::planning::normalize::{LeafKind, NormalFormId, NormalFormKind, NormalFormListOp};
18use crate::planning::semantics::{
19    self, apply_list_with_rows, apply_option_constraint, apply_spec_member_command,
20    bound_value_kind_from_raw_suggestion, calendar_decomposition, calendar_unit_factor,
21    canonicalize_signature, conversion_target_to_semantic, duration_decomposition,
22    lift_range_endpoint, number_with_unit_to_value_kind, parser_value_to_value_kind,
23    primitive_boolean_arc, primitive_date_arc, primitive_number_arc, primitive_ratio_arc,
24    primitive_text_arc, primitive_time_arc, range_type_specification_from_endpoints,
25    scale_decomposition, value_kind_matches_spec, value_to_semantic, ArithmeticComputation,
26    BaseMeasureVector, BoundValueKind, ComparisonComputation, ConstraintNames, DataDefinition,
27    DataOrigin, DataPath, Expression, ExpressionKind, ImportEffectivePin, KeyField, LemmaType,
28    ListKey, ListOp, ListPredKind, ListReduce, LiteralValue, PathSegment, RawSuggestion,
29    ReferenceEnd, ReferenceTarget, RowBinding, RowEnd, RowInstance, RowOrigin, RulePath,
30    SemanticConversionTarget, SpecIdentity, SpecInstance, SpecVersionKey, TypeDefiningSpec,
31    TypeExtends, TypeFamily, TypeOptions, TypeSpecification, TypedLiteral, UnitTokenResolver,
32    ValueKind, SPEC_LIST_HAS_NO_OPTIONS,
33};
34use crate::planning::spec_value::{RowFieldRead, RulePresence, SpecCandidates};
35use crate::planning::typing::{
36    comparison_type, date_predicate_type, list_command_result_type, list_has_instance_rows,
37    logical_connective_type, logical_not_type, math_op_type, measure_range_matches_measure,
38    past_future_range_type, piecewise_type, range_bound_result_type, range_matches_measure_type,
39    range_matches_range_measure, range_span_type, refine_list_key_type, resolve_anonymous_measure,
40    result_is_veto_type, step_rounding_compatible, step_rounding_type, unit_conversion_type,
41    ListCommandResultKind, MeasureScope,
42};
43use crate::planning::unit_index::{
44    PlanningOwner, PlanningUnitIndex, UnitIndex, UnitMergeConflict, UnitOwner,
45};
46use crate::Error;
47
48use crate::computation::arithmetic::{SignatureFactor, SignatureIndex};
49use crate::computation::units::owning_type_declares_unit_name;
50use crate::evaluation::evaluate_data::typed_literal_from_parsed;
51use crate::limits::{ResourceLimits, MAX_UNIT_EXPONENT};
52pub use crate::planning::typing::DecompositionMatch;
53pub(crate) use crate::planning::typing::{
54    compute_arithmetic_result_type, find_unique_measure_type_in_unit_index,
55    infer_range_type_from_endpoint_types, quantity_integer_power_type,
56};
57use crate::string_distance::closest_name;
58use ast::DataValue as ParsedDataValue;
59use indexmap::IndexMap;
60use rust_decimal::Decimal;
61use std::cmp::Ordering;
62use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
63use std::fmt;
64use std::sync::Arc;
65
66/// One `uses` import after its dependency body (or bodies) have been planned.
67enum PlannedImport {
68    Static {
69        identity: Arc<SpecIdentity>,
70        template: Arc<ExecutionPlan>,
71    },
72    Dynamic {
73        pin: ImportEffectivePin,
74        candidates: Vec<(Arc<SpecIdentity>, Arc<ExecutionPlan>)>,
75    },
76}
77
78impl PlannedImport {
79    fn primary_identity(&self) -> &Arc<SpecIdentity> {
80        match self {
81            PlannedImport::Static { identity, .. } => identity,
82            PlannedImport::Dynamic { candidates, .. } => {
83                &candidates
84                    .first()
85                    .expect("BUG: dynamic import candidates are non-empty")
86                    .0
87            }
88        }
89    }
90
91    fn is_dynamic(&self) -> bool {
92        matches!(self, PlannedImport::Dynamic { .. })
93    }
94}
95
96/// Memo of inferred expression types per expression table (one table per rule).
97/// The memo does not include `computed_rule_types` or `resolved_types`, so an entry is valid
98/// only while those are unchanged. Every change to either must be followed by `clear()`:
99/// after phase 4 merges rule-target constraints into `resolved_types`, and when
100/// `ensure_seed` replaces a recursive rule's undetermined placeholder with its seed.
101#[derive(Default)]
102struct InferExpressionTypeCache {
103    /// Types of a table's nodes in table order, keyed by the table's address. A table lists
104    /// children before parents, so a typed prefix always covers the operands of its next node.
105    tables: HashMap<usize, Vec<Arc<LemmaType>>>,
106    /// Keys of the tables typed since the last [`Self::clear`].
107    typed: Vec<usize>,
108}
109
110impl InferExpressionTypeCache {
111    fn new() -> Self {
112        Self::default()
113    }
114
115    /// Forgets every type and keeps each table's storage for the next wave.
116    fn clear(&mut self) {
117        for key in self.typed.drain(..) {
118            self.tables
119                .get_mut(&key)
120                .expect("BUG: a typed table is in the cache")
121                .clear();
122        }
123    }
124}
125
126/// Data bindings map: maps a target data name path to the binding's value and source.
127///
128/// The key is the path of **data names** from the spec being built to the target data,
129/// relative to that spec: `-> with salary: 7500` on `uses employee` in the spec being
130/// built is `["employee", "salary"]`; when the import is built, the same binding is
131/// passed down as `["salary"]`. Spec names are not part of the key.
132type DataBindings = IndexMap<Vec<String>, (BindingValue, Source)>;
133
134/// Binding value stored in [`DataBindings`]: a scalar literal/reference or the rows
135/// that replace an instance-list slot.
136///
137/// References on the binding's right-hand side (e.g. `data license.other: law.other`)
138/// are resolved at binding collection time against the spec in which the binding
139/// itself was written (not the nested target spec). The resolved [`ReferenceTarget`]
140/// is carried through so the nested spec's planning does not need the outer
141/// spec's scope to interpret the reference.
142#[derive(Debug, Clone)]
143pub(crate) enum BindingValue {
144    /// Literal RHS (parsed as a `Value`). Applied as a plain value to the bound data.
145    Literal(ast::Value),
146    /// Reference RHS pre-resolved to a concrete reference target.
147    Reference { target: ReferenceTarget },
148    /// `-> with items: ada, kim` on a `uses`: the named `uses` instances of the spec that
149    /// wrote the binding, in order. Resolved to [`Self::InstanceRows`] by that spec.
150    InstanceAliases(Vec<ast::Reference>),
151    /// Rows that replace an instance-list slot of an imported spec: one copy per alias.
152    InstanceRows(Vec<RowCopy>),
153}
154
155/// A row written in a data list: the bindings its local instance is planned with.
156///
157/// `-> with ada` copies every binding of `uses ada`; `-> with name: "x"` binds the
158/// written fields. Keys are relative to the row spec.
159#[derive(Debug, Clone)]
160pub(crate) struct RowCopy {
161    /// Spec of the copied instance (the alias's spec, or the list's row spec for a field row).
162    identity: SpecIdentity,
163    /// `ada` for `-> with ada`; `row 2` for a field row.
164    name: String,
165    bindings: DataBindings,
166    source: Source,
167}
168
169/// Identity of a row reached from the plan being built, for visited sets.
170type RowKey = *const RowInstance;
171
172/// A row reached from the plan being built: `context` holds the rows whose contexts
173/// enclose it, outermost first. The last is the row owning it; empty when this plan
174/// owns it.
175#[derive(Clone)]
176struct Located<'s> {
177    row: &'s RowInstance,
178    context: Vec<&'s RowInstance>,
179}
180
181impl<'s> Located<'s> {
182    fn root(row: &'s RowInstance) -> Self {
183        Self {
184            row,
185            context: Vec::new(),
186        }
187    }
188
189    /// Context of the rows this row owns.
190    fn inside(&self) -> Vec<&'s RowInstance> {
191        let mut context = self.context.clone();
192        context.push(self.row);
193        context
194    }
195
196    /// Context of the rows owned by the scope a binding written `hops` instances
197    /// above this row names ([`InstanceKey::scope`]).
198    fn context_at(&self, hops: u32) -> Vec<&'s RowInstance> {
199        let hops = usize::try_from(hops).expect("BUG: hops fit usize");
200        let len = self.context.len();
201        assert!(
202            hops <= len,
203            "BUG: a binding hops above the plan being built"
204        );
205        self.context[..len - hops].to_vec()
206    }
207
208    /// The row a binding written `hops` instances above this row resolves in; `None`
209    /// for the plan being built.
210    fn scope(&self, hops: u32) -> Option<Located<'s>> {
211        let mut context = self.context_at(hops);
212        let row = context.pop()?;
213        Some(Located { row, context })
214    }
215
216    fn key(&self) -> Vec<RowKey> {
217        self.context
218            .iter()
219            .chain(std::iter::once(&self.row))
220            .map(|row| *row as RowKey)
221            .collect()
222    }
223
224    /// Hops naming this row from the plan being built, when it and every row enclosing
225    /// it is a `uses` import.
226    fn import_segments(&self) -> Option<Vec<PathSegment>> {
227        self.context
228            .iter()
229            .chain(std::iter::once(&self.row))
230            .map(|row| match &row.origin {
231                RowOrigin::Import { alias } => Some(PathSegment::import(alias, &row.identity)),
232                RowOrigin::Numbered { .. }
233                | RowOrigin::Written { .. }
234                | RowOrigin::Inline { .. } => None,
235            })
236            .collect()
237    }
238}
239
240/// A read of `end` landing on row `at`.
241struct ForcedRead<'s> {
242    at: Located<'s>,
243    end: RowEnd,
244}
245
246/// How [`Graph::template_rule_reads`] treats rule references.
247#[derive(Clone, Copy, PartialEq, Eq)]
248enum RuleWalk {
249    /// Walk the referenced rule's reads in the same context.
250    Follow,
251    /// Read the referenced rule, and read data slots too.
252    Show,
253}
254
255/// Where a reference target lands: on this plan's own `end`, or as a read on a row.
256enum Landing<'s> {
257    Plan(RowEnd),
258    Row(ForcedRead<'s>),
259}
260
261/// Rules of `uses` imports that Show reaches from the rules of the plan being built,
262/// with every Show edge, keyed by paths composed from the import hops.
263#[derive(Debug, Default)]
264pub(crate) struct ShowImports {
265    /// Edges into imports of each rule of this plan, and every edge of each reached
266    /// import rule.
267    pub(crate) edges: BTreeMap<RulePath, BTreeSet<RulePath>>,
268    /// The edges of each reached import rule its normalized body reads: the rules whose
269    /// data needs it shares.
270    pub(crate) need_edges: BTreeMap<RulePath, BTreeSet<RulePath>>,
271    /// Data each Show rule reads that no binding fills: data of this plan, or data of
272    /// an import.
273    pub(crate) data_reads: BTreeMap<RulePath, BTreeSet<DataPath>>,
274    /// Template identity of every import data path in [`Self::data_reads`].
275    pub(crate) data: BTreeMap<DataPath, SpecIdentity>,
276    /// Every reached import rule: its template identity and its rule there.
277    pub(crate) rules: BTreeMap<RulePath, (SpecIdentity, RulePath)>,
278}
279
280type ScopeKey = Vec<Vec<Vec<RowKey>>>;
281
282fn scope_key(scope: &[Vec<Located<'_>>]) -> ScopeKey {
283    scope
284        .iter()
285        .map(|rows| rows.iter().map(Located::key).collect())
286        .collect()
287}
288
289/// Rows the list command at row level `level` iterates.
290fn level_rows<'r, 's>(scope: &'r [Vec<Located<'s>>], level: u32) -> &'r [Located<'s>] {
291    scope
292        .get(usize::try_from(level).expect("BUG: row level fits usize"))
293        .expect("BUG: row read outside its list command's scope")
294}
295
296#[derive(Debug)]
297pub(crate) struct Graph<'a> {
298    /// Root spec being planned (for error spec_context).
299    main_spec: &'a LemmaSpec,
300    data: IndexMap<DataPath, DataDefinition>,
301    rules: BTreeMap<RulePath, RuleNode<'a>>,
302    /// Rules whose expression conversion failed. Each has an error in `graph_errors`.
303    dropped_rules: BTreeSet<RulePath>,
304    /// Every rule in dependency order (topo), imports flattened in.
305    rule_order: Vec<RulePath>,
306    /// Data→data references in dependency order (topo). Targets before dependents
307    /// so chained references resolve. Refs to non-reference data are unordered
308    /// among themselves.
309    data_reference_order: Vec<DataPath>,
310    /// Every [`DataDefinition::Reference`] path → where its chain ends.
311    /// Filled in [`Self::validate`] after data-reference cycles are rejected.
312    reference_ends: IndexMap<DataPath, ReferenceEnd>,
313    /// Instances each rule can yield (empty when not spec-valued or list-valued).
314    /// Filled by [`Self::link_spec_values`].
315    rule_candidates: HashMap<RulePath, SpecCandidates>,
316    instance_lists: InstanceLists,
317    /// The rules of this plan and every `uses` import rule they reach, with their reads.
318    /// Filled by [`Self::sort_show_rules`].
319    show_imports: ShowImports,
320    /// Dependencies of every Show rule, keyed like `show_order`.
321    show_edges: BTreeMap<RulePath, BTreeSet<RulePath>>,
322    /// Show rules in dependency order: ready rules in path order, first ready first.
323    show_order: Vec<RulePath>,
324    /// Scale target of each dynamic unit this spec's types declare, in this spec's
325    /// coordinates. A dynamic unit of a type another spec declares has no entry: its
326    /// factor belongs to that spec. Filled by [`Self::plan_unit_scales`].
327    unit_scale_targets: IndexMap<(TypeFamily, String), ReferenceTarget>,
328}
329
330impl<'a> Graph<'a> {
331    pub(crate) fn unit_scale_targets(&self) -> &IndexMap<(TypeFamily, String), ReferenceTarget> {
332        &self.unit_scale_targets
333    }
334
335    pub(crate) fn data(&self) -> &IndexMap<DataPath, DataDefinition> {
336        &self.data
337    }
338
339    pub(crate) fn rules(&self) -> &BTreeMap<RulePath, RuleNode<'a>> {
340        &self.rules
341    }
342
343    pub(crate) fn rule_presence(&self, path: &RulePath) -> RulePresence {
344        if self.rules.contains_key(path) {
345            RulePresence::Present
346        } else if self.dropped_rules.contains(path) {
347            RulePresence::Dropped
348        } else {
349            RulePresence::Absent
350        }
351    }
352
353    pub(crate) fn rules_mut(&mut self) -> &mut BTreeMap<RulePath, RuleNode<'a>> {
354        &mut self.rules
355    }
356
357    pub(crate) fn rule_order(&self) -> &[RulePath] {
358        &self.rule_order
359    }
360
361    pub(crate) fn data_reference_order(&self) -> &[DataPath] {
362        &self.data_reference_order
363    }
364
365    pub(crate) fn reference_ends(&self) -> &IndexMap<DataPath, ReferenceEnd> {
366        &self.reference_ends
367    }
368
369    pub(crate) fn rule_candidates(&self) -> &HashMap<RulePath, SpecCandidates> {
370        &self.rule_candidates
371    }
372
373    pub(crate) fn show_imports(&self) -> &ShowImports {
374        &self.show_imports
375    }
376
377    pub(crate) fn show_edges(&self) -> &BTreeMap<RulePath, BTreeSet<RulePath>> {
378        &self.show_edges
379    }
380
381    pub(crate) fn show_order(&self) -> &[RulePath] {
382        &self.show_order
383    }
384
385    pub(crate) fn main_spec(&self) -> &LemmaSpec {
386        self.main_spec
387    }
388
389    pub(crate) fn instance_lists(&self) -> &InstanceLists {
390        &self.instance_lists
391    }
392
393    /// Build the data map: one entry per data (Value or Import), with defaults and coercion applied.
394    /// Preserves definition order from the source spec.
395    pub(crate) fn build_data(
396        &self,
397        resolved_by_type_name: &IndexMap<String, Arc<LemmaType>>,
398    ) -> Result<IndexMap<DataPath, DataDefinition>, Vec<Error>> {
399        struct PendingReference {
400            target: ReferenceTarget,
401            resolved_type: Arc<LemmaType>,
402            local_constraints: Option<Vec<Constraint>>,
403            local_suggestion: Option<BoundValueKind>,
404            local_value: Option<BoundValueKind>,
405            origin: DataOrigin,
406        }
407
408        let mut schema: HashMap<DataPath, Arc<LemmaType>> = HashMap::new();
409        let mut declared_suggestions: HashMap<DataPath, BoundValueKind> = HashMap::new();
410        let mut declared_values: HashMap<DataPath, BoundValueKind> = HashMap::new();
411        let mut values: HashMap<DataPath, (LiteralValue, DataOrigin)> = HashMap::new();
412        let mut value_sources: HashMap<DataPath, Source> = HashMap::new();
413        let mut references: HashMap<DataPath, PendingReference> = HashMap::new();
414
415        for (path, rfv) in self.data.iter() {
416            match rfv {
417                DataDefinition::Value {
418                    value,
419                    resolved_type,
420                    origin,
421                    source,
422                } => {
423                    values.insert(path.clone(), (value.clone(), *origin));
424                    value_sources.insert(path.clone(), source.clone());
425                    schema.insert(path.clone(), Arc::clone(resolved_type));
426                }
427                DataDefinition::TypeDeclaration {
428                    resolved_type,
429                    declared_suggestion,
430                    declared_value,
431                    ..
432                } => {
433                    schema.insert(path.clone(), Arc::clone(resolved_type));
434                    if let Some(dv) = declared_suggestion {
435                        declared_suggestions.insert(path.clone(), dv.clone());
436                    }
437                    if let Some(dv) = declared_value {
438                        declared_values.insert(path.clone(), dv.clone());
439                    }
440                }
441                DataDefinition::Import { .. } => {}
442                DataDefinition::InstanceList { .. } => {}
443                DataDefinition::Reference {
444                    target,
445                    resolved_type,
446                    local_constraints,
447                    local_suggestion,
448                    local_value,
449                    origin,
450                    ..
451                } => {
452                    schema.insert(path.clone(), Arc::clone(resolved_type));
453                    references.insert(
454                        path.clone(),
455                        PendingReference {
456                            target: target.clone(),
457                            resolved_type: Arc::clone(resolved_type),
458                            local_constraints: local_constraints.clone(),
459                            local_suggestion: local_suggestion.clone(),
460                            local_value: local_value.clone(),
461                            origin: *origin,
462                        },
463                    );
464                }
465            }
466        }
467
468        let mut coercion_errors: Vec<Error> = Vec::new();
469        for (path, (value, _)) in values.iter_mut() {
470            let Some(schema_type) = schema.get(path).cloned() else {
471                continue;
472            };
473            if let Some(type_name) = schema_type.name.as_deref() {
474                if let Some(resolved) = resolved_by_type_name.get(type_name) {
475                    if let Err(failure) = semantics::refresh_measure_literal_canonical_magnitude(
476                        value,
477                        schema_type.as_ref(),
478                        resolved,
479                    ) {
480                        coercion_errors.push(Error::validation(
481                            format!("Data '{path}' measure recanonicalization failed: {failure}"),
482                            value_sources.get(path).cloned(),
483                            None::<String>,
484                        ));
485                        continue;
486                    }
487                }
488            }
489            let typed = TypedLiteral {
490                value: value.value.clone(),
491                lemma_type: Arc::clone(&schema_type),
492            };
493            match Self::coerce_literal_to_schema_type(&typed, &schema_type) {
494                Ok(coerced) => {
495                    *value = coerced.to_literal();
496                    schema.insert(path.clone(), Arc::clone(&coerced.lemma_type));
497                }
498                Err(msg) => {
499                    coercion_errors.push(Error::validation(
500                        format!("Data '{path}' incompatible with declared type: {msg}"),
501                        value_sources.get(path).cloned(),
502                        None::<String>,
503                    ));
504                }
505            }
506        }
507        if !coercion_errors.is_empty() {
508            return Err(coercion_errors);
509        }
510
511        let mut data = IndexMap::new();
512        for (path, rfv) in &self.data {
513            let source = rfv.source().clone();
514            if let DataDefinition::InstanceList { .. } | DataDefinition::Import { .. } = rfv {
515                data.insert(path.clone(), rfv.clone());
516            } else if let Some(pending) = references.remove(path) {
517                data.insert(
518                    path.clone(),
519                    DataDefinition::Reference {
520                        target: pending.target,
521                        resolved_type: pending.resolved_type,
522                        local_constraints: pending.local_constraints,
523                        local_suggestion: pending.local_suggestion,
524                        local_value: pending.local_value,
525                        origin: pending.origin,
526                        source,
527                    },
528                );
529            } else if let Some((value, origin)) = values.remove(path) {
530                let resolved_type = schema
531                    .remove(path)
532                    .expect("BUG: value data must have schema type after coercion");
533                data.insert(
534                    path.clone(),
535                    DataDefinition::Value {
536                        value,
537                        resolved_type,
538                        origin,
539                        source,
540                    },
541                );
542            } else {
543                let resolved_type = schema
544                    .get(path)
545                    .cloned()
546                    .expect("non-spec-ref data has schema (value, reference, or type-only)");
547                let declared_suggestion = declared_suggestions.remove(path);
548                let declared_value = declared_values.remove(path);
549                data.insert(
550                    path.clone(),
551                    DataDefinition::TypeDeclaration {
552                        resolved_type,
553                        declared_suggestion,
554                        declared_value,
555                        source,
556                    },
557                );
558            }
559        }
560        Ok(data)
561    }
562
563    /// Keep a measure literal's written unit when replacing `lemma_type` with a schema type
564    /// that has the same specifications (or compatible family).
565    fn preserve_measure_binding(
566        schema_type: &Arc<LemmaType>,
567        previous: &LemmaType,
568    ) -> Arc<LemmaType> {
569        match previous.unit() {
570            Some(unit)
571                if matches!(
572                    &schema_type.specifications,
573                    TypeSpecification::Measure { .. } | TypeSpecification::Ratio { .. }
574                ) =>
575            {
576                Arc::new(schema_type.as_ref().clone().with_unit(unit))
577            }
578            Some(_) | None => Arc::clone(schema_type),
579        }
580    }
581
582    pub(crate) fn coerce_literal_to_schema_type(
583        lit: &TypedLiteral,
584        schema_type: &Arc<LemmaType>,
585    ) -> Result<TypedLiteral, String> {
586        fn range_endpoint_schema_type(schema_type: &LemmaType) -> Option<Arc<LemmaType>> {
587            schema_type
588                .specifications
589                .element_from_range()
590                .map(|element_spec| Arc::new(LemmaType::primitive(element_spec)))
591        }
592
593        let schema_ref = schema_type.as_ref();
594        match (&schema_ref.specifications, &lit.value) {
595            (
596                TypeSpecification::NumberRange { .. }
597                | TypeSpecification::DateRange { .. }
598                | TypeSpecification::TimeRange { .. }
599                | TypeSpecification::RatioRange { .. }
600                | TypeSpecification::MeasureRange { .. },
601                ValueKind::Range(left, right),
602            ) if !lit
603                .lemma_type
604                .specifications
605                .equal_ignoring_unit(&schema_ref.specifications) =>
606            {
607                let endpoint_schema_type =
608                    range_endpoint_schema_type(schema_ref).unwrap_or_else(|| {
609                        unreachable!("BUG: range_endpoint_schema_type missing range schema arm")
610                    });
611                let left_schema =
612                    Self::preserve_measure_binding(&endpoint_schema_type, left.lemma_type.as_ref());
613                let right_schema = Self::preserve_measure_binding(
614                    &endpoint_schema_type,
615                    right.lemma_type.as_ref(),
616                );
617                let coerced_left = Self::coerce_scalar_literal(left, &left_schema)?;
618                let coerced_right = Self::coerce_scalar_literal(right, &right_schema)?;
619                Ok(TypedLiteral {
620                    value: ValueKind::Range(Box::new(coerced_left), Box::new(coerced_right)),
621                    lemma_type: Arc::clone(schema_type),
622                })
623            }
624            _ => Self::coerce_scalar_literal(lit, schema_type),
625        }
626    }
627
628    /// [`Self::coerce_literal_to_schema_type`] for a literal that is not a range against a
629    /// range schema: a range endpoint is coerced here, never as a range.
630    fn coerce_scalar_literal(
631        lit: &TypedLiteral,
632        schema_type: &Arc<LemmaType>,
633    ) -> Result<TypedLiteral, String> {
634        let schema_ref = schema_type.as_ref();
635        if lit
636            .lemma_type
637            .specifications
638            .equal_ignoring_unit(&schema_ref.specifications)
639        {
640            if !value_kind_matches_spec(&lit.value, &schema_ref.specifications) {
641                panic!(
642                    "BUG: LiteralValue value kind {:?} inconsistent with lemma_type {:?}",
643                    lit.value, lit.lemma_type.specifications
644                );
645            }
646            let mut out = lit.clone();
647            out.lemma_type = Self::preserve_measure_binding(schema_type, &lit.lemma_type);
648            return Ok(out);
649        }
650        match (&schema_ref.specifications, &lit.value) {
651            (TypeSpecification::Number { .. }, ValueKind::Number(_))
652            | (TypeSpecification::Text { .. }, ValueKind::Text(_))
653            | (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
654            | (TypeSpecification::Date { .. }, ValueKind::Date(_))
655            | (TypeSpecification::Time { .. }, ValueKind::Time(_)) => {
656                let mut out = lit.clone();
657                out.lemma_type = Arc::clone(schema_type);
658                Ok(out)
659            }
660            (TypeSpecification::Measure { .. }, ValueKind::Measure(_)) => {
661                if !lit.lemma_type.same_measure_family(schema_ref)
662                    && !lit.lemma_type.compatible_with_anonymous_measure(schema_ref)
663                    && !lit
664                        .lemma_type
665                        .specifications
666                        .equal_ignoring_unit(&schema_ref.specifications)
667                {
668                    return Err(format!(
669                        "value {} cannot be used as type {}: incompatible measure families",
670                        lit,
671                        schema_ref.name()
672                    ));
673                }
674                let mut out = lit.clone();
675                out.lemma_type = Self::preserve_measure_binding(schema_type, &lit.lemma_type);
676                Ok(out)
677            }
678            (TypeSpecification::Ratio { .. }, ValueKind::Ratio(_)) => {
679                let mut out = lit.clone();
680                out.lemma_type = Self::preserve_measure_binding(schema_type, &lit.lemma_type);
681                Ok(out)
682            }
683            _ => Err(format!(
684                "value {} cannot be used as type {}",
685                lit,
686                schema_ref.name()
687            )),
688        }
689    }
690
691    /// Resolve each data-target [`DataDefinition::Reference`]'s provisional
692    /// `resolved_type` into its final merged form by combining:
693    ///   1. the target data's declared schema type,
694    ///   2. any local `-> ...` constraints attached to the reference itself,
695    ///   3. the LHS-declared type of the referencing data (when present; only
696    ///      possible in a binding whose bound data has its own type
697    ///      declaration in the nested spec).
698    ///
699    /// `ordered_reference_paths` is the dependency order from
700    /// [`Self::compute_data_reference_order`] (targets before
701    /// dependents). Each resolution is applied immediately so a reference
702    /// whose target is itself a reference reads the target's final resolved
703    /// type — batched application would leave chains `Undetermined`.
704    ///
705    /// Rule-target references are not in the order — they are resolved later
706    /// in [`Self::resolve_rule_reference_types`] using the inferred rule
707    /// type, which is only available after [`infer_rule_types`] has run.
708    fn resolve_data_reference_types(
709        &mut self,
710        ordered_reference_paths: &[DataPath],
711        resolved_types: &ResolvedTypesMap,
712    ) -> Result<(), Vec<Error>> {
713        let mut errors: Vec<Error> = Vec::new();
714        let main_spec = self.main_spec;
715        let resolve_unit = unit_tokens_for_spec(resolved_types, main_spec);
716
717        for reference_path in ordered_reference_paths {
718            let (target_data_path, provisional, local_constraints, source) =
719                match self.data.get(reference_path) {
720                    Some(DataDefinition::Reference {
721                        target: ReferenceTarget::Data(path),
722                        resolved_type,
723                        local_constraints,
724                        source,
725                        ..
726                    }) => (
727                        path.clone(),
728                        Arc::clone(resolved_type),
729                        local_constraints.clone(),
730                        source.clone(),
731                    ),
732                    _ => unreachable!(
733                        "BUG: reference evaluation order must contain only data-target references"
734                    ),
735                };
736
737            let Some(target_entry) = self.data.get(&target_data_path) else {
738                errors.push(reference_error(
739                    self.main_spec,
740                    &source,
741                    format!(
742                        "Data reference '{}' target '{}' does not exist",
743                        reference_path, target_data_path
744                    ),
745                ));
746                continue;
747            };
748
749            let target_type_arc = match target_entry {
750                DataDefinition::TypeDeclaration { resolved_type, .. }
751                | DataDefinition::Reference { resolved_type, .. }
752                | DataDefinition::InstanceList { resolved_type, .. }
753                | DataDefinition::Value { resolved_type, .. } => Arc::clone(resolved_type),
754                DataDefinition::Import { .. } => {
755                    errors.push(reference_error(
756                        self.main_spec,
757                        &source,
758                        format!(
759                            "Data reference '{}' target '{}' is a spec reference and cannot carry a value",
760                            reference_path, target_data_path
761                        ),
762                    ));
763                    continue;
764                }
765            };
766
767            let lhs_declared_type: Option<&LemmaType> = if provisional.is_undetermined() {
768                None
769            } else {
770                Some(provisional.as_ref())
771            };
772
773            if let Some(lhs) = lhs_declared_type {
774                if let Some(msg) = reference_kind_mismatch_message(
775                    lhs,
776                    target_type_arc.as_ref(),
777                    reference_path,
778                    &target_data_path,
779                    "target",
780                ) {
781                    errors.push(reference_error(self.main_spec, &source, msg));
782                    continue;
783                }
784            }
785
786            // Merge: prefer LHS-declared spec when present so child-declared
787            // constraints (e.g. `maximum 5` from a binding's parent type
788            // chain) are enforced on the copied value at run time. Without
789            // a LHS-declared type, fall back to the target's spec.
790            let mut merged = match lhs_declared_type {
791                Some(lhs) => lhs.clone(),
792                None => target_type_arc.as_ref().clone(),
793            };
794            let mut raw_suggestion: Option<RawSuggestion> = None;
795            let mut raw_value: Option<RawSuggestion> = None;
796            if let Some(constraints) = &local_constraints {
797                let constraint_type_name = merged.name();
798                match apply_constraints_to_spec(
799                    self.main_spec,
800                    &constraint_type_name,
801                    &reference_path.data,
802                    &mut merged,
803                    constraints,
804                    &source,
805                    ConstraintSuggestionSink {
806                        suggestion: &mut raw_suggestion,
807                        value: &mut raw_value,
808                        resolve_unit: Some(&resolve_unit),
809                    },
810                ) {
811                    Ok(()) => {}
812                    Err(errs) => {
813                        errors.extend(errs);
814                        continue;
815                    }
816                }
817            }
818            let captured_suggestion = match raw_suggestion {
819                None => None,
820                Some(raw) => {
821                    match bound_value_kind_from_raw_suggestion(
822                        raw,
823                        &merged.specifications,
824                        &merged.name(),
825                    ) {
826                        Ok(vk) => Some(vk),
827                        Err(message) => {
828                            errors.push(reference_error(self.main_spec, &source, message));
829                            continue;
830                        }
831                    }
832                }
833            };
834            let captured_value = match raw_value {
835                None => None,
836                Some(raw) => {
837                    match bound_value_kind_from_raw_suggestion(
838                        raw,
839                        &merged.specifications,
840                        &merged.name(),
841                    ) {
842                        Ok(vk) => Some(vk),
843                        Err(message) => {
844                            errors.push(reference_error(self.main_spec, &source, message));
845                            continue;
846                        }
847                    }
848                }
849            };
850
851            // Apply immediately: later references in the order may target
852            // this one and must read the final merged type.
853            if let Some(DataDefinition::Reference {
854                resolved_type,
855                local_suggestion,
856                local_value,
857                ..
858            }) = self.data.get_mut(reference_path)
859            {
860                *resolved_type = Arc::new(merged);
861                if captured_suggestion.is_some() {
862                    *local_suggestion = captured_suggestion;
863                }
864                if captured_value.is_some() {
865                    *local_value = captured_value;
866                }
867            } else {
868                unreachable!("BUG: reference path disappeared during type resolution");
869            }
870        }
871
872        if errors.is_empty() {
873            Ok(())
874        } else {
875            Err(errors)
876        }
877    }
878
879    /// Resolve each rule-target [`DataDefinition::Reference`]'s `resolved_type`
880    /// from the inferred type of the target rule. Applies the same LHS-vs-target
881    /// kind compatibility check and local `-> ...` constraint merge that
882    /// [`Self::resolve_data_reference_types`] applies to data-target references.
883    ///
884    /// Must run AFTER [`infer_rule_types`] so each target rule's inferred type
885    /// is available, and BEFORE [`check_rule_types`] so consumers see the
886    /// merged reference type during validation.
887    fn resolve_rule_reference_types(
888        &mut self,
889        computed_rule_types: &HashMap<RulePath, Arc<LemmaType>>,
890        resolved_types: &ResolvedTypesMap,
891    ) -> Result<(), Vec<Error>> {
892        let mut errors: Vec<Error> = Vec::new();
893        let mut updates: Vec<RuleReferenceUpdate> = Vec::new();
894        let main_spec = self.main_spec;
895        let resolve_unit = unit_tokens_for_spec(resolved_types, main_spec);
896
897        for (reference_path, entry) in &self.data {
898            let DataDefinition::Reference {
899                target,
900                resolved_type: provisional,
901                local_constraints,
902                source,
903                ..
904            } = entry
905            else {
906                continue;
907            };
908
909            let target_rule_path = match target {
910                ReferenceTarget::Rule(path) => path,
911                ReferenceTarget::Data(_) => continue,
912            };
913
914            let Some(target_type) = computed_rule_types.get(target_rule_path) else {
915                match self.rule_presence(target_rule_path) {
916                    RulePresence::Dropped => {}
917                    RulePresence::Present => {
918                        panic!(
919                            "BUG: rule '{target_rule_path}' type not inferred before dependent '{reference_path}' in topological order"
920                        )
921                    }
922                    RulePresence::Absent => {
923                        errors.push(reference_error(
924                            self.main_spec,
925                            source,
926                            format!(
927                                "Data reference '{}' target rule '{}' does not exist",
928                                reference_path, target_rule_path
929                            ),
930                        ));
931                    }
932                }
933                continue;
934            };
935
936            if target_type.is_spec() {
937                errors.push(reference_error(
938                    self.main_spec,
939                    source,
940                    format!(
941                        "cannot supply spec instance '{}' as a value",
942                        target_rule_path.rule
943                    ),
944                ));
945                continue;
946            }
947
948            // A target rule whose inferred type is `veto` carries no concrete
949            // schema kind, so a LHS declared type cannot be checked against
950            // it at planning time. The runtime veto propagation in the
951            // evaluator will surface the rule's veto reason directly.
952            if target_type.vetoed() || target_type.is_undetermined() {
953                let mut merged = target_type.as_ref().clone();
954                let mut raw_suggestion: Option<RawSuggestion> = None;
955                let mut raw_value: Option<RawSuggestion> = None;
956                if let Some(constraints) = local_constraints {
957                    let constraint_type_name = merged.name();
958                    match apply_constraints_to_spec(
959                        self.main_spec,
960                        &constraint_type_name,
961                        &reference_path.data,
962                        &mut merged,
963                        constraints,
964                        source,
965                        ConstraintSuggestionSink {
966                            suggestion: &mut raw_suggestion,
967                            value: &mut raw_value,
968                            resolve_unit: Some(&resolve_unit),
969                        },
970                    ) {
971                        Ok(()) => {}
972                        Err(errs) => {
973                            errors.extend(errs);
974                            continue;
975                        }
976                    }
977                }
978                let captured_suggestion = match raw_suggestion {
979                    None => None,
980                    Some(raw) => {
981                        match bound_value_kind_from_raw_suggestion(
982                            raw,
983                            &merged.specifications,
984                            &merged.name(),
985                        ) {
986                            Ok(vk) => Some(vk),
987                            Err(message) => {
988                                errors.push(reference_error(self.main_spec, source, message));
989                                continue;
990                            }
991                        }
992                    }
993                };
994                let captured_value = match raw_value {
995                    None => None,
996                    Some(raw) => {
997                        match bound_value_kind_from_raw_suggestion(
998                            raw,
999                            &merged.specifications,
1000                            &merged.name(),
1001                        ) {
1002                            Ok(vk) => Some(vk),
1003                            Err(message) => {
1004                                errors.push(reference_error(self.main_spec, source, message));
1005                                continue;
1006                            }
1007                        }
1008                    }
1009                };
1010                updates.push((
1011                    reference_path.clone(),
1012                    Arc::new(merged),
1013                    captured_suggestion,
1014                    captured_value,
1015                ));
1016                continue;
1017            }
1018
1019            let lhs_declared_type: Option<&LemmaType> = if provisional.is_undetermined() {
1020                None
1021            } else {
1022                Some(provisional.as_ref())
1023            };
1024
1025            if let Some(lhs) = lhs_declared_type {
1026                if let Some(msg) = reference_kind_mismatch_message(
1027                    lhs,
1028                    target_type,
1029                    reference_path,
1030                    target_rule_path,
1031                    "target rule",
1032                ) {
1033                    errors.push(reference_error(self.main_spec, source, msg));
1034                    continue;
1035                }
1036            }
1037
1038            // Prefer LHS-declared spec when present (see data-target merge
1039            // for rationale).
1040            let mut merged = match lhs_declared_type {
1041                Some(lhs) => lhs.clone(),
1042                None => target_type.as_ref().clone(),
1043            };
1044            let mut raw_suggestion: Option<RawSuggestion> = None;
1045            let mut raw_value: Option<RawSuggestion> = None;
1046            if let Some(constraints) = local_constraints {
1047                let constraint_type_name = merged.name();
1048                match apply_constraints_to_spec(
1049                    self.main_spec,
1050                    &constraint_type_name,
1051                    &reference_path.data,
1052                    &mut merged,
1053                    constraints,
1054                    source,
1055                    ConstraintSuggestionSink {
1056                        suggestion: &mut raw_suggestion,
1057                        value: &mut raw_value,
1058                        resolve_unit: Some(&resolve_unit),
1059                    },
1060                ) {
1061                    Ok(()) => {}
1062                    Err(errs) => {
1063                        errors.extend(errs);
1064                        continue;
1065                    }
1066                }
1067            }
1068            let captured_suggestion = match raw_suggestion {
1069                None => None,
1070                Some(raw) => {
1071                    match bound_value_kind_from_raw_suggestion(
1072                        raw,
1073                        &merged.specifications,
1074                        &merged.name(),
1075                    ) {
1076                        Ok(vk) => Some(vk),
1077                        Err(message) => {
1078                            errors.push(reference_error(self.main_spec, source, message));
1079                            continue;
1080                        }
1081                    }
1082                }
1083            };
1084            let captured_value = match raw_value {
1085                None => None,
1086                Some(raw) => {
1087                    match bound_value_kind_from_raw_suggestion(
1088                        raw,
1089                        &merged.specifications,
1090                        &merged.name(),
1091                    ) {
1092                        Ok(vk) => Some(vk),
1093                        Err(message) => {
1094                            errors.push(reference_error(self.main_spec, source, message));
1095                            continue;
1096                        }
1097                    }
1098                }
1099            };
1100
1101            updates.push((
1102                reference_path.clone(),
1103                Arc::new(merged),
1104                captured_suggestion,
1105                captured_value,
1106            ));
1107        }
1108
1109        for (path, new_type, new_suggestion, new_value) in updates {
1110            if let Some(DataDefinition::Reference {
1111                resolved_type,
1112                local_suggestion,
1113                local_value,
1114                ..
1115            }) = self.data.get_mut(&path)
1116            {
1117                *resolved_type = new_type;
1118                if new_suggestion.is_some() {
1119                    *local_suggestion = new_suggestion;
1120                }
1121                if new_value.is_some() {
1122                    *local_value = new_value;
1123                }
1124            } else {
1125                unreachable!(
1126                    "BUG: rule-target reference path disappeared between collect and update phases"
1127                );
1128            }
1129        }
1130
1131        if errors.is_empty() {
1132            Ok(())
1133        } else {
1134            Err(errors)
1135        }
1136    }
1137
1138    /// Add a `depends_on_rules` edge from every rule that reads a rule-target
1139    /// reference data path to the reference's target rule. Planning needs a
1140    /// reference target in `completed_rules` (NormalFormId) before the consumer
1141    /// lowers, and topological sort must detect cycles that flow through reference
1142    /// paths.
1143    /// Kind-check every reference a stored row binds, now that data and rule target
1144    /// types are final. Stored rows are data of the row store, not data definitions,
1145    /// so they do not pass through [`Self::resolve_data_reference_types`] /
1146    /// [`Self::resolve_rule_reference_types`]; the compatibility check mirrors them.
1147    fn check_row_store_reference_types(
1148        &mut self,
1149        computed_rule_types: &HashMap<RulePath, Arc<LemmaType>>,
1150    ) -> Result<(), Vec<Error>> {
1151        let mut errors = Vec::new();
1152        let graph = StoredRowKindCheckGraph {
1153            main_spec: self.main_spec,
1154            lists: &self.instance_lists,
1155            data: &self.data,
1156            rules: &self.rules,
1157            dropped_rules: &self.dropped_rules,
1158            computed_rule_types,
1159        };
1160        check_stored_row_reference_types(&graph, &mut errors);
1161        if errors.is_empty() {
1162            Ok(())
1163        } else {
1164            Err(errors)
1165        }
1166    }
1167
1168    fn add_rule_reference_dependency_edges(&mut self) {
1169        let mut reference_targets: BTreeMap<RulePath, BTreeSet<RulePath>> = BTreeMap::new();
1170        for (rule_path, rule_node) in &self.rules {
1171            let mut found = BTreeSet::new();
1172            for node in rule_node.expression.table.nodes() {
1173                if let ExpressionKind::DataPath(path) = &node.kind {
1174                    if let Some(ReferenceEnd::Rule(target)) = self.reference_ends.get(path) {
1175                        found.insert(target.clone());
1176                    }
1177                }
1178            }
1179            reference_targets.insert(rule_path.clone(), found);
1180        }
1181
1182        for (rule_path, targets) in reference_targets {
1183            self.rules
1184                .get_mut(&rule_path)
1185                .expect("BUG: rule with reference targets missing in graph")
1186                .depends_on_rules
1187                .extend(targets);
1188        }
1189    }
1190
1191    /// A rule that reads through a `uses … -> effective <rule>` alias depends on that
1192    /// pin rule, so a pin that reads the import is a planning cycle.
1193    fn add_dynamic_import_pin_dependency_edges(&mut self) {
1194        let mut pin_by_alias: HashMap<String, RulePath> = HashMap::new();
1195        for (path, definition) in &self.data {
1196            let DataDefinition::Import {
1197                pin: Some(ImportEffectivePin::Rule(pin)),
1198                ..
1199            } = definition
1200            else {
1201                continue;
1202            };
1203            pin_by_alias.insert(path.data.clone(), pin.clone());
1204        }
1205        if pin_by_alias.is_empty() {
1206            return;
1207        }
1208        let mut updates: Vec<(RulePath, RulePath)> = Vec::new();
1209        for (rule_path, rule_node) in &self.rules {
1210            let mut pins = BTreeSet::new();
1211            for node in rule_node.expression.table.nodes() {
1212                let ExpressionKind::SpecMember { base, .. } = &node.kind else {
1213                    continue;
1214                };
1215                let ExpressionKind::DataPath(path) = rule_node.expression.table.kind(*base) else {
1216                    continue;
1217                };
1218                if let Some(pin) = pin_by_alias.get(&path.data) {
1219                    pins.insert(pin.clone());
1220                }
1221            }
1222            for pin in pins {
1223                updates.push((rule_path.clone(), pin));
1224            }
1225        }
1226        for (rule_path, pin) in updates {
1227            self.rules
1228                .get_mut(&rule_path)
1229                .expect("BUG: rule from dynamic pin edge missing in graph")
1230                .depends_on_rules
1231                .insert(pin);
1232        }
1233    }
1234
1235    /// A rule that reads data whose type names an options rule depends on that rule,
1236    /// so its inputs are needs of the reader and the options rule is ordered first.
1237    fn add_options_rule_dependency_edges(&mut self) {
1238        let mut updates: Vec<(RulePath, RulePath)> = Vec::new();
1239        for (rule_path, rule_node) in &self.rules {
1240            let mut targets = BTreeSet::new();
1241            for node in rule_node.expression.table.nodes() {
1242                let ExpressionKind::DataPath(path) = &node.kind else {
1243                    continue;
1244                };
1245                let Some(target) = self
1246                    .data
1247                    .get(path)
1248                    .and_then(|definition| definition.schema_type())
1249                    .and_then(|lemma_type| execution_plan::options_rule_path(path, lemma_type))
1250                else {
1251                    continue;
1252                };
1253                if self.rules.contains_key(&target) {
1254                    targets.insert(target);
1255                }
1256            }
1257            for target in targets {
1258                updates.push((rule_path.clone(), target));
1259            }
1260        }
1261        for (rule_path, target) in updates {
1262            self.rules
1263                .get_mut(&rule_path)
1264                .expect("BUG: rule from iteration missing in graph")
1265                .depends_on_rules
1266                .insert(target);
1267        }
1268    }
1269
1270    /// Plans the dynamic unit scales of this spec, once every node is typed:
1271    ///
1272    /// - [`Self::unit_scale_targets`]: the scale target of each dynamic unit this spec's
1273    ///   types declare, in this spec's coordinates.
1274    /// - [`RuleNode::scale_families`]: the measure families of every typed node of a rule.
1275    /// - A `depends_on_rules` edge from each rule to every local rule that scales one of
1276    ///   its families, so the scale is evaluated before the rule. Uses-qualified targets
1277    ///   are member cells, evaluated in the rule's walk, and need no edge.
1278    ///
1279    /// A value written in, or converted to, a dynamic unit whose factor another spec
1280    /// declares is an error: the factor belongs to the spec that declares the unit.
1281    fn plan_unit_scales(
1282        &mut self,
1283        resolved_types: &ResolvedTypesMap,
1284        inferred_types: &HashMap<RulePath, Arc<LemmaType>>,
1285        cache: &mut InferExpressionTypeCache,
1286    ) -> Vec<Error> {
1287        // Unresolved types already failed planning in type resolution, which reports them.
1288        let Some(types) = find_types_by_spec(resolved_types, self.main_spec) else {
1289            return Vec::new();
1290        };
1291        self.unit_scale_targets = collect_unit_scale_targets(types, &self.data);
1292
1293        let mut scale_rules: BTreeMap<TypeFamily, BTreeSet<RulePath>> = BTreeMap::new();
1294        for ((family, _), target) in &self.unit_scale_targets {
1295            let rule = match target {
1296                ReferenceTarget::Rule(path) if path.segments.is_empty() => path,
1297                ReferenceTarget::Data(path) if path.segments.is_empty() => {
1298                    match self.reference_ends.get(path) {
1299                        Some(ReferenceEnd::Rule(rule)) if rule.segments.is_empty() => rule,
1300                        Some(ReferenceEnd::Rule(_) | ReferenceEnd::Promptable(_)) | None => {
1301                            continue
1302                        }
1303                    }
1304                }
1305                ReferenceTarget::Rule(_) | ReferenceTarget::Data(_) => continue,
1306            };
1307            scale_rules
1308                .entry(family.clone())
1309                .or_default()
1310                .insert(rule.clone());
1311        }
1312
1313        let mut errors = Vec::new();
1314        for (path, definition) in &self.data {
1315            let DataDefinition::Value {
1316                value,
1317                resolved_type,
1318                source,
1319                ..
1320            } = definition
1321            else {
1322                continue;
1323            };
1324            if !path.segments.is_empty() {
1325                continue;
1326            }
1327            for (family, unit) in unscaled_units(&value.value, resolved_type.as_ref()) {
1328                if let Some(message) = self.foreign_scale_message(&family, &unit) {
1329                    errors.push(engine_error_at_graph(self, source, message));
1330                }
1331            }
1332        }
1333
1334        let mut families_by_rule: Vec<(RulePath, BTreeSet<TypeFamily>)> = Vec::new();
1335        for (rule_path, rule_node) in &self.rules {
1336            let expression = rule_node.expression.root();
1337            infer_expression_type(expression, self, inferred_types, resolved_types, cache);
1338            let key = expression.table as *const ExprTable<ExpressionKind> as usize;
1339            let node_types = &cache.tables[&key];
1340            let mut families = BTreeSet::new();
1341            for (index, node_type) in node_types.iter().enumerate() {
1342                if let Some(family) = node_type.held_measure_family() {
1343                    families.insert(family.clone());
1344                }
1345                let node = expression.child(ExprId::from_index(index));
1346                let used: Vec<(TypeFamily, String)> = match node.kind() {
1347                    ExpressionKind::Literal(literal) => {
1348                        unscaled_units(&literal.value, literal.lemma_type.as_ref())
1349                    }
1350                    ExpressionKind::UnitConversion(
1351                        _,
1352                        SemanticConversionTarget::Unit {
1353                            unit_name,
1354                            owning_type,
1355                        },
1356                    ) if owning_type.unit_is_dynamic(unit_name) => vec![(
1357                        owning_type
1358                            .measure_family()
1359                            .expect("BUG: a dynamic conversion unit belongs to a measure family")
1360                            .clone(),
1361                        unit_name.clone(),
1362                    )],
1363                    ExpressionKind::UnitConversion(
1364                        inner,
1365                        SemanticConversionTarget::Type(ConversionTypeKind::Number),
1366                    ) if node_types[inner.index()].is_measure() => {
1367                        let source_type = node_types[inner.index()].as_ref();
1368                        match source_type.measure_runtime_signature().first() {
1369                            Some((unit, _)) if source_type.unit_is_dynamic(unit) => {
1370                                vec![(
1371                                    source_type
1372                                        .measure_family()
1373                                        .expect("BUG: a measure with a dynamic unit has a family")
1374                                        .clone(),
1375                                    unit.clone(),
1376                                )]
1377                            }
1378                            Some(_) | None => Vec::new(),
1379                        }
1380                    }
1381                    _ => Vec::new(),
1382                };
1383                for (family, unit) in used {
1384                    if let Some(message) = self.foreign_scale_message(&family, &unit) {
1385                        let source = node.source().unwrap_or(&rule_node.source);
1386                        errors.push(engine_error_at_graph(self, source, message));
1387                    }
1388                }
1389            }
1390            families_by_rule.push((rule_path.clone(), families));
1391        }
1392
1393        for (rule_path, families) in families_by_rule {
1394            let rule_node = self
1395                .rules
1396                .get_mut(&rule_path)
1397                .expect("BUG: rule from unit-scale planning missing in graph");
1398            for family in &families {
1399                if let Some(rules) = scale_rules.get(family) {
1400                    rule_node.depends_on_rules.extend(rules.iter().cloned());
1401                }
1402            }
1403            rule_node.scale_families = families;
1404        }
1405        errors
1406    }
1407
1408    /// Error message when this spec writes `unit` of `family` but another spec owns
1409    /// its factor; `None` when the unit has a literal factor or this spec owns it.
1410    fn foreign_scale_message(&self, family: &TypeFamily, unit: &str) -> Option<String> {
1411        if self
1412            .unit_scale_targets
1413            .contains_key(&(family.clone(), unit.to_string()))
1414        {
1415            return None;
1416        }
1417        Some(format!(
1418            "unit '{unit}' takes its conversion factor from spec '{}' and cannot be used in spec '{}'",
1419            family.spec, self.main_spec.name
1420        ))
1421    }
1422
1423    /// Lemma type of a resolved unit-scale target. Local targets live on this
1424    /// graph; uses-qualified targets walk import hops into the template plan
1425    /// (same hop walk as [`reference_target_type`]).
1426    fn scale_target_lemma_type(
1427        &self,
1428        target: &ReferenceTarget,
1429        inferred_types: &HashMap<RulePath, Arc<LemmaType>>,
1430    ) -> Arc<LemmaType> {
1431        let segments = match target {
1432            ReferenceTarget::Data(path) => &path.segments,
1433            ReferenceTarget::Rule(rule) => &rule.segments,
1434        };
1435        if !segments.is_empty() {
1436            let mut identity: Option<&SpecIdentity> = None;
1437            for segment in segments {
1438                let slot = DataPath::local(segment.uses.to_string());
1439                let data = match identity {
1440                    None => &self.data,
1441                    Some(identity) => &self.instance_lists.templates[identity].data,
1442                };
1443                let Some(DataDefinition::Import { row, .. }) = data.get(&slot) else {
1444                    panic!("BUG: unit scale target hop '{slot}' of {target:?} is not a uses slot");
1445                };
1446                identity = Some(row.identity.as_ref());
1447            }
1448            let template = &self.instance_lists.templates
1449                [identity.expect("BUG: a hop scale target has an import identity")];
1450            return match target {
1451                ReferenceTarget::Data(path) => instance_list::row_data_type(
1452                    &template.data,
1453                    &DataPath::local(path.data.clone()),
1454                ),
1455                ReferenceTarget::Rule(rule) => instance_list::row_end_type(
1456                    template,
1457                    &RowEnd::Rule(RulePath::new(Vec::new(), rule.rule.clone())),
1458                ),
1459            };
1460        }
1461        match target {
1462            ReferenceTarget::Data(path) => {
1463                let definition = self.data.get(path).unwrap_or_else(|| {
1464                    panic!("BUG: resolved unit scale data target '{path}' missing from graph data")
1465                });
1466                Arc::clone(definition.resolved_type_arc().unwrap_or_else(|| {
1467                    panic!("BUG: unit scale data target '{path}' has no schema type")
1468                }))
1469            }
1470            ReferenceTarget::Rule(path) => {
1471                if let Some(arc) = inferred_types.get(path) {
1472                    return Arc::clone(arc);
1473                }
1474                let rule = self.rules.get(path).unwrap_or_else(|| {
1475                    panic!("BUG: resolved unit scale rule target '{path}' missing from graph rules")
1476                });
1477                Arc::clone(&rule.rule_type)
1478            }
1479        }
1480    }
1481
1482    fn check_unit_scale_target_types(
1483        &self,
1484        resolved_types: &ResolvedTypesMap,
1485        inferred_types: &HashMap<RulePath, Arc<LemmaType>>,
1486    ) -> Vec<Error> {
1487        let Some(types) = find_types_by_spec(resolved_types, self.main_spec) else {
1488            return Vec::new();
1489        };
1490        let mut errors = Vec::new();
1491        for (type_name, lemma_type) in &types.resolved {
1492            let TypeSpecification::Measure { units, .. } = &lemma_type.specifications else {
1493                continue;
1494            };
1495            let source = self
1496                .main_spec
1497                .data
1498                .iter()
1499                .find(|data| data.reference.is_local() && data.reference.name == *type_name)
1500                .map(|data| &data.source_location)
1501                .or_else(|| {
1502                    self.main_spec
1503                        .data
1504                        .first()
1505                        .map(|data| &data.source_location)
1506                })
1507                .or_else(|| {
1508                    self.main_spec
1509                        .rules
1510                        .first()
1511                        .map(|rule| &rule.source_location)
1512                })
1513                .expect("BUG: spec with a measure type has a source location");
1514            for unit in units.iter() {
1515                let Some(target) = unit.resolved_scale_target() else {
1516                    continue;
1517                };
1518                let target_type = self.scale_target_lemma_type(target, inferred_types);
1519                if !target_type.is_number() {
1520                    errors.push(engine_error_at_graph(
1521                        self,
1522                        source,
1523                        format!(
1524                            "unit '{}' conversion factor must be a number, got {}",
1525                            unit.name,
1526                            target_type.name()
1527                        ),
1528                    ));
1529                }
1530            }
1531        }
1532        errors
1533    }
1534
1535    /// Rules a rule forces while it reads rows, as dependency edges. A row's
1536    /// references stay deferred until a read lands on their field: opening the row's
1537    /// context resolves the deferred reference of every field the read touches, in the
1538    /// context the binding names. A reference this plan wrote forces its target rule
1539    /// here; one written inside a row spec runs in that row's context and its reads
1540    /// land on further rows, so the walk follows them until they reach this plan.
1541    /// Evaluation spawns each target rule at that point, so a cycle through rows would
1542    /// re-enter the running rule. Runs after [`Self::link_spec_values`] and before the
1543    /// final topological sort, which reports the cycle.
1544    fn add_stored_row_dependency_edges(&mut self) {
1545        let mut updates: Vec<(RulePath, BTreeSet<RulePath>)> = Vec::new();
1546        for (rule_path, rule_node) in &self.rules {
1547            let edges = self.forced_rules(self.rule_row_reads(rule_node));
1548            if !edges.is_empty() {
1549                updates.push((rule_path.clone(), edges));
1550            }
1551        }
1552        for (rule_path, edges) in updates {
1553            self.rules
1554                .get_mut(&rule_path)
1555                .expect("BUG: rule with stored-row edges missing in graph")
1556                .depends_on_rules
1557                .extend(edges);
1558        }
1559    }
1560
1561    /// Reads rule `rule_node` of this plan makes on rows: row fields of the lists its
1562    /// commands iterate, and members of the rows its expressions yield.
1563    fn rule_row_reads<'s>(&'s self, rule_node: &RuleNode<'_>) -> Vec<ForcedRead<'s>> {
1564        let table = &rule_node.expression.table;
1565        let mut reads: Vec<ForcedRead<'s>> = Vec::new();
1566        // `scope` holds the rows each enclosing list command iterates, by row
1567        // level (outermost first).
1568        let mut visited: HashSet<(ExprId, ScopeKey)> = HashSet::new();
1569        let mut stack: Vec<(ExprId, Vec<Vec<Located<'s>>>)> = Vec::new();
1570        for (condition, result) in &rule_node.branches {
1571            if let Some(condition) = condition {
1572                stack.push((*condition, Vec::new()));
1573            }
1574            stack.push((*result, Vec::new()));
1575        }
1576        while let Some((id, scope)) = stack.pop() {
1577            if !visited.insert((id, scope_key(&scope))) {
1578                continue;
1579            }
1580            match table.kind(id) {
1581                ExpressionKind::RowField { level, end, .. } => {
1582                    for row in level_rows(&scope, *level) {
1583                        reads.push(ForcedRead {
1584                            at: row.clone(),
1585                            end: end.clone(),
1586                        });
1587                    }
1588                }
1589                ExpressionKind::ListCommand {
1590                    list,
1591                    op,
1592                    row_level,
1593                } => {
1594                    stack.push((*list, scope.clone()));
1595                    let mut inner = scope.clone();
1596                    if let Some(level) = row_level {
1597                        let depth =
1598                            u32::try_from(scope.len()).expect("BUG: row scope depth exceeds u32");
1599                        assert!(
1600                                *level == depth,
1601                                "BUG: list command row level {level} but converted at scope depth {depth}"
1602                            );
1603                        inner.push(self.expression_rows(table, *list, &scope));
1604                    }
1605                    match op {
1606                        ListOp::Reduce {
1607                            argument: Some(argument),
1608                            ..
1609                        }
1610                        | ListOp::Sort {
1611                            argument: Some(argument),
1612                            ..
1613                        } => stack.push((*argument, inner)),
1614                        ListOp::Predicate { expr, .. } => stack.push((*expr, inner)),
1615                        ListOp::Find { keys } => {
1616                            for key in keys {
1617                                stack.push((*key, scope.clone()));
1618                            }
1619                        }
1620                        ListOp::Top { n } => stack.push((*n, scope.clone())),
1621                        ListOp::With { rows } => {
1622                            for row in rows {
1623                                stack.push((*row, scope.clone()));
1624                            }
1625                        }
1626                        ListOp::WithRow { fields, .. } => {
1627                            for (_, value) in fields {
1628                                stack.push((*value, scope.clone()));
1629                            }
1630                        }
1631                        ListOp::First
1632                        | ListOp::Last
1633                        | ListOp::Count
1634                        | ListOp::Key { .. }
1635                        | ListOp::Reduce { argument: None, .. }
1636                        | ListOp::Sort { argument: None, .. } => {}
1637                    }
1638                }
1639                ExpressionKind::SpecMember {
1640                    base, path, name, ..
1641                } => {
1642                    for row in self.expression_rows(table, *base, &scope) {
1643                        if let Some(read) = self.member_read(row, path, name) {
1644                            reads.push(read);
1645                        }
1646                    }
1647                    stack.push((*base, scope.clone()));
1648                }
1649                kind => kind.for_each_child(|child| stack.push((child, scope.clone()))),
1650            }
1651        }
1652        reads
1653    }
1654
1655    /// Rules of this plan the reads `reads` force, following every read they open.
1656    fn forced_rules<'s>(&'s self, mut reads: Vec<ForcedRead<'s>>) -> BTreeSet<RulePath> {
1657        let mut edges: BTreeSet<RulePath> = BTreeSet::new();
1658        let mut visited: HashSet<(Vec<RowKey>, RowEnd)> = HashSet::new();
1659        while let Some(read) = reads.pop() {
1660            if !visited.insert((read.at.key(), read.end.clone())) {
1661                continue;
1662            }
1663            let row = read.at.row;
1664            let template = self.template_of_row(row);
1665            let slots: Vec<DataPath> = match &read.end {
1666                RowEnd::Data(path) => vec![path.clone()],
1667                RowEnd::Rule(_) => template
1668                    .row_end_reads(&read.end)
1669                    .into_iter()
1670                    .map(|field_read| field_read.slot)
1671                    .collect(),
1672            };
1673            for slot in &slots {
1674                if let Some(RowBinding::Deferred { target, hops }) = row.get_binding(template, slot)
1675                {
1676                    let owner = read.at.scope(*hops);
1677                    self.target_read(owner, target, &mut reads, &mut edges);
1678                }
1679            }
1680            if let RowEnd::Rule(rule) = &read.end {
1681                self.template_rule_reads(template, rule, &read.at, &mut reads, RuleWalk::Follow);
1682            }
1683        }
1684        edges
1685    }
1686
1687    /// The Show graph through `uses` imports: every import rule the rules of this plan
1688    /// reach, read on its instance, with the edges and the unfilled data reads of each.
1689    fn reached_imports(&self) -> ShowImports {
1690        let mut out = ShowImports::default();
1691        let mut pending: Vec<(Located<'_>, RulePath)> = Vec::new();
1692        for (rule_path, rule_node) in &self.rules {
1693            let (edges, data) =
1694                self.show_reads(self.rule_row_reads(rule_node), &mut pending, &mut out);
1695            out.edges.insert(rule_path.clone(), edges);
1696            out.data_reads.insert(rule_path.clone(), data);
1697        }
1698        while let Some((at, rule)) = pending.pop() {
1699            let path = RulePath::new(
1700                at.import_segments()
1701                    .expect("BUG: a pending Show rule sits on a `uses` import chain"),
1702                rule.rule.clone(),
1703            );
1704            if out.rules.contains_key(&path) {
1705                continue;
1706            }
1707            let template = self.template_of_row(at.row);
1708            out.rules.insert(
1709                path.clone(),
1710                (at.row.identity.as_ref().clone(), rule.clone()),
1711            );
1712            let mut reads = Vec::new();
1713            self.template_rule_reads(template, &rule, &at, &mut reads, RuleWalk::Show);
1714            let (mut edges, data) = self.show_reads(reads, &mut pending, &mut out);
1715            out.need_edges.insert(path.clone(), edges.clone());
1716            let show_rule = template.show_rules.get(&rule).unwrap_or_else(|| {
1717                panic!(
1718                    "BUG: rule '{rule}' missing from Show of template '{}'",
1719                    template.spec_name
1720                )
1721            });
1722            let mut reads = Vec::new();
1723            for dependency in &show_rule.depends_on_rules {
1724                let at = if dependency.segments.is_empty() {
1725                    at.clone()
1726                } else {
1727                    let aliases: Vec<String> = dependency
1728                        .segments
1729                        .iter()
1730                        .map(|segment| segment.uses.to_string())
1731                        .collect();
1732                    self.locate_instance(Some(at.clone()), &aliases)
1733                };
1734                reads.push(ForcedRead {
1735                    at,
1736                    end: RowEnd::Rule(RulePath::new(Vec::new(), dependency.rule.clone())),
1737                });
1738            }
1739            let (written, written_data) = self.show_reads(reads, &mut pending, &mut out);
1740            assert!(written_data.is_empty(), "BUG: a rule dependency read data");
1741            edges.extend(written);
1742            out.edges.insert(path.clone(), edges);
1743            out.data_reads.insert(path, data);
1744        }
1745        out
1746    }
1747
1748    /// Show edges and unfilled data reads of the reads `reads`. A rule read on a `uses`
1749    /// import chain is an edge to that import rule, queued on `pending`; a rule of this
1750    /// plan is an edge; a data read follows its binding or reference to where it ends.
1751    /// Reads on list rows follow every read they open, as [`Self::forced_rules`] does.
1752    fn show_reads<'s>(
1753        &'s self,
1754        mut reads: Vec<ForcedRead<'s>>,
1755        pending: &mut Vec<(Located<'s>, RulePath)>,
1756        out: &mut ShowImports,
1757    ) -> (BTreeSet<RulePath>, BTreeSet<DataPath>) {
1758        let mut edges: BTreeSet<RulePath> = BTreeSet::new();
1759        let mut data: BTreeSet<DataPath> = BTreeSet::new();
1760        let mut visited: HashSet<(Vec<RowKey>, RowEnd)> = HashSet::new();
1761        let mut landings: Vec<Landing<'s>> = Vec::new();
1762        loop {
1763            while let Some(landing) = landings.pop() {
1764                match landing {
1765                    Landing::Row(read) => reads.push(read),
1766                    Landing::Plan(RowEnd::Rule(rule)) => {
1767                        edges.insert(rule);
1768                    }
1769                    Landing::Plan(RowEnd::Data(path)) => match self.data.get(&path) {
1770                        Some(DataDefinition::Reference { .. }) => {
1771                            match self.reference_ends.get(&path).unwrap_or_else(|| {
1772                                panic!("BUG: reference '{path}' missing from reference_ends")
1773                            }) {
1774                                ReferenceEnd::Rule(rule) => landings.push(
1775                                    self.target_landing(None, &ReferenceTarget::Rule(rule.clone())),
1776                                ),
1777                                ReferenceEnd::Promptable(target) => {
1778                                    landings.push(self.target_landing(
1779                                        None,
1780                                        &ReferenceTarget::Data(target.clone()),
1781                                    ))
1782                                }
1783                            }
1784                        }
1785                        Some(DataDefinition::Import { .. }) => {}
1786                        Some(definition) => {
1787                            if !definition.is_bound() {
1788                                data.insert(path);
1789                            }
1790                        }
1791                        None => panic!("BUG: Show read of data '{path}' missing from the plan"),
1792                    },
1793                }
1794            }
1795            let Some(read) = reads.pop() else {
1796                break;
1797            };
1798            if !visited.insert((read.at.key(), read.end.clone())) {
1799                continue;
1800            }
1801            let row = read.at.row;
1802            let template = self.template_of_row(row);
1803            let segments = read.at.import_segments();
1804            match (&read.end, segments) {
1805                (RowEnd::Rule(rule), Some(segments)) => {
1806                    edges.insert(RulePath::new(segments, rule.rule.clone()));
1807                    pending.push((read.at, rule.clone()));
1808                }
1809                (RowEnd::Rule(rule), None) => {
1810                    for field_read in template.row_end_reads(&read.end) {
1811                        if let Some(RowBinding::Deferred { target, hops }) =
1812                            row.get_binding(template, &field_read.slot)
1813                        {
1814                            landings.push(self.target_landing(read.at.scope(*hops), target));
1815                        }
1816                    }
1817                    self.template_rule_reads(
1818                        template,
1819                        rule,
1820                        &read.at,
1821                        &mut reads,
1822                        RuleWalk::Follow,
1823                    );
1824                }
1825                (RowEnd::Data(slot), segments) => {
1826                    match row.get_binding(template, slot) {
1827                        Some(RowBinding::Deferred { target, hops }) => {
1828                            landings.push(self.target_landing(read.at.scope(*hops), target));
1829                        }
1830                        Some(_) => {}
1831                        None => {
1832                            let Some(segments) = segments else {
1833                                continue;
1834                            };
1835                            match template.data.get(slot) {
1836                                Some(DataDefinition::Reference { .. }) => {
1837                                    let target = match template.reference_ends.get(slot).unwrap_or_else(
1838                                    || panic!("BUG: reference '{slot}' missing from reference_ends"),
1839                                ) {
1840                                    ReferenceEnd::Rule(rule) => ReferenceTarget::Rule(rule.clone()),
1841                                    ReferenceEnd::Promptable(target) => {
1842                                        ReferenceTarget::Data(target.clone())
1843                                    }
1844                                };
1845                                    landings
1846                                        .push(self.target_landing(Some(read.at.clone()), &target));
1847                                }
1848                                Some(DataDefinition::Import { .. }) => {}
1849                                Some(definition) => {
1850                                    if !definition.is_bound() {
1851                                        let path = DataPath {
1852                                            segments,
1853                                            data: slot.data.clone(),
1854                                        };
1855                                        out.data
1856                                            .insert(path.clone(), row.identity.as_ref().clone());
1857                                        data.insert(path);
1858                                    }
1859                                }
1860                                None => panic!(
1861                                    "BUG: Show read of '{slot}' missing from template '{}'",
1862                                    template.spec_name
1863                                ),
1864                            }
1865                        }
1866                    }
1867                }
1868            }
1869        }
1870        (edges, data)
1871    }
1872
1873    /// The read `target` makes in context `owner` (`None` for this plan): a rule of
1874    /// this plan becomes an edge; any other target is a read on the row it names.
1875    fn target_read<'s>(
1876        &'s self,
1877        owner: Option<Located<'s>>,
1878        target: &ReferenceTarget,
1879        reads: &mut Vec<ForcedRead<'s>>,
1880        edges: &mut BTreeSet<RulePath>,
1881    ) {
1882        match self.target_landing(owner, target) {
1883            Landing::Row(read) => reads.push(read),
1884            Landing::Plan(RowEnd::Rule(rule)) => {
1885                edges.insert(rule);
1886            }
1887            Landing::Plan(RowEnd::Data(_)) => {}
1888        }
1889    }
1890
1891    /// Where `target`, read in context `owner` (`None` for this plan), lands.
1892    fn target_landing<'s>(
1893        &'s self,
1894        owner: Option<Located<'s>>,
1895        target: &ReferenceTarget,
1896    ) -> Landing<'s> {
1897        let (segments, end) = match target {
1898            ReferenceTarget::Rule(rule) => (
1899                &rule.segments,
1900                RowEnd::Rule(RulePath::new(Vec::new(), rule.rule.clone())),
1901            ),
1902            ReferenceTarget::Data(path) => (
1903                &path.segments,
1904                RowEnd::Data(DataPath::local(path.data.clone())),
1905            ),
1906        };
1907        let aliases: Vec<String> = segments
1908            .iter()
1909            .map(|segment| {
1910                assert!(
1911                    segment.row.is_none(),
1912                    "BUG: reference target hops are `uses` slots, got row hop `{}`",
1913                    segment.input_key()
1914                );
1915                segment.uses.to_string()
1916            })
1917            .collect();
1918        let at = match (owner, aliases.is_empty()) {
1919            (None, true) => return Landing::Plan(end),
1920            (owner, _) if aliases.is_empty() => owner.expect("BUG: owner checked above"),
1921            (owner, _) => self.locate_instance(owner, &aliases),
1922        };
1923        Landing::Row(ForcedRead { at, end })
1924    }
1925
1926    /// Reads landing on rows while `rule` of `template` evaluates in the context of
1927    /// row `at`: every [`NormalFormKind::RowField`] reads rows of the list command at
1928    /// its level, and a [`NormalFormKind::SpecMember`] reads the rows its base yields.
1929    /// Rule-reference cells walk the referenced rule in the same context.
1930    ///
1931    /// [`RuleWalk::Show`] stops at rule references instead, reading the rule on `at`, and
1932    /// also reads every data slot of `template` the rule reads on `at`: the direct reads
1933    /// of one Show rule.
1934    fn template_rule_reads<'s>(
1935        &'s self,
1936        template: &'s ExecutionPlan,
1937        rule: &RulePath,
1938        at: &Located<'s>,
1939        reads: &mut Vec<ForcedRead<'s>>,
1940        walk: RuleWalk,
1941    ) {
1942        let mut rules: Vec<RulePath> = vec![rule.clone()];
1943        let mut walked: BTreeSet<RulePath> = BTreeSet::new();
1944        let mut visited: HashSet<(NormalFormId, ScopeKey)> = HashSet::new();
1945        while let Some(rule) = rules.pop() {
1946            if !walked.insert(rule.clone()) {
1947                continue;
1948            }
1949            let root = template
1950                .rules
1951                .get(&rule)
1952                .unwrap_or_else(|| {
1953                    panic!(
1954                        "BUG: row rule '{rule}' missing from template plan '{}'",
1955                        template.spec_name
1956                    )
1957                })
1958                .normal_form;
1959            let mut stack: Vec<(NormalFormId, Vec<Vec<Located<'s>>>)> = vec![(root, Vec::new())];
1960            while let Some((id, scope)) = stack.pop() {
1961                if !visited.insert((id, scope_key(&scope))) {
1962                    continue;
1963                }
1964                let nf = template.normal_form(id);
1965                if let Some(target) = &nf.rule_ref {
1966                    match walk {
1967                        RuleWalk::Follow => rules.push(target.clone()),
1968                        RuleWalk::Show => reads.push(ForcedRead {
1969                            at: at.clone(),
1970                            end: RowEnd::Rule(target.clone()),
1971                        }),
1972                    }
1973                    continue;
1974                }
1975                match &nf.kind {
1976                    NormalFormKind::Leaf(LeafKind::DataPath(path)) if walk == RuleWalk::Show => {
1977                        if !matches!(template.data.get(path), Some(DataDefinition::Import { .. })) {
1978                            reads.push(ForcedRead {
1979                                at: at.clone(),
1980                                end: RowEnd::Data(path.clone()),
1981                            });
1982                        }
1983                        if let Some(options) = template.options_rule_of(path) {
1984                            reads.push(ForcedRead {
1985                                at: at.clone(),
1986                                end: RowEnd::Rule(options),
1987                            });
1988                        }
1989                    }
1990                    NormalFormKind::RowField { level, end, .. } => {
1991                        for row in level_rows(&scope, *level) {
1992                            reads.push(ForcedRead {
1993                                at: row.clone(),
1994                                end: end.clone(),
1995                            });
1996                        }
1997                    }
1998                    NormalFormKind::SpecMember { base, row, .. } => {
1999                        let (last, hops) = row
2000                            .ends
2001                            .split_last()
2002                            .expect("BUG: a row read has a last end");
2003                        for base_row in self.normal_cell_rows(template, *base, at, &scope) {
2004                            let mut held = base_row;
2005                            for hop in hops {
2006                                let RowEnd::Data(slot) = hop else {
2007                                    panic!("BUG: a row read hop is a `uses` slot, got {hop:?}");
2008                                };
2009                                held = self.import_hop(held, slot);
2010                            }
2011                            reads.push(ForcedRead {
2012                                at: held,
2013                                end: last.clone(),
2014                            });
2015                        }
2016                        stack.push((*base, scope.clone()));
2017                    }
2018                    NormalFormKind::ListCommand {
2019                        list,
2020                        args,
2021                        row_level,
2022                        ..
2023                    } => {
2024                        stack.push((*list, scope.clone()));
2025                        let mut inner = scope.clone();
2026                        if let Some(level) = row_level {
2027                            let depth = u32::try_from(scope.len())
2028                                .expect("BUG: row scope depth exceeds u32");
2029                            assert!(
2030                                *level == depth,
2031                                "BUG: list command row level {level} but walked at scope depth {depth}"
2032                            );
2033                            inner.push(self.normal_cell_rows(template, *list, at, &scope));
2034                        }
2035                        for arg in args {
2036                            stack.push((*arg, inner.clone()));
2037                        }
2038                    }
2039                    kind => kind.for_each_child(|child| stack.push((child, scope.clone()))),
2040                }
2041            }
2042        }
2043    }
2044
2045    /// Rows the cell `id` of `template` can yield while it evaluates in the context of
2046    /// row `at`, with `scope` the rows of the enclosing list commands. A single row
2047    /// picked from a list (`first`, `find`) stands in for all of the list's rows.
2048    fn normal_cell_rows<'s>(
2049        &'s self,
2050        template: &'s ExecutionPlan,
2051        id: NormalFormId,
2052        at: &Located<'s>,
2053        scope: &[Vec<Located<'s>>],
2054    ) -> Vec<Located<'s>> {
2055        let mut memo: HashMap<NormalFormId, Vec<Located<'s>>> = HashMap::new();
2056        let mut stack: Vec<(NormalFormId, bool)> = vec![(id, false)];
2057        while let Some((cell, ready)) = stack.pop() {
2058            if memo.contains_key(&cell) {
2059                continue;
2060            }
2061            let nf = template.normal_form(cell);
2062            let children: Vec<NormalFormId> = if nf.rule_ref.is_some() {
2063                Vec::new()
2064            } else {
2065                match &nf.kind {
2066                    NormalFormKind::SpecMember { base, .. } => vec![*base],
2067                    NormalFormKind::ListCommand {
2068                        list,
2069                        op: NormalFormListOp::With,
2070                        args,
2071                        ..
2072                    } => std::iter::once(*list).chain(args.iter().copied()).collect(),
2073                    NormalFormKind::ListCommand { list, .. } => vec![*list],
2074                    NormalFormKind::Piecewise(arms) => arms.iter().map(|(_, body)| *body).collect(),
2075                    _ => Vec::new(),
2076                }
2077            };
2078            if !ready {
2079                stack.push((cell, true));
2080                stack.extend(children.into_iter().map(|child| (child, false)));
2081                continue;
2082            }
2083            let child_rows = |memo: &HashMap<NormalFormId, Vec<Located<'s>>>| -> Vec<Located<'s>> {
2084                children
2085                    .iter()
2086                    .flat_map(|child| memo[child].iter().cloned())
2087                    .collect()
2088            };
2089            let rows = match (&nf.rule_ref, &nf.kind) {
2090                (Some(rule), _) => self.end_rows(at.clone(), &RowEnd::Rule(rule.clone())),
2091                (None, NormalFormKind::Leaf(LeafKind::DataPath(path))) => {
2092                    self.end_rows(at.clone(), &RowEnd::Data(path.clone()))
2093                }
2094                (None, NormalFormKind::Leaf(LeafKind::RowValue { level })) => {
2095                    level_rows(scope, *level).to_vec()
2096                }
2097                (None, NormalFormKind::RowField { level, end, .. }) => level_rows(scope, *level)
2098                    .iter()
2099                    .flat_map(|row| self.end_rows(row.clone(), end))
2100                    .collect(),
2101                (None, NormalFormKind::SpecMember { row, .. }) => child_rows(&memo)
2102                    .into_iter()
2103                    .flat_map(|base| self.ends_rows(base, &row.ends))
2104                    .collect(),
2105                (None, NormalFormKind::ListCommand { .. } | NormalFormKind::Piecewise(_)) => {
2106                    child_rows(&memo)
2107                }
2108                (None, _) => Vec::new(),
2109            };
2110            memo.insert(cell, rows);
2111        }
2112        memo.remove(&id)
2113            .expect("BUG: the requested cell is resolved last")
2114    }
2115
2116    /// Rows the expression `id` of a rule of this plan can yield, with `scope` the
2117    /// rows of the enclosing list commands. A single row picked from a list
2118    /// (`first`, `find`) stands in for all of the list's rows.
2119    fn expression_rows<'s>(
2120        &'s self,
2121        table: &ExprTable<ExpressionKind>,
2122        id: ExprId,
2123        scope: &[Vec<Located<'s>>],
2124    ) -> Vec<Located<'s>> {
2125        let mut memo: HashMap<ExprId, Vec<Located<'s>>> = HashMap::new();
2126        let mut stack: Vec<(ExprId, bool)> = vec![(id, false)];
2127        while let Some((at, ready)) = stack.pop() {
2128            if memo.contains_key(&at) {
2129                continue;
2130            }
2131            let children: Vec<ExprId> = match table.kind(at) {
2132                ExpressionKind::SpecMember { base, .. } => vec![*base],
2133                ExpressionKind::ListCommand {
2134                    list,
2135                    op: ListOp::With { rows },
2136                    ..
2137                } => std::iter::once(*list).chain(rows.iter().copied()).collect(),
2138                ExpressionKind::ListCommand { list, .. } => vec![*list],
2139                ExpressionKind::Piecewise(arms) => arms.iter().map(|(_, body)| *body).collect(),
2140                _ => Vec::new(),
2141            };
2142            if !ready {
2143                stack.push((at, true));
2144                stack.extend(children.into_iter().map(|child| (child, false)));
2145                continue;
2146            }
2147            let child_rows: Vec<Located<'s>> = children
2148                .iter()
2149                .flat_map(|child| memo[child].iter().cloned())
2150                .collect();
2151            let rows = match table.kind(at) {
2152                ExpressionKind::DataPath(path) => self.plan_slot_rows(path),
2153                ExpressionKind::RulePath(rule) => match self.rule_candidates.get(rule) {
2154                    Some(candidates) => candidates
2155                        .instances
2156                        .iter()
2157                        .map(|instance| self.locate_instance(None, instance))
2158                        .collect(),
2159                    None => {
2160                        assert!(
2161                            self.dropped_rules.contains(rule),
2162                            "BUG: rule '{rule}' is linked but has no candidates"
2163                        );
2164                        Vec::new()
2165                    }
2166                },
2167                ExpressionKind::RowValue { level, .. } => level_rows(scope, *level).to_vec(),
2168                ExpressionKind::RowField { level, end, .. } => level_rows(scope, *level)
2169                    .iter()
2170                    .flat_map(|row| self.end_rows(row.clone(), end))
2171                    .collect(),
2172                ExpressionKind::SpecMember { path, name, .. } => child_rows
2173                    .into_iter()
2174                    .flat_map(|base| {
2175                        let ends = self.member_ends(base.row, path, name);
2176                        match ends {
2177                            Some(ends) => self.ends_rows(base, &ends),
2178                            None => Vec::new(),
2179                        }
2180                    })
2181                    .collect(),
2182                ExpressionKind::ListCommand { .. } | ExpressionKind::Piecewise(_) => child_rows,
2183                _ => Vec::new(),
2184            };
2185            memo.insert(at, rows);
2186        }
2187        memo.remove(&id)
2188            .expect("BUG: the requested expression is resolved last")
2189    }
2190
2191    /// Rows data `path` of this plan holds: its `uses` instance, or the rows its list
2192    /// writes.
2193    fn plan_slot_rows(&self, path: &DataPath) -> Vec<Located<'_>> {
2194        match self.data.get(path) {
2195            Some(DataDefinition::Import { row, .. }) => vec![Located::root(row)],
2196            Some(DataDefinition::InstanceList { .. }) => self
2197                .instance_lists
2198                .sources
2199                .get(path)
2200                .expect("BUG: instance list data has a source")
2201                .store
2202                .shape_rows()
2203                .map(|row| Located::root(row))
2204                .collect(),
2205            _ => Vec::new(),
2206        }
2207    }
2208
2209    /// Ends `path.name` reads on a row of `row`'s spec; `None` when its spec has no
2210    /// such member (type checking reports it).
2211    fn member_ends(&self, row: &RowInstance, path: &[String], name: &str) -> Option<Vec<RowEnd>> {
2212        let parts: Vec<&str> = path
2213            .iter()
2214            .map(String::as_str)
2215            .chain(std::iter::once(name))
2216            .collect();
2217        let template = self.template_of_row(row);
2218        instance_list::row_end_chain(
2219            &row.identity,
2220            instance_list::RowEndView::Plan(template),
2221            &self.instance_lists.templates,
2222            &parts,
2223        )
2224        .map(|chain| chain.into_iter().map(|hop| hop.end).collect())
2225    }
2226
2227    /// The read `path.name` makes on row `at`.
2228    fn member_read<'s>(
2229        &'s self,
2230        at: Located<'s>,
2231        path: &[String],
2232        name: &str,
2233    ) -> Option<ForcedRead<'s>> {
2234        let ends = self.member_ends(at.row, path, name)?;
2235        let (last, hops) = ends
2236            .split_last()
2237            .expect("BUG: a member chain has a last end");
2238        let mut at = at;
2239        for hop in hops {
2240            let RowEnd::Data(slot) = hop else {
2241                panic!("BUG: a member chain hop is a `uses` slot, got {hop:?}");
2242            };
2243            at = self.import_hop(at, slot);
2244        }
2245        Some(ForcedRead {
2246            at,
2247            end: last.clone(),
2248        })
2249    }
2250
2251    /// Rows the chain `ends` yields on row `at`.
2252    fn ends_rows<'s>(&'s self, at: Located<'s>, ends: &[RowEnd]) -> Vec<Located<'s>> {
2253        let (last, hops) = ends.split_last().expect("BUG: a row read has a last end");
2254        let mut at = at;
2255        for hop in hops {
2256            let RowEnd::Data(slot) = hop else {
2257                panic!("BUG: a row read hop is a `uses` slot, got {hop:?}");
2258            };
2259            at = self.import_hop(at, slot);
2260        }
2261        self.end_rows(at, last)
2262    }
2263
2264    /// Rows a read of `end` on row `at` yields: the instance of a `uses` slot, the
2265    /// rows of a list field, or the instances a spec-valued rule of the row's spec
2266    /// returns.
2267    fn end_rows<'s>(&'s self, at: Located<'s>, end: &RowEnd) -> Vec<Located<'s>> {
2268        let template = self.template_of_row(at.row);
2269        match end {
2270            RowEnd::Rule(rule) => template
2271                .rule_candidates
2272                .get(rule)
2273                .unwrap_or_else(|| {
2274                    panic!(
2275                        "BUG: rule '{rule}' of '{}' has no candidate list",
2276                        template.spec_name
2277                    )
2278                })
2279                .instances
2280                .iter()
2281                .map(|instance| self.locate_instance(Some(at.clone()), instance))
2282                .collect(),
2283            RowEnd::Data(path) => match template.data.get(path) {
2284                Some(DataDefinition::Import { .. }) => vec![self.import_hop(at, path)],
2285                Some(DataDefinition::InstanceList { .. }) => {
2286                    match at.row.get_binding(template, path) {
2287                        Some(RowBinding::Rows { rows, hops, .. }) => {
2288                            let owner = at.context_at(*hops);
2289                            rows.iter()
2290                                .map(|row| Located {
2291                                    row,
2292                                    context: owner.clone(),
2293                                })
2294                                .collect()
2295                        }
2296                        Some(RowBinding::Bound(_)) | None => {
2297                            let context = at.inside();
2298                            template
2299                                .instance_lists
2300                                .get(path)
2301                                .expect("BUG: template instance list has a source")
2302                                .store
2303                                .shape_rows()
2304                                .map(|row| Located {
2305                                    row,
2306                                    context: context.clone(),
2307                                })
2308                                .collect()
2309                        }
2310                        Some(binding) => {
2311                            panic!("BUG: list field '{path}' holds {binding:?}")
2312                        }
2313                    }
2314                }
2315                _ => Vec::new(),
2316            },
2317        }
2318    }
2319
2320    /// The instance in `uses` slot `slot` of row `at`: the nested row a consumer wrote,
2321    /// or the row the row spec declares.
2322    fn import_hop<'s>(&'s self, at: Located<'s>, slot: &DataPath) -> Located<'s> {
2323        let template = self.template_of_row(at.row);
2324        let row = match at.row.get_binding(template, slot) {
2325            Some(RowBinding::Instance(row)) => row.as_ref(),
2326            None => match template.data.get(slot) {
2327                Some(DataDefinition::Import { row, .. }) => row.as_ref(),
2328                other => panic!("BUG: `uses` hop '{slot}' is not a `uses` slot: {other:?}"),
2329            },
2330            Some(binding) => panic!("BUG: `uses` slot '{slot}' holds {binding:?}"),
2331        };
2332        Located {
2333            row,
2334            context: at.inside(),
2335        }
2336    }
2337
2338    /// The instance `aliases` names from context `from` (`None` for this plan).
2339    fn locate_instance<'s>(&'s self, from: Option<Located<'s>>, aliases: &[String]) -> Located<'s> {
2340        let (first, rest) = aliases
2341            .split_first()
2342            .expect("BUG: an instance has an alias");
2343        let first = DataPath::local(first.clone());
2344        let mut at = match from {
2345            Some(from) => self.import_hop(from, &first),
2346            None => match self.data.get(&first) {
2347                Some(DataDefinition::Import { row, .. }) => Located::root(row),
2348                other => panic!("BUG: instance `{first}` is not a `uses` of the plan: {other:?}"),
2349            },
2350        };
2351        for alias in rest {
2352            at = self.import_hop(at, &DataPath::local(alias.clone()));
2353        }
2354        at
2355    }
2356
2357    /// Template plan of the spec of `row`.
2358    fn template_of_row(&self, row: &RowInstance) -> &ExecutionPlan {
2359        self.instance_lists
2360            .templates
2361            .get(&row.identity)
2362            .expect("BUG: rows are rows of a planned row spec")
2363    }
2364
2365    /// For each [`DataDefinition::Reference`] in `self.data`, follow the chain to
2366    /// its end (promptable slot or rule). Cycles among data-target references are
2367    /// impossible here because `compute_data_reference_order` already rejected them.
2368    fn compute_reference_ends(&self) -> IndexMap<DataPath, ReferenceEnd> {
2369        let mut out: IndexMap<DataPath, ReferenceEnd> = IndexMap::new();
2370        for (path, def) in &self.data {
2371            let DataDefinition::Reference { target, .. } = def else {
2372                continue;
2373            };
2374            match target {
2375                ReferenceTarget::Rule(rule_path) => {
2376                    out.insert(path.clone(), ReferenceEnd::Rule(rule_path.clone()));
2377                }
2378                ReferenceTarget::Data(start) => {
2379                    out.insert(path.clone(), self.expect_reference_end(start));
2380                }
2381            }
2382        }
2383        out
2384    }
2385
2386    /// Where the data path `start` ends: itself when it holds a slot, else the end
2387    /// of the reference chain it starts. Every target was validated by planning.
2388    fn expect_reference_end(&self, start: &DataPath) -> ReferenceEnd {
2389        let mut path = start;
2390        for _ in 0..=self.data.len() {
2391            match self.data.get(path) {
2392                Some(
2393                    DataDefinition::Value { .. }
2394                    | DataDefinition::TypeDeclaration { .. }
2395                    | DataDefinition::InstanceList { .. },
2396                ) => return ReferenceEnd::Promptable(path.clone()),
2397                Some(DataDefinition::Reference {
2398                    target: ReferenceTarget::Rule(rule),
2399                    ..
2400                }) => return ReferenceEnd::Rule(rule.clone()),
2401                Some(DataDefinition::Reference {
2402                    target: ReferenceTarget::Data(next),
2403                    ..
2404                }) => path = next,
2405                Some(DataDefinition::Import { .. }) => {
2406                    panic!("BUG: data reference chain from '{start}' ends at import '{path}'")
2407                }
2408                None => {
2409                    panic!("BUG: data-target reference chain from '{start}' ends at missing data '{path}'")
2410                }
2411            }
2412        }
2413        panic!("BUG: data reference chain from '{start}' outlasts plan data (cycles are rejected)")
2414    }
2415
2416    /// Compute an order in which data-target references can be evaluated at
2417    /// runtime so each reference's target (when itself a reference) has been
2418    /// evaluated first. Rule-target references are intentionally excluded —
2419    /// they are resolved lazily on first read in the evaluator from the
2420    /// already-evaluated target rule's result. Cycles among data-target
2421    /// references are reported as planning errors.
2422    fn compute_data_reference_order(&self) -> Result<Vec<DataPath>, Vec<Error>> {
2423        let reference_paths: Vec<DataPath> = self
2424            .data
2425            .iter()
2426            .filter_map(|(p, d)| match d {
2427                DataDefinition::Reference {
2428                    target: ReferenceTarget::Data(_),
2429                    ..
2430                } => Some(p.clone()),
2431                _ => None,
2432            })
2433            .collect();
2434
2435        if reference_paths.is_empty() {
2436            return Ok(Vec::new());
2437        }
2438
2439        let reference_set: BTreeSet<DataPath> = reference_paths.iter().cloned().collect();
2440        let mut in_degree: BTreeMap<DataPath, usize> = BTreeMap::new();
2441        let mut dependents: BTreeMap<DataPath, Vec<DataPath>> = BTreeMap::new();
2442        for p in &reference_paths {
2443            in_degree.insert(p.clone(), 0);
2444            dependents.insert(p.clone(), Vec::new());
2445        }
2446
2447        for p in &reference_paths {
2448            let Some(DataDefinition::Reference {
2449                target: ReferenceTarget::Data(target_path),
2450                ..
2451            }) = self.data.get(p)
2452            else {
2453                unreachable!("BUG: reference path collected above is not a data-target reference");
2454            };
2455            if reference_set.contains(target_path) {
2456                *in_degree
2457                    .get_mut(p)
2458                    .expect("BUG: reference missing in_degree") += 1;
2459                dependents
2460                    .get_mut(target_path)
2461                    .expect("BUG: reference missing dependents list")
2462                    .push(p.clone());
2463            }
2464        }
2465
2466        let mut queue: VecDeque<DataPath> = in_degree
2467            .iter()
2468            .filter(|(_, d)| **d == 0)
2469            .map(|(p, _)| p.clone())
2470            .collect();
2471
2472        let mut result: Vec<DataPath> = Vec::new();
2473        while let Some(path) = queue.pop_front() {
2474            result.push(path.clone());
2475            if let Some(deps) = dependents.get(&path) {
2476                for dependent in deps.clone() {
2477                    let degree = in_degree
2478                        .get_mut(&dependent)
2479                        .expect("BUG: reference dependent missing in_degree");
2480                    *degree -= 1;
2481                    if *degree == 0 {
2482                        queue.push_back(dependent);
2483                    }
2484                }
2485            }
2486        }
2487
2488        if result.len() != reference_paths.len() {
2489            let cycle_members: Vec<DataPath> = reference_paths
2490                .iter()
2491                .filter(|p| !result.contains(p))
2492                .cloned()
2493                .collect();
2494            let cycle_display: String = cycle_members
2495                .iter()
2496                .map(|p| p.to_string())
2497                .collect::<Vec<_>>()
2498                .join(", ");
2499            let errors: Vec<Error> = cycle_members
2500                .iter()
2501                .map(|p| {
2502                    let source = self
2503                        .data
2504                        .get(p)
2505                        .expect("BUG: cycle member collected from self.data")
2506                        .source();
2507                    reference_error(
2508                        self.main_spec,
2509                        source,
2510                        format!("Circular data reference ({})", cycle_display),
2511                    )
2512                })
2513                .collect();
2514            return Err(errors);
2515        }
2516
2517        Ok(result)
2518    }
2519
2520    /// Cycles among the data bindings of the `uses` instances this plan writes
2521    /// (`-> with a: i.b` and `-> with b: i.a`): a chain of data reads through bindings
2522    /// and references that returns to a read it already made. Each is an error naming
2523    /// the data on the cycle by its path from this plan.
2524    fn check_binding_reference_cycles(&self) -> Result<(), Vec<Error>> {
2525        enum Step<'s> {
2526            Row(Located<'s>, DataPath),
2527            Plan(DataPath),
2528        }
2529        let mut errors = Vec::new();
2530        let mut reported: BTreeSet<Vec<String>> = BTreeSet::new();
2531        let mut rows: Vec<(Located<'_>, &Source)> = Vec::new();
2532        for definition in self.data.values() {
2533            if let DataDefinition::Import { row, source, .. } = definition {
2534                rows.push((Located::root(row), source));
2535            }
2536        }
2537        while let Some((at, source)) = rows.pop() {
2538            let template = self.template_of_row(at.row);
2539            for (slot, binding) in at.row.bindings.iter().enumerate() {
2540                match binding {
2541                    Some(RowBinding::Instance(nested)) => rows.push((
2542                        Located {
2543                            row: nested,
2544                            context: at.inside(),
2545                        },
2546                        source,
2547                    )),
2548                    Some(RowBinding::Deferred {
2549                        target: ReferenceTarget::Data(_),
2550                        ..
2551                    }) => {
2552                        let (path, _) = template
2553                            .data
2554                            .get_index(slot)
2555                            .expect("BUG: row binding slot is data of its template");
2556                        let mut chain: Vec<(Vec<RowKey>, DataPath)> = Vec::new();
2557                        let mut names: Vec<String> = Vec::new();
2558                        let mut step = Step::Row(at.clone(), path.clone());
2559                        loop {
2560                            let landing = match step {
2561                                Step::Plan(path) => match self.data.get(&path) {
2562                                    Some(DataDefinition::Reference {
2563                                        target: target @ ReferenceTarget::Data(_),
2564                                        ..
2565                                    }) => self.target_landing(None, target),
2566                                    _ => break,
2567                                },
2568                                Step::Row(at, slot) => {
2569                                    let read = (at.key(), slot.clone());
2570                                    if let Some(start) = chain.iter().position(|seen| *seen == read)
2571                                    {
2572                                        let mut cycle = names[start..].to_vec();
2573                                        cycle.sort();
2574                                        if reported.insert(cycle.clone()) {
2575                                            errors.push(reference_error(
2576                                                self.main_spec,
2577                                                source,
2578                                                format!(
2579                                                    "Circular data reference ({})",
2580                                                    cycle.join(", ")
2581                                                ),
2582                                            ));
2583                                        }
2584                                        break;
2585                                    }
2586                                    let segments = at.import_segments().expect(
2587                                        "BUG: binding cycles walk `uses` rows only: imports, their nested instances, and `uses` hops",
2588                                    );
2589                                    names.push(
2590                                        DataPath::new(segments, slot.data.clone()).input_key(),
2591                                    );
2592                                    chain.push(read);
2593                                    let template = self.template_of_row(at.row);
2594                                    match at.row.get_binding(template, &slot) {
2595                                        Some(RowBinding::Deferred {
2596                                            target: target @ ReferenceTarget::Data(_),
2597                                            hops,
2598                                        }) => self.target_landing(at.scope(*hops), target),
2599                                        Some(_) => break,
2600                                        None => match template.reference_ends.get(&slot) {
2601                                            Some(ReferenceEnd::Promptable(end)) => self
2602                                                .target_landing(
2603                                                    Some(at.clone()),
2604                                                    &ReferenceTarget::Data(end.clone()),
2605                                                ),
2606                                            Some(ReferenceEnd::Rule(_)) | None => break,
2607                                        },
2608                                    }
2609                                }
2610                            };
2611                            step = match landing {
2612                                Landing::Row(ForcedRead {
2613                                    at,
2614                                    end: RowEnd::Data(slot),
2615                                }) => Step::Row(at, slot),
2616                                Landing::Plan(RowEnd::Data(path)) => Step::Plan(path),
2617                                Landing::Row(ForcedRead {
2618                                    end: RowEnd::Rule(_),
2619                                    ..
2620                                })
2621                                | Landing::Plan(RowEnd::Rule(_)) => break,
2622                            };
2623                        }
2624                    }
2625                    Some(_) | None => {}
2626                }
2627            }
2628        }
2629        if errors.is_empty() {
2630            Ok(())
2631        } else {
2632            Err(errors)
2633        }
2634    }
2635
2636    /// What every rule can yield as a spec value, and the errors of member reads on
2637    /// those values. Repeats until the candidates stop changing: a rule read on a
2638    /// self-list row has no edge (the row evaluates it), so its candidates reach the
2639    /// reader through the previous pass.
2640    fn link_spec_values(&mut self) -> Result<Vec<Error>, Vec<Error>> {
2641        use crate::planning::spec_value::{
2642            assert_expression_candidates_linked, spec_member_errors, CandidateScope, CandidateTable,
2643        };
2644
2645        let rule_paths: HashSet<RulePath> = self.rules.keys().cloned().collect();
2646        let dropped_rules = self.dropped_rules.clone();
2647        let mut all_rules: BTreeSet<RulePath> = rule_paths.iter().cloned().collect();
2648        all_rules.extend(dropped_rules.iter().cloned());
2649        let presence = move |rule: &RulePath| {
2650            if rule_paths.contains(rule) {
2651                RulePresence::Present
2652            } else if dropped_rules.contains(rule) {
2653                RulePresence::Dropped
2654            } else {
2655                RulePresence::Absent
2656            }
2657        };
2658        let order = self.topological_sort()?;
2659        let row_end_reads =
2660            |identity: &SpecIdentity, end: &RowEnd| self.row_end_reads(identity, end);
2661        let mut previous: HashMap<RulePath, SpecCandidates> = HashMap::new();
2662        loop {
2663            let mut rule_candidates = previous.clone();
2664            let mut member_errors = Vec::new();
2665            for path in &order {
2666                let node = self
2667                    .rules
2668                    .get(path)
2669                    .expect("BUG: rule from topological sort not in graph");
2670                let scope = CandidateScope {
2671                    rule_candidates: &rule_candidates,
2672                    data: &self.data,
2673                    rules: &all_rules,
2674                    sources: &self.instance_lists.sources,
2675                    rule_presence: &presence,
2676                    row_end_reads: &row_end_reads,
2677                    self_identity: &self.instance_lists.self_identity,
2678                    templates: &self.instance_lists.templates,
2679                };
2680                let table = CandidateTable::build(&node.expression, &scope);
2681                let mut candidates = SpecCandidates::default();
2682                for (_, result) in &node.branches {
2683                    candidates.extend(table.candidates(*result));
2684                }
2685                for (source, message) in spec_member_errors(&node.expression, &table, &scope) {
2686                    let source = source.expect("BUG: spec member expression carries its source");
2687                    member_errors.push(Error::validation_with_context(
2688                        message,
2689                        Some(source),
2690                        None::<String>,
2691                        Some(self.main_spec),
2692                        None,
2693                    ));
2694                }
2695                rule_candidates.insert(path.clone(), candidates);
2696            }
2697            if rule_candidates == previous {
2698                let scope = CandidateScope {
2699                    rule_candidates: &rule_candidates,
2700                    data: &self.data,
2701                    rules: &all_rules,
2702                    sources: &self.instance_lists.sources,
2703                    rule_presence: &presence,
2704                    row_end_reads: &row_end_reads,
2705                    self_identity: &self.instance_lists.self_identity,
2706                    templates: &self.instance_lists.templates,
2707                };
2708                for node in self.rules.values() {
2709                    assert_expression_candidates_linked(&node.expression, &scope);
2710                }
2711                self.rule_candidates = rule_candidates;
2712                return Ok(member_errors);
2713            }
2714            previous = rule_candidates;
2715        }
2716    }
2717
2718    /// Row fields a read of `end` on a row of `identity` touches
2719    /// ([`ExecutionPlan::row_end_reads`]); rows of the spec being planned read this
2720    /// graph, since their plan is not built yet.
2721    fn row_end_reads(&self, identity: &SpecIdentity, end: &RowEnd) -> Vec<RowFieldRead> {
2722        match self.instance_lists.row_template_of(identity) {
2723            Some(RowTemplate::Plan(template)) => template.row_end_reads(end),
2724            Some(RowTemplate::Root) => self.root_row_end_reads(end),
2725            None => Vec::new(),
2726        }
2727    }
2728
2729    fn root_row_end_reads(&self, end: &RowEnd) -> Vec<RowFieldRead> {
2730        let mut reads: Vec<RowFieldRead> = Vec::new();
2731        let push = |path: &DataPath, reads: &mut Vec<RowFieldRead>| {
2732            if let Some(read) = self.root_row_slot(path) {
2733                if !reads.contains(&read) {
2734                    reads.push(read);
2735                }
2736            }
2737        };
2738        let rule_of_data = |path: &DataPath| -> Option<RulePath> {
2739            let mut current = path;
2740            for _ in 0..=self.data.len() {
2741                match self.data.get(current)? {
2742                    DataDefinition::Reference {
2743                        target: ReferenceTarget::Data(next),
2744                        ..
2745                    } => current = next,
2746                    DataDefinition::Reference {
2747                        target: ReferenceTarget::Rule(rule),
2748                        ..
2749                    } => return Some(rule.clone()),
2750                    _ => return None,
2751                }
2752            }
2753            panic!(
2754                "BUG: data reference chain from '{path}' outlasts graph data (cycles are rejected)"
2755            )
2756        };
2757        let start_rule = match end {
2758            RowEnd::Data(path) => match rule_of_data(path) {
2759                Some(rule) => Some(rule),
2760                None => {
2761                    push(path, &mut reads);
2762                    None
2763                }
2764            },
2765            RowEnd::Rule(rule) => Some(rule.clone()),
2766        };
2767        match start_rule {
2768            None => {}
2769            Some(rule) => {
2770                let mut pending = vec![rule];
2771                let mut seen: BTreeSet<RulePath> = BTreeSet::new();
2772                while let Some(rule) = pending.pop() {
2773                    if !seen.insert(rule.clone()) {
2774                        continue;
2775                    }
2776                    let Some(node) = self.rules.get(&rule) else {
2777                        continue;
2778                    };
2779                    for id in node.expression.table.ids() {
2780                        if let ExpressionKind::DataPath(path) = node.expression.table.kind(id) {
2781                            push(path, &mut reads);
2782                        }
2783                    }
2784                    pending.extend(node.depends_on_rules.iter().cloned());
2785                }
2786            }
2787        }
2788        reads
2789    }
2790
2791    /// The slot a row of this spec supplies when `path` is read on it: the data itself
2792    /// or the end of its reference chain, unless the spec binds it. `unbound` when the
2793    /// spec gives it no value.
2794    fn root_row_slot(&self, path: &DataPath) -> Option<RowFieldRead> {
2795        let mut current = path;
2796        for _ in 0..=self.data.len() {
2797            match self.data.get(current)? {
2798                DataDefinition::Reference {
2799                    target: ReferenceTarget::Data(next),
2800                    local_value: None,
2801                    ..
2802                } => current = next,
2803                DataDefinition::Reference { .. } | DataDefinition::Import { .. } => return None,
2804                definition @ (DataDefinition::TypeDeclaration { .. }
2805                | DataDefinition::Value { .. }
2806                | DataDefinition::InstanceList { .. }) => {
2807                    return (!definition.is_bound()).then(|| RowFieldRead {
2808                        slot: current.clone(),
2809                        unbound: definition.value().is_none(),
2810                    });
2811                }
2812            }
2813        }
2814        panic!("BUG: data reference chain from '{path}' outlasts graph data (cycles are rejected)")
2815    }
2816
2817    /// Filter `dependencies` to rules that are present.
2818    fn present_rule_dependencies(&self, dependencies: Vec<RulePath>) -> BTreeSet<RulePath> {
2819        let mut present = BTreeSet::new();
2820        for dependency in dependencies {
2821            match self.rule_presence(&dependency) {
2822                RulePresence::Present => {
2823                    present.insert(dependency);
2824                }
2825                RulePresence::Dropped => {}
2826                RulePresence::Absent => {
2827                    panic!("BUG: rule '{dependency}' neither in graph nor dropped")
2828                }
2829            }
2830        }
2831        present
2832    }
2833
2834    /// The Show graph: every rule of this plan and every `uses` import rule it reaches,
2835    /// with their edges, sorted. A cycle through import rules is an error naming every
2836    /// rule on it by its path from this plan.
2837    fn sort_show_rules(&mut self) -> Result<(), Vec<Error>> {
2838        let show_imports = self.reached_imports();
2839        let mut edges: BTreeMap<RulePath, BTreeSet<RulePath>> = BTreeMap::new();
2840        for (path, node) in &self.rules {
2841            let mut rule_edges: BTreeSet<RulePath> = node
2842                .depends_on_rules
2843                .iter()
2844                .filter(|dependency| self.rules.contains_key(*dependency))
2845                .cloned()
2846                .collect();
2847            rule_edges.extend(show_imports.edges.get(path).into_iter().flatten().cloned());
2848            edges.insert(path.clone(), rule_edges);
2849        }
2850        for path in show_imports.rules.keys() {
2851            edges.insert(
2852                path.clone(),
2853                show_imports
2854                    .edges
2855                    .get(path)
2856                    .expect("BUG: reached import rule has Show edges")
2857                    .clone(),
2858            );
2859        }
2860        let mut in_degree: BTreeMap<&RulePath, usize> = BTreeMap::new();
2861        let mut dependents: BTreeMap<&RulePath, Vec<&RulePath>> = BTreeMap::new();
2862        for (path, dependencies) in &edges {
2863            in_degree.insert(path, dependencies.len());
2864            for dependency in dependencies {
2865                assert!(
2866                    edges.contains_key(dependency),
2867                    "BUG: Show edge '{path}' -> '{dependency}' leaves the Show rules"
2868                );
2869                dependents.entry(dependency).or_default().push(path);
2870            }
2871        }
2872        let mut queue: VecDeque<&RulePath> = in_degree
2873            .iter()
2874            .filter(|(_, degree)| **degree == 0)
2875            .map(|(path, _)| *path)
2876            .collect();
2877        let mut order: Vec<RulePath> = Vec::with_capacity(edges.len());
2878        while let Some(path) = queue.pop_front() {
2879            for dependent in dependents.get(path).into_iter().flatten() {
2880                let degree = in_degree
2881                    .get_mut(dependent)
2882                    .expect("BUG: Show rule missing from in_degree");
2883                *degree -= 1;
2884                if *degree == 0 {
2885                    queue.push_back(dependent);
2886                }
2887            }
2888            order.push(path.clone());
2889        }
2890        if order.len() != edges.len() {
2891            let unsorted: Vec<&RulePath> =
2892                edges.keys().filter(|path| !order.contains(path)).collect();
2893            let missing = nodes_on_cycles(&unsorted, |path| {
2894                edges
2895                    .get(path)
2896                    .expect("BUG: unsorted Show rule missing from its edges")
2897                    .iter()
2898                    .collect()
2899            });
2900            let rules_involved: String = missing
2901                .iter()
2902                .map(|path| path.input_key())
2903                .collect::<Vec<_>>()
2904                .join(", ");
2905            let message = format!("Circular dependency (rules: {rules_involved})");
2906            return Err(missing
2907                .into_iter()
2908                .map(|path| {
2909                    let source = match self.rules.get(path) {
2910                        Some(node) => node.source.clone(),
2911                        None => {
2912                            let (identity, rule) = show_imports.rules.get(path).expect(
2913                                "BUG: Show rule is neither local nor a reached import rule",
2914                            );
2915                            self.instance_lists
2916                                .templates
2917                                .get(identity)
2918                                .unwrap_or_else(|| {
2919                                    panic!(
2920                                        "BUG: import rule '{path}' has no template of '{identity}'"
2921                                    )
2922                                })
2923                                .rules
2924                                .get(rule)
2925                                .unwrap_or_else(|| {
2926                                    panic!("BUG: import rule '{path}' missing from its template")
2927                                })
2928                                .source
2929                                .clone()
2930                        }
2931                    };
2932                    Error::validation_with_context(
2933                        message.clone(),
2934                        Some(source),
2935                        None::<String>,
2936                        Some(self.main_spec),
2937                        None,
2938                    )
2939                })
2940                .collect());
2941        }
2942        self.show_imports = show_imports;
2943        self.show_edges = edges;
2944        self.show_order = order;
2945        Ok(())
2946    }
2947
2948    /// Dependency order of every rule in the graph.
2949    fn topological_sort(&mut self) -> Result<Vec<RulePath>, Vec<Error>> {
2950        let mut depends_on: BTreeMap<RulePath, BTreeSet<RulePath>> = BTreeMap::new();
2951        let local_deps: Vec<(RulePath, Vec<RulePath>)> = self
2952            .rules
2953            .iter()
2954            .map(|(rule_path, rule_node)| {
2955                (
2956                    rule_path.clone(),
2957                    rule_node.depends_on_rules.iter().cloned().collect(),
2958                )
2959            })
2960            .collect();
2961        for (rule_path, dependencies) in local_deps {
2962            let present = self.present_rule_dependencies(dependencies);
2963            depends_on.insert(rule_path, present);
2964        }
2965        let mut in_degree: BTreeMap<RulePath, usize> = BTreeMap::new();
2966        let mut dependents: BTreeMap<RulePath, Vec<RulePath>> = BTreeMap::new();
2967        let mut queue = VecDeque::new();
2968        let mut result = Vec::new();
2969        for (rule_path, dependencies) in &depends_on {
2970            in_degree.insert(rule_path.clone(), dependencies.len());
2971            for dependency in dependencies {
2972                dependents
2973                    .entry(dependency.clone())
2974                    .or_default()
2975                    .push(rule_path.clone());
2976            }
2977        }
2978
2979        for (rule_path, degree) in &in_degree {
2980            if *degree == 0 {
2981                queue.push_back(rule_path.clone());
2982            }
2983        }
2984
2985        while let Some(rule_path) = queue.pop_front() {
2986            for dependent in dependents.get(&rule_path).into_iter().flatten() {
2987                let degree = in_degree
2988                    .get_mut(dependent)
2989                    .expect("BUG: rule missing from in_degree");
2990                *degree -= 1;
2991                if *degree == 0 {
2992                    queue.push_back(dependent.clone());
2993                }
2994            }
2995            result.push(rule_path);
2996        }
2997
2998        if result.len() != depends_on.len() {
2999            let unsorted: Vec<&RulePath> = depends_on
3000                .keys()
3001                .filter(|rule| !result.contains(rule))
3002                .collect();
3003            let on_cycles = nodes_on_cycles(&unsorted, |rule| {
3004                depends_on
3005                    .get(rule)
3006                    .expect("BUG: unsorted rule missing from depends_on")
3007                    .iter()
3008                    .collect()
3009            });
3010            let cycle: Vec<Source> = on_cycles
3011                .iter()
3012                .filter_map(|rule| self.rules.get(*rule).map(|n| n.source.clone()))
3013                .collect();
3014
3015            if cycle.is_empty() {
3016                unreachable!(
3017                    "BUG: circular dependency detected but no sources could be collected ({} rules on cycles)",
3018                    on_cycles.len()
3019                );
3020            }
3021            let rules_involved: String = on_cycles
3022                .iter()
3023                .map(|rule| rule.input_key())
3024                .collect::<Vec<_>>()
3025                .join(", ");
3026            let message = format!("Circular dependency (rules: {})", rules_involved);
3027            let errors: Vec<Error> = cycle
3028                .into_iter()
3029                .map(|source| {
3030                    Error::validation_with_context(
3031                        message.clone(),
3032                        Some(source),
3033                        None::<String>,
3034                        Some(self.main_spec),
3035                        None,
3036                    )
3037                })
3038                .collect();
3039            return Err(errors);
3040        }
3041
3042        Ok(result)
3043    }
3044}
3045
3046#[derive(Debug)]
3047pub(crate) struct RuleNode<'a> {
3048    /// The whole rule in one table: the default expression when the rule has no unless
3049    /// clauses, else a `Piecewise` over every branch in source order whose default arm
3050    /// has condition literal `true`.
3051    pub expression: Expression,
3052    /// Condition and result ids in [`Self::expression`]: the default first (no condition),
3053    /// then each unless clause in source order.
3054    pub branches: Vec<(Option<ExprId>, ExprId)>,
3055    pub source: Source,
3056
3057    pub depends_on_rules: BTreeSet<RulePath>,
3058
3059    /// Named on a self-list row (`components.unit_cost`), by this rule or another.
3060    /// Planning twin of eval [`crate::planning::execution_plan::ExecutionPlan::self_row_rules`].
3061    /// Not an edge in [`Self::depends_on_rules`]: child rows are evaluated first.
3062    pub recursive: bool,
3063
3064    /// Computed type of this rule's result (populated during validation)
3065    /// Every rule MUST have a type (Lemma is strictly typed)
3066    pub rule_type: Arc<LemmaType>,
3067
3068    /// Measure families of every typed node of [`Self::expression`]. The scale
3069    /// targets of these families are evaluated before the rule body.
3070    /// Filled by [`Graph::plan_unit_scales`].
3071    pub scale_families: BTreeSet<TypeFamily>,
3072
3073    /// Spec this rule belongs to (for type resolution during validation)
3074    pub spec: &'a LemmaSpec,
3075}
3076
3077type ResolvedTypesMap<'a> = Vec<(Arc<LemmaRepository>, &'a LemmaSpec, ResolvedSpecTypes)>;
3078
3079/// Parent lookup result: specs, inherited suggest, inherited fill, parent type.
3080type ParentLookupOk = (
3081    TypeSpecification,
3082    Option<RawSuggestion>,
3083    Option<RawSuggestion>,
3084    Arc<LemmaType>,
3085);
3086
3087/// Local resolved type entry during topo-sort: type, suggest, fill.
3088type ResolvedTypeEntry = (Arc<LemmaType>, Option<RawSuggestion>, Option<RawSuggestion>);
3089
3090fn local_type_entries(resolved_types: &ResolvedSpecTypes) -> HashMap<String, ResolvedTypeEntry> {
3091    resolved_types
3092        .resolved
3093        .iter()
3094        .map(|(name, lemma_type)| {
3095            (
3096                name.clone(),
3097                (
3098                    Arc::clone(lemma_type),
3099                    resolved_types.source_defaults.get(name).cloned(),
3100                    resolved_types.source_value_defaults.get(name).cloned(),
3101                ),
3102            )
3103        })
3104        .collect()
3105}
3106
3107/// Pending rule-reference type merge: path, type, local suggest, local fill.
3108type RuleReferenceUpdate = (
3109    DataPath,
3110    Arc<LemmaType>,
3111    Option<BoundValueKind>,
3112    Option<BoundValueKind>,
3113);
3114
3115/// Ok payload of [`GraphBuilder::build`]: data, rules, dropped rules, soft errors, resolved types.
3116type GraphBuildOk<'a> = (
3117    IndexMap<DataPath, DataDefinition>,
3118    BTreeMap<RulePath, RuleNode<'a>>,
3119    BTreeSet<RulePath>,
3120    Vec<Error>,
3121    ResolvedTypesMap<'a>,
3122    InstanceLists,
3123);
3124
3125struct GraphBuilder<'a, 'i> {
3126    data: IndexMap<DataPath, DataDefinition>,
3127    rules: BTreeMap<RulePath, RuleNode<'a>>,
3128    dropped_rules: BTreeSet<RulePath>,
3129    context: &'a Context,
3130    local_types: ResolvedTypesMap<'a>,
3131    errors: Vec<Error>,
3132    /// Errors of a nested spec whose template could not be built. When any nested
3133    /// spec fails, the build aborts and reports only these: the consumer's own
3134    /// follow-on errors are cascading noise (references the failed spec can no
3135    /// longer resolve), and the nested errors already name the root cause.
3136    nested_errors: Vec<Error>,
3137    nested_spec_failed: bool,
3138    main_spec: &'a LemmaSpec,
3139    limits: &'a ResourceLimits,
3140    instance_lists: InstanceLists,
3141    interners: &'i mut instance_list::PassInterners,
3142    /// Local rules named on a self-list row while converting; marked [`RuleNode::recursive`]
3143    /// after every rule node exists.
3144    self_list_row_rules: BTreeSet<RulePath>,
3145}
3146
3147struct RuleExpressionConversion<'a, 'b> {
3148    /// Nodes of every expression converted for the rule being added.
3149    table: ExprTable<ExpressionKind>,
3150    spec: &'a LemmaSpec,
3151    /// Repository `spec` is loaded under.
3152    repository: &'b Arc<LemmaRepository>,
3153    data_map: &'b HashMap<String, &'b LemmaData>,
3154    rule_names: &'b HashSet<&'b str>,
3155    /// Rule being converted. Never a clash for a row field: it cannot read itself.
3156    rule: &'b str,
3157    effective: &'b EffectiveDate,
3158    depends_on_rules: &'b mut BTreeSet<RulePath>,
3159    /// Instance-list commands enclosing the expression being converted, outermost first.
3160    row_scopes: Vec<RowScope>,
3161}
3162
3163impl RuleExpressionConversion<'_, '_> {
3164    fn push(&mut self, kind: ExpressionKind, source: Option<Source>) -> ExprId {
3165        self.table.push(kind, source)
3166    }
3167}
3168
3169type AstRef<'a> = ExprRef<'a, ast::ExpressionKind>;
3170
3171/// Work item of a DFS over resolved and source expressions and rule references.
3172enum Pending<'a> {
3173    Sem(ExprRef<'a, ExpressionKind>),
3174    Ast(AstRef<'a>),
3175    Rule(RulePath),
3176}
3177
3178/// What an identity search looks for in the expressions it walks.
3179#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
3180enum SearchKind {
3181    /// The row spec of the instance list an expression yields.
3182    List,
3183    /// The row spec of the single row an expression yields (`list -> first`).
3184    Row,
3185}
3186
3187/// Where the identity a nested search finds goes.
3188enum Continuation<'a> {
3189    /// It is the answer of the whole search.
3190    Answer,
3191    /// The list searched for a `-> first` / `-> last` / `-> find`: its row spec is the
3192    /// enclosing row search's answer.
3193    RowOfList,
3194    /// The row a member is read on: `path.name` of that row is the list the enclosing
3195    /// list search is after.
3196    Member { path: &'a [String], name: &'a str },
3197}
3198
3199/// One depth-first search with an explicit work stack. Nested searches (the list behind a
3200/// `-> first`, the row a member is read on) stack on top of it; the first expression that
3201/// yields an identity wins, in source order.
3202struct IdentitySearch<'a> {
3203    kind: SearchKind,
3204    pending: Vec<Pending<'a>>,
3205    /// Rules entered by this search and the searches it nests in, per kind.
3206    visiting: BTreeSet<(SearchKind, RulePath)>,
3207    on_found: Continuation<'a>,
3208}
3209
3210impl<'a> IdentitySearch<'a> {
3211    fn new(kind: SearchKind, first: Pending<'a>, on_found: Continuation<'a>) -> Self {
3212        Self {
3213            kind,
3214            pending: vec![first],
3215            visiting: BTreeSet::new(),
3216            on_found,
3217        }
3218    }
3219
3220    fn nested(&self, kind: SearchKind, first: Pending<'a>, on_found: Continuation<'a>) -> Self {
3221        Self {
3222            kind,
3223            pending: vec![first],
3224            visiting: self.visiting.clone(),
3225            on_found,
3226        }
3227    }
3228
3229    /// `false` when `rule` was already entered by this search or an enclosing one.
3230    fn enter(&mut self, rule: &RulePath) -> bool {
3231        self.visiting.insert((self.kind, rule.clone()))
3232    }
3233}
3234
3235/// Outcome of one work item of an [`IdentitySearch`].
3236enum SearchStep<'a> {
3237    Next,
3238    Nested(IdentitySearch<'a>),
3239    Found(SpecIdentity),
3240}
3241
3242/// Children converted in the enclosing row scope. List command arguments are not among them:
3243/// they are converted in their own row scope by the list command.
3244fn for_each_conversion_child(kind: &ast::ExpressionKind, mut f: impl FnMut(ExprId)) {
3245    match kind {
3246        ast::ExpressionKind::ListCommand { list, .. } => f(*list),
3247        other => other.for_each_child(f),
3248    }
3249}
3250
3251fn one(children: &[ExprId]) -> ExprId {
3252    let &[child] = children else {
3253        panic!("BUG: AST expression with one operand has one converted child");
3254    };
3255    child
3256}
3257
3258/// A chain's first operand and the operands of its steps, in source order.
3259/// Converted `first` and steps of an AST chain whose operands converted to `children`.
3260fn convert_chain_steps<Op: Clone>(
3261    steps: &[ast::ChainStep<Op>],
3262    children: &[ExprId],
3263) -> (ExprId, Vec<ast::ChainStep<Op>>) {
3264    let mut children = children.iter().copied();
3265    let first = children
3266        .next()
3267        .expect("BUG: chain expression always has a first operand");
3268    let steps: Vec<ast::ChainStep<Op>> = steps
3269        .iter()
3270        .zip(children.by_ref())
3271        .map(|(step, operand)| ast::ChainStep {
3272            op: step.op.clone(),
3273            operand,
3274            source_location: step.source_location.clone(),
3275        })
3276        .collect();
3277    assert!(
3278        children.next().is_none(),
3279        "BUG: chain converted to more operands than steps"
3280    );
3281    (first, steps)
3282}
3283
3284fn two(children: &[ExprId]) -> (ExprId, ExprId) {
3285    let &[left, right] = children else {
3286        panic!("BUG: AST expression with two operands has two converted children");
3287    };
3288    (left, right)
3289}
3290
3291/// Work of [`GraphBuilder::convert_ast_expression`]. A node's operands are converted before
3292/// the node; a list command's arguments are converted between its list operand and its
3293/// build, each in the row scope the argument names.
3294enum ConvertFrame<'t> {
3295    /// Convert the subtree at this node.
3296    Enter(AstRef<'t>),
3297    /// Operands of this node are converted; their results are on the value stack.
3298    Node(AstRef<'t>),
3299    /// List command whose next argument is converted, or which is built when none is left.
3300    List(Box<ListCommandBuild<'t>>),
3301    /// Argument of a list command was converted; its result is on the value stack.
3302    ListArgument(Box<ListCommandBuild<'t>>, ListArgumentSink),
3303    /// Ends the row scope pushed for a list command argument.
3304    PopRowScope,
3305}
3306
3307/// Stacks of [`GraphBuilder::convert_ast_expression`], reused across one rule's expressions.
3308/// Empty between calls.
3309struct ConvertScratch<'t> {
3310    frames: Vec<ConvertFrame<'t>>,
3311    values: Vec<Option<ExprId>>,
3312    children: Vec<ExprId>,
3313}
3314
3315/// Where the converted value of a list command argument goes.
3316enum ListArgumentSink {
3317    /// Positional argument: an unconvertible argument makes the command unconvertible.
3318    Positional,
3319    /// Value of the `-> with` row field at this data path.
3320    Field(DataPath),
3321}
3322
3323/// Argument of a list command, converted in queue order.
3324enum ListArgument {
3325    /// Converted in the row scopes enclosing the command.
3326    Enclosing(ExprId),
3327    /// Converted with the rows of the list in view.
3328    InRows(ExprId),
3329    /// Value of the `-> with` row field `name`.
3330    Field { name: String, value: ExprId },
3331}
3332
3333/// A list command whose arguments are being converted.
3334struct ListCommandBuild<'t> {
3335    node: AstRef<'t>,
3336    source: Source,
3337    list: ExprId,
3338    row_level: Option<u32>,
3339    row_scope: Option<RowScope>,
3340    pending: VecDeque<ListArgument>,
3341    converted: Vec<ExprId>,
3342    fields: Vec<(DataPath, ExprId)>,
3343    failed: bool,
3344}
3345
3346/// Arguments of `command` in the order they are converted.
3347fn list_command_arguments(
3348    command: &ast::ListCommand,
3349    row_scope: &Option<RowScope>,
3350) -> VecDeque<ListArgument> {
3351    match command {
3352        ast::ListCommand::Find { keys } => keys
3353            .iter()
3354            .map(|key| ListArgument::Enclosing(*key))
3355            .collect(),
3356        ast::ListCommand::First
3357        | ast::ListCommand::Last
3358        | ast::ListCommand::Count
3359        | ast::ListCommand::Key { .. } => VecDeque::new(),
3360        ast::ListCommand::Sum { argument }
3361        | ast::ListCommand::Avg { argument }
3362        | ast::ListCommand::Product { argument }
3363        | ast::ListCommand::Min { argument }
3364        | ast::ListCommand::Max { argument }
3365        | ast::ListCommand::Sort { argument, .. } => argument
3366            .iter()
3367            .map(|argument| ListArgument::InRows(*argument))
3368            .collect(),
3369        ast::ListCommand::Any { form }
3370        | ast::ListCommand::All { form }
3371        | ast::ListCommand::Where { form } => {
3372            let expr = match form {
3373                ast::ListPredicate::Value { expr } | ast::ListPredicate::Instance { expr } => expr,
3374            };
3375            VecDeque::from([ListArgument::InRows(*expr)])
3376        }
3377        ast::ListCommand::Top { n } => VecDeque::from([ListArgument::Enclosing(*n)]),
3378        ast::ListCommand::With { rows } => match (rows, row_scope) {
3379            (ast::ListWithRows::Values(values), None)
3380            | (ast::ListWithRows::Values(values), Some(_)) => values
3381                .iter()
3382                .map(|value| ListArgument::Enclosing(*value))
3383                .collect(),
3384            (ast::ListWithRows::FieldPairs(_), None) => VecDeque::new(),
3385            (ast::ListWithRows::FieldPairs(pairs), Some(_)) => pairs
3386                .iter()
3387                .map(|(field, value)| ListArgument::Field {
3388                    name: field.to_string(),
3389                    value: *value,
3390                })
3391                .collect(),
3392        },
3393    }
3394}
3395
3396fn optional_argument(
3397    argument: &Option<ExprId>,
3398    converted: &mut impl Iterator<Item = ExprId>,
3399) -> Option<ExprId> {
3400    argument.as_ref().map(|_| next_argument(converted))
3401}
3402
3403fn next_argument(converted: &mut impl Iterator<Item = ExprId>) -> ExprId {
3404    converted
3405        .next()
3406        .expect("BUG: one converted argument per list command argument")
3407}
3408
3409fn list_predicate_op(
3410    form: &ast::ListPredicate,
3411    kind: ListPredKind,
3412    converted: &mut impl Iterator<Item = ExprId>,
3413) -> ListOp {
3414    match form {
3415        ast::ListPredicate::Value { .. } => ListOp::Predicate {
3416            kind,
3417            form: ast::ListPredicateForm::Value,
3418            expr: next_argument(converted),
3419        },
3420        ast::ListPredicate::Instance { .. } => ListOp::Predicate {
3421            kind,
3422            form: ast::ListPredicateForm::Instance,
3423            expr: next_argument(converted),
3424        },
3425    }
3426}
3427
3428/// Spec being converted.
3429struct SourceScope<'s> {
3430    spec: &'s LemmaSpec,
3431}
3432
3433/// Outcome of reading a name as a row field inside an instance-list command.
3434enum RowFieldLookup {
3435    /// The [`ExpressionKind::RowField`] read, and the member read on it when the field
3436    /// is a `uses` slot read through (`items.sub.doubled`).
3437    Field {
3438        field: Box<ExpressionKind>,
3439        member: Option<(Vec<String>, String)>,
3440    },
3441    NotARowField,
3442    /// An error was pushed.
3443    Failed,
3444}
3445
3446/// The row field the first hop of `chain` reads on rows of `identity` at `level`, and
3447/// the member read through it for the rest of `parts`.
3448fn row_field_lookup(
3449    level: u32,
3450    identity: &SpecIdentity,
3451    chain: Vec<RowHop>,
3452    parts: &[&str],
3453    qualifier: Option<Vec<String>>,
3454) -> RowFieldLookup {
3455    let first = chain
3456        .into_iter()
3457        .next()
3458        .expect("BUG: a row end chain has a first hop");
3459    assert_eq!(
3460        first.identity, *identity,
3461        "BUG: a row field chain starts on the rows it reads"
3462    );
3463    let member = match parts {
3464        [_] => None,
3465        [_, through @ .., name] => Some((
3466            through.iter().map(|part| (*part).to_string()).collect(),
3467            (*name).to_string(),
3468        )),
3469        [] => unreachable!("BUG: a row field names at least one part"),
3470    };
3471    RowFieldLookup::Field {
3472        field: Box::new(ExpressionKind::RowField {
3473            level,
3474            identity: first.identity,
3475            end: first.end,
3476            qualifier,
3477        }),
3478        member,
3479    }
3480}
3481
3482/// Rows visible while converting one list command argument.
3483#[derive(Clone)]
3484enum RowScope {
3485    Value {
3486        level: u32,
3487        list: ExprId,
3488    },
3489    Instance {
3490        /// Path of the list operand (`menu`, `card`+`menu`) when it is a plain reference.
3491        list_path: Option<Vec<String>>,
3492        identity: SpecIdentity,
3493        template: RowTemplate,
3494        level: u32,
3495    },
3496}
3497
3498impl RowScope {
3499    fn level(&self) -> u32 {
3500        match self {
3501            Self::Value { level, .. } | Self::Instance { level, .. } => *level,
3502        }
3503    }
3504}
3505
3506/// A spec whose imports are being resolved into `local_types`. It is finished after its
3507/// last import.
3508struct SpecTypesFrame<'a> {
3509    spec: &'a LemmaSpec,
3510    repository: Arc<LemmaRepository>,
3511    effective: EffectiveDate,
3512    next_data: usize,
3513    /// Remaining dynamic-import candidates to visit before the next data row.
3514    pending_targets: VecDeque<discovery::ResolvedDependencyEdge<'a>>,
3515}
3516
3517fn reference_error(main_spec: &LemmaSpec, source: &Source, message: String) -> Error {
3518    Error::validation_with_context(
3519        message,
3520        Some(source.clone()),
3521        None::<String>,
3522        Some(main_spec),
3523        None,
3524    )
3525}
3526
3527/// Decide whether an LHS-declared reference type and the resolved target type
3528/// share a compatible kind. Returns `None` when they do; returns `Some(msg)`
3529/// describing the mismatch otherwise.
3530///
3531/// "Same kind" requires:
3532/// 1. matching base type spec (number / measure / text / ratio / …) — see
3533///    [`LemmaType::has_same_base_type`]; and
3534/// 2. for measure and measure-range types, matching measure family — see
3535///    [`LemmaType::same_measure_family`] (ranges via [`range_span_type`]
3536///    element). Two quantities in different families (e.g. `eur` vs
3537///    `celsius`) share the `Measure` discriminant but are not interchangeable
3538///    values; copying one into the other would silently propagate a
3539///    wrong-domain measure. Ratio / ratio-range refs stay base-kind only.
3540///
3541/// `target_kind_label` distinguishes the two callers ("target" for data
3542/// references, "target rule" for rule references) so the message reads
3543/// naturally.
3544fn reference_kind_mismatch_message<P: fmt::Display>(
3545    lhs: &LemmaType,
3546    target_type: &LemmaType,
3547    reference_path: &DataPath,
3548    target_path: &P,
3549    target_kind_label: &str,
3550) -> Option<String> {
3551    if !lhs.has_same_base_type(target_type) {
3552        return Some(format!(
3553            "Data reference '{}' type mismatch: declared as '{}' but {} '{}' is '{}'",
3554            reference_path,
3555            lhs.name(),
3556            target_kind_label,
3557            target_path,
3558            target_type.name(),
3559        ));
3560    }
3561
3562    let lhs_span;
3563    let target_span;
3564    let (lhs_measure, target_measure): (&LemmaType, &LemmaType) = if lhs.is_measure() {
3565        (lhs, target_type)
3566    } else if lhs.is_measure_range() {
3567        lhs_span = range_span_type(lhs);
3568        target_span = range_span_type(target_type);
3569        if lhs_span.is_undetermined() || target_span.is_undetermined() {
3570            unreachable!(
3571                "BUG: MeasureRange reference kinds must yield measure elements via range_span_type"
3572            );
3573        }
3574        (lhs_span.as_ref(), target_span.as_ref())
3575    } else {
3576        return None;
3577    };
3578
3579    if lhs_measure.same_measure_family(target_measure) {
3580        return None;
3581    }
3582    let lhs_family = lhs_measure.measure_family_name().expect(
3583        "BUG: declared measure data must carry a family name; \
3584         anonymous measure types only arise from runtime synthesis \
3585         and never appear as a reference's LHS-declared type",
3586    );
3587    let target_family = target_measure.measure_family_name().expect(
3588        "BUG: declared measure data must carry a family name; \
3589         anonymous measure types only arise from runtime synthesis \
3590         and never appear as a reference target's schema type",
3591    );
3592    Some(format!(
3593        "Data reference '{}' measure family mismatch: declared as '{}' (family '{}') but {} '{}' is '{}' (family '{}')",
3594        reference_path,
3595        lhs.name(),
3596        lhs_family,
3597        target_kind_label,
3598        target_path,
3599        target_type.name(),
3600        target_family,
3601    ))
3602}
3603
3604/// Read-only graph state the stored-row reference kind-check resolves field,
3605/// target, and rule types through.
3606struct StoredRowKindCheckGraph<'a> {
3607    main_spec: &'a LemmaSpec,
3608    lists: &'a InstanceLists,
3609    data: &'a IndexMap<DataPath, DataDefinition>,
3610    rules: &'a BTreeMap<RulePath, RuleNode<'a>>,
3611    dropped_rules: &'a BTreeSet<RulePath>,
3612    computed_rule_types: &'a HashMap<RulePath, Arc<LemmaType>>,
3613}
3614
3615/// A row whose reference bindings resolve in the plan being built: `depth` instances
3616/// below that plan, named `prefix` for messages, with `source` the place that wrote it.
3617pub(crate) struct OwnedRow<'r> {
3618    pub row: &'r RowInstance,
3619    pub depth: u32,
3620    pub prefix: Vec<String>,
3621    pub source: &'r Source,
3622}
3623
3624/// Calls `f` with every reference binding the plan holding `data` and `lists` wrote,
3625/// in plan order: on its `uses` rows and its stored list rows, and on the rows nested
3626/// in them. Bindings written inside a row spec belong to that spec's plan.
3627pub(crate) fn for_each_owned_reference<'r>(
3628    data: &'r IndexMap<DataPath, DataDefinition>,
3629    lists: &'r InstanceLists,
3630    mut f: impl FnMut(&OwnedRow<'r>, usize, &'r ReferenceTarget),
3631) {
3632    let mut pending: Vec<OwnedRow<'r>> = Vec::new();
3633    for (path, definition) in data {
3634        if let DataDefinition::Import { row, source, .. } = definition {
3635            pending.push(OwnedRow {
3636                row,
3637                depth: 0,
3638                prefix: vec![path.data.clone()],
3639                source,
3640            });
3641        }
3642    }
3643    for source in lists.sources.values() {
3644        for row in &source.store.rows {
3645            pending.push(written_owned_row(row, 0, &[]));
3646        }
3647    }
3648    pending.reverse();
3649    while let Some(owned) = pending.pop() {
3650        let row = owned.row;
3651        let mut nested_rows: Vec<OwnedRow<'r>> = Vec::new();
3652        for (slot, binding) in row.bindings.iter().enumerate() {
3653            match binding {
3654                None | Some(RowBinding::Bound(_)) => {}
3655                Some(RowBinding::Rows { rows, hops, .. }) => {
3656                    let depth = owned
3657                        .depth
3658                        .checked_sub(*hops)
3659                        .expect("BUG: list rows hop above the plan being built");
3660                    for nested in rows {
3661                        nested_rows.push(written_owned_row(nested, depth, &owned.prefix));
3662                    }
3663                }
3664                Some(RowBinding::Instance(nested)) => {
3665                    let RowOrigin::Import { alias } = &nested.origin else {
3666                        unreachable!("BUG: a nested instance binding holds an import row");
3667                    };
3668                    let mut prefix = owned.prefix.clone();
3669                    prefix.push(alias.to_string());
3670                    nested_rows.push(OwnedRow {
3671                        row: nested,
3672                        depth: owned.depth + 1,
3673                        prefix,
3674                        source: owned.source,
3675                    });
3676                }
3677                Some(RowBinding::Deferred { target, hops }) => {
3678                    if *hops == owned.depth {
3679                        f(&owned, slot, target);
3680                    }
3681                }
3682            }
3683        }
3684        pending.extend(nested_rows.into_iter().rev());
3685    }
3686}
3687
3688/// Kind-check the reference bindings the plan being built wrote against their template
3689/// field types.
3690fn check_stored_row_reference_types(graph: &StoredRowKindCheckGraph, errors: &mut Vec<Error>) {
3691    for_each_owned_reference(graph.data, graph.lists, |owned, slot, target| {
3692        let template = graph
3693            .lists
3694            .templates
3695            .get(&owned.row.identity)
3696            .expect("BUG: written rows are rows of a planned row spec");
3697        let (field_path, definition) = template
3698            .data
3699            .get_index(slot)
3700            .expect("BUG: row slot is template data");
3701        let display_path = DataPath {
3702            segments: Vec::new(),
3703            data: owned
3704                .prefix
3705                .iter()
3706                .map(String::as_str)
3707                .chain(std::iter::once(field_path.data.as_str()))
3708                .collect::<Vec<_>>()
3709                .join("."),
3710        };
3711        let lhs = definition
3712            .resolved_type_arc()
3713            .expect("BUG: row field is typed template data");
3714        if lhs.is_undetermined() {
3715            return;
3716        }
3717        let Some((target_type, kind_label)) =
3718            reference_target_type(graph, target, &display_path, owned.source, errors)
3719        else {
3720            return;
3721        };
3722        if let Some(msg) = reference_kind_mismatch_message(
3723            lhs,
3724            &target_type,
3725            &display_path,
3726            &match target {
3727                ReferenceTarget::Data(path) => path.to_string(),
3728                ReferenceTarget::Rule(rule) => rule.to_string(),
3729            },
3730            kind_label,
3731        ) {
3732            errors.push(reference_error(graph.main_spec, owned.source, msg));
3733        }
3734    });
3735}
3736
3737/// A row written in a list, nested under the row named `holder`.
3738fn written_owned_row<'r>(row: &'r RowInstance, depth: u32, holder: &[String]) -> OwnedRow<'r> {
3739    let RowOrigin::Written { instance, source } = &row.origin else {
3740        unreachable!("BUG: stored rows are written rows");
3741    };
3742    let mut prefix = holder.to_vec();
3743    prefix.extend(instance.prefix.iter().map(PathSegment::input_key));
3744    OwnedRow {
3745        row,
3746        depth,
3747        prefix,
3748        source,
3749    }
3750}
3751
3752/// Type of reference `target` of the plan being built, with its label for messages.
3753/// `None` after pushing an error, or when the target rule was dropped.
3754fn reference_target_type(
3755    graph: &StoredRowKindCheckGraph,
3756    target: &ReferenceTarget,
3757    display_path: &DataPath,
3758    source: &Source,
3759    errors: &mut Vec<Error>,
3760) -> Option<(Arc<LemmaType>, &'static str)> {
3761    let segments = match target {
3762        ReferenceTarget::Data(path) => &path.segments,
3763        ReferenceTarget::Rule(rule) => &rule.segments,
3764    };
3765    if !segments.is_empty() {
3766        let mut identity: Option<&SpecIdentity> = None;
3767        for segment in segments {
3768            let slot = DataPath::local(segment.uses.to_string());
3769            let data = match identity {
3770                None => graph.data,
3771                Some(identity) => &graph.lists.templates[identity].data,
3772            };
3773            let Some(DataDefinition::Import { row, .. }) = data.get(&slot) else {
3774                panic!("BUG: reference target hop '{slot}' of {target:?} is not a `uses` slot");
3775            };
3776            identity = Some(&row.identity);
3777        }
3778        let template = &graph.lists.templates[identity.expect("BUG: a member target has a hop")];
3779        return Some(match target {
3780            ReferenceTarget::Data(path) => (
3781                instance_list::row_data_type(&template.data, &DataPath::local(path.data.clone())),
3782                "target",
3783            ),
3784            ReferenceTarget::Rule(rule) => (
3785                instance_list::row_end_type(
3786                    template,
3787                    &RowEnd::Rule(RulePath::new(Vec::new(), rule.rule.clone())),
3788                ),
3789                "target rule",
3790            ),
3791        });
3792    }
3793    match target {
3794        ReferenceTarget::Data(target_path) => {
3795            let Some(entry) = graph.data.get(target_path) else {
3796                errors.push(reference_error(
3797                    graph.main_spec,
3798                    source,
3799                    format!(
3800                        "Data reference '{}' target '{}' does not exist",
3801                        display_path, target_path
3802                    ),
3803                ));
3804                return None;
3805            };
3806            match entry {
3807                DataDefinition::TypeDeclaration { resolved_type, .. }
3808                | DataDefinition::Reference { resolved_type, .. }
3809                | DataDefinition::InstanceList { resolved_type, .. }
3810                | DataDefinition::Value { resolved_type, .. } => {
3811                    Some((Arc::clone(resolved_type), "target"))
3812                }
3813                DataDefinition::Import { .. } => {
3814                    errors.push(reference_error(
3815                        graph.main_spec,
3816                        source,
3817                        format!(
3818                            "Data reference '{}' target '{}' is a spec reference and cannot carry a value",
3819                            display_path, target_path
3820                        ),
3821                    ));
3822                    None
3823                }
3824            }
3825        }
3826        ReferenceTarget::Rule(target_rule) => {
3827            if let Some(target_type) = graph.computed_rule_types.get(target_rule) {
3828                return Some((Arc::clone(target_type), "target rule"));
3829            }
3830            if graph.rules.contains_key(target_rule) {
3831                panic!("BUG: rule '{target_rule}' type not inferred before row reference check")
3832            }
3833            if !graph.dropped_rules.contains(target_rule) {
3834                errors.push(reference_error(
3835                    graph.main_spec,
3836                    source,
3837                    format!(
3838                        "Data reference '{}' target rule '{}' does not exist",
3839                        display_path, target_rule
3840                    ),
3841                ));
3842            }
3843            None
3844        }
3845    }
3846}
3847
3848/// Type name shown in `-> suggest` constraint errors (the declared type, not the data slot).
3849fn constraint_application_type_name(parent: &ParentType, data_name: &str) -> String {
3850    let mut current = parent;
3851    loop {
3852        match current {
3853            ParentType::Custom { name, .. } => return name.clone(),
3854            ParentType::Qualified { inner, .. }
3855            | ParentType::Ranged { inner, .. }
3856            | ParentType::List { inner, .. } => current = inner.as_ref(),
3857            ParentType::Primitive { .. } => return data_name.to_string(),
3858        }
3859    }
3860}
3861
3862/// Named ranged types (`cargo_mass range`, `score range`) refresh from the element after
3863/// decomposition. Apply their constraints only after that refresh so units/decomp exist and
3864/// range-local bounds overwrite inherited endpoints as last writer.
3865fn should_defer_ranged_constraints(parent: &ParentType) -> bool {
3866    matches!(parent, ParentType::Ranged { .. })
3867        && matches!(
3868            element_parent_type(parent),
3869            ParentType::Custom { .. } | ParentType::Qualified { .. }
3870        )
3871}
3872
3873/// Fold a list of definition-style constraints into a [`TypeSpecification`].
3874/// Used for both the GraphBuilder's regular TypeDeclaration path and the
3875/// post-build reference type-merging pass, so the underlying constraint
3876/// application logic stays in one place.
3877struct ConstraintSuggestionSink<'a> {
3878    suggestion: &'a mut Option<RawSuggestion>,
3879    value: &'a mut Option<RawSuggestion>,
3880    resolve_unit: Option<&'a UnitTokenResolver<'a>>,
3881}
3882
3883fn apply_constraints_to_spec(
3884    spec: &LemmaSpec,
3885    type_name: &str,
3886    data_name: &str,
3887    lemma_type: &mut LemmaType,
3888    constraints: &[Constraint],
3889    source: &Source,
3890    sink: ConstraintSuggestionSink<'_>,
3891) -> Result<(), Vec<Error>> {
3892    let mut errors = Vec::new();
3893
3894    let mut seen_unit_names: std::collections::HashSet<String> = Default::default();
3895    let mut seen_singleton_commands: std::collections::HashSet<TypeConstraintCommand> =
3896        Default::default();
3897    let mut has_suggest = false;
3898    let mut has_value = false;
3899    for row in constraints.iter() {
3900        let command = row.command;
3901        let args = &row.args;
3902        if command == TypeConstraintCommand::Suggest {
3903            has_suggest = true;
3904        }
3905        if command == TypeConstraintCommand::With {
3906            has_value = true;
3907        }
3908        if command == TypeConstraintCommand::Unit {
3909            if let Some(CommandArg::Label(name)) = args.first() {
3910                if !seen_unit_names.insert(name.clone()) {
3911                    errors.push(Error::validation_with_context(
3912                        format!(
3913                            "Duplicate unit '{}': each unit name may appear at most once per type definition",
3914                            name
3915                        ),
3916                        Some(source.clone()),
3917                        None::<String>,
3918                        Some(spec),
3919                        None,
3920                    ));
3921                }
3922            }
3923        }
3924        if matches!(
3925            command,
3926            TypeConstraintCommand::Minimum
3927                | TypeConstraintCommand::Maximum
3928                | TypeConstraintCommand::Decimals
3929                | TypeConstraintCommand::Suggest
3930                | TypeConstraintCommand::With
3931        ) && !(command == TypeConstraintCommand::With
3932            && matches!(lemma_type.specifications, TypeSpecification::List { .. }))
3933            && !seen_singleton_commands.insert(command)
3934        {
3935            errors.push(Error::validation_with_context(
3936                format!(
3937                    "Duplicate '{command}' constraint: each may appear at most once per type declaration"
3938                ),
3939                Some(source.clone()),
3940                None::<String>,
3941                Some(spec),
3942                None,
3943            ));
3944        }
3945    }
3946    if has_suggest && has_value {
3947        errors.push(Error::validation_with_context(
3948            "Cannot use both 'with' and 'suggest' on the same data declaration".to_string(),
3949            Some(source.clone()),
3950            None::<String>,
3951            Some(spec),
3952            None,
3953        ));
3954    }
3955    if !errors.is_empty() {
3956        return Err(errors);
3957    }
3958
3959    let ConstraintSuggestionSink {
3960        suggestion: declared_suggestion,
3961        value: declared_value,
3962        resolve_unit,
3963    } = sink;
3964    let names = ConstraintNames {
3965        type_name,
3966        data_name,
3967    };
3968
3969    let mut apply_one = |lemma_type: &mut LemmaType,
3970                         command: TypeConstraintCommand,
3971                         args: &[CommandArg],
3972                         declared_suggestion: &mut Option<RawSuggestion>,
3973                         declared_value: &mut Option<RawSuggestion>| {
3974        let mut suggestion_before = declared_suggestion.clone();
3975        let mut value_before = declared_value.clone();
3976        let applied = if matches!(
3977            command,
3978            TypeConstraintCommand::Option | TypeConstraintCommand::Options
3979        ) {
3980            apply_option_constraint(lemma_type, command, args, resolve_unit, spec)
3981        } else if matches!(
3982            command,
3983            TypeConstraintCommand::Suggest | TypeConstraintCommand::With
3984        ) && matches!(lemma_type.specifications, TypeSpecification::Spec { .. })
3985        {
3986            let target = match command {
3987                TypeConstraintCommand::Suggest => &mut suggestion_before,
3988                TypeConstraintCommand::With => &mut value_before,
3989                _ => unreachable!("BUG: only Suggest or With in this arm"),
3990            };
3991            apply_spec_member_command(lemma_type, command, args, spec, target)
3992        } else if matches!(lemma_type.specifications, TypeSpecification::List { .. })
3993            && command == TypeConstraintCommand::With
3994        {
3995            apply_list_with_rows(
3996                lemma_type,
3997                names,
3998                args,
3999                &mut value_before,
4000                resolve_unit,
4001                spec,
4002            )
4003        } else {
4004            lemma_type.apply_constraint(
4005                names,
4006                command,
4007                args,
4008                &mut suggestion_before,
4009                &mut value_before,
4010                resolve_unit,
4011            )
4012        };
4013        match applied {
4014            Ok(()) => {
4015                *declared_suggestion = suggestion_before;
4016                *declared_value = value_before;
4017            }
4018            Err(e) => {
4019                errors.push(Error::validation_with_context(
4020                    format!("Failed to apply constraint '{}': {}", command, e),
4021                    Some(source.clone()),
4022                    None::<String>,
4023                    Some(spec),
4024                    None,
4025                ));
4026            }
4027        }
4028    };
4029
4030    for row in constraints {
4031        let command = row.command;
4032        let args = &row.args;
4033        if matches!(
4034            command,
4035            TypeConstraintCommand::Unit | TypeConstraintCommand::Trait
4036        ) {
4037            apply_one(
4038                lemma_type,
4039                command,
4040                args,
4041                declared_suggestion,
4042                declared_value,
4043            );
4044        }
4045    }
4046    for row in constraints {
4047        let command = row.command;
4048        let args = &row.args;
4049        if !matches!(
4050            command,
4051            TypeConstraintCommand::Unit | TypeConstraintCommand::Trait
4052        ) {
4053            apply_one(
4054                lemma_type,
4055                command,
4056                args,
4057                declared_suggestion,
4058                declared_value,
4059            );
4060        }
4061    }
4062    if !errors.is_empty() {
4063        return Err(errors);
4064    }
4065    Ok(())
4066}
4067
4068/// Every node reachable from the root carries a `source_location`.
4069fn expression_has_source_locations(expr: &ast::Expression) -> bool {
4070    expr.table
4071        .nodes()
4072        .all(|node| node.source_location.is_some())
4073}
4074
4075/// Boundary validation for programmatically constructed ASTs: every expression
4076/// in every spec of the ordered dependency list must carry a `source_location`.
4077/// Parser output always does; a miss means an external consumer built an invalid
4078/// AST, which is an `Err(Error)` at this system boundary (not a panic).
4079fn validate_ast_source_locations(
4080    ordered_dependencies: &[discovery::DependencySpec<'_>],
4081) -> Vec<Error> {
4082    let mut errors = Vec::new();
4083    for node in ordered_dependencies {
4084        let spec = node.spec;
4085        for rule in &spec.rules {
4086            if !expression_has_source_locations(&rule.expression) {
4087                errors.push(Error::validation(
4088                    format!(
4089                        "In spec '{}': rule '{}' contains an expression without a source location; \
4090                         programmatically constructed ASTs must set source_location on every expression",
4091                        spec.name, rule.name
4092                    ),
4093                    Some(rule.source_location.clone()),
4094                    None::<String>,
4095                ));
4096            }
4097            for clause in &rule.unless_clauses {
4098                if !expression_has_source_locations(&clause.condition)
4099                    || !expression_has_source_locations(&clause.result)
4100                {
4101                    errors.push(Error::validation(
4102                        format!(
4103                            "In spec '{}': rule '{}' has an unless clause with an expression without \
4104                             a source location; programmatically constructed ASTs must set \
4105                             source_location on every expression",
4106                            spec.name, rule.name
4107                        ),
4108                        Some(clause.source_location.clone()),
4109                        None::<String>,
4110                    ));
4111                }
4112            }
4113        }
4114    }
4115    errors
4116}
4117
4118impl<'a> Graph<'a> {
4119    /// Build the typed rule/data graph for main_spec using its ordered dependencies.
4120    pub(crate) fn build(
4121        context: &'a Context,
4122        repository: &Arc<LemmaRepository>,
4123        main_spec: &'a LemmaSpec,
4124        ordered_dependencies: &[discovery::DependencySpec<'a>],
4125        effective: &EffectiveDate,
4126        limits: &'a ResourceLimits,
4127        interners: &mut instance_list::PassInterners,
4128    ) -> Result<(Graph<'a>, ResolvedSpecTypes), Vec<Error>> {
4129        let boundary_errors = validate_ast_source_locations(ordered_dependencies);
4130        if !boundary_errors.is_empty() {
4131            return Err(boundary_errors);
4132        }
4133
4134        let mut errors: Vec<Error> = Vec::new();
4135
4136        let mut slices = HashMap::new();
4137        for node in ordered_dependencies {
4138            match interners
4139                .slices
4140                .of(context, &node.repository, node.spec, limits)
4141            {
4142                Ok(version_slices) => {
4143                    slices.insert(
4144                        SpecVersionKey::of(&node.repository, node.spec),
4145                        version_slices,
4146                    );
4147                }
4148                Err(slice_errors) => errors.extend(slice_errors),
4149            }
4150        }
4151        if !errors.is_empty() {
4152            return Err(errors);
4153        }
4154
4155        let mut type_resolver = TypeResolver::new(context, slices);
4156        for node in ordered_dependencies {
4157            errors.extend(type_resolver.register_all(&node.repository, node.spec));
4158        }
4159
4160        let (data, rules, dropped_rules, graph_errors, local_types, instance_lists) =
4161            match GraphBuilder::build(
4162                context,
4163                repository,
4164                main_spec,
4165                effective,
4166                &type_resolver,
4167                limits,
4168                interners,
4169            ) {
4170                Ok(built) => built,
4171                Err(build_errors) => {
4172                    errors.extend(build_errors);
4173                    return Err(errors);
4174                }
4175            };
4176
4177        let mut graph = Graph {
4178            data,
4179            rules,
4180            dropped_rules,
4181            rule_order: Vec::new(),
4182            data_reference_order: Vec::new(),
4183            reference_ends: IndexMap::new(),
4184            rule_candidates: HashMap::new(),
4185            main_spec,
4186            instance_lists,
4187            show_imports: ShowImports::default(),
4188            show_edges: BTreeMap::new(),
4189            show_order: Vec::new(),
4190            unit_scale_targets: IndexMap::new(),
4191        };
4192
4193        let validation_errors = match graph.validate(&local_types, limits) {
4194            Ok(()) => Vec::new(),
4195            Err(errors) => errors,
4196        };
4197
4198        errors.extend(graph_errors);
4199        errors.extend(validation_errors);
4200
4201        if errors.is_empty() {
4202            let main_resolved_types = local_types
4203                .into_iter()
4204                .find_map(|(_, spec, types)| {
4205                    discovery::same_loaded_spec(spec, main_spec).then_some(types)
4206                })
4207                .expect("BUG: main spec missing from local_types");
4208            Ok((graph, main_resolved_types))
4209        } else {
4210            Err(errors)
4211        }
4212    }
4213
4214    fn validate(
4215        &mut self,
4216        resolved_types: &ResolvedTypesMap,
4217        limits: &ResourceLimits,
4218    ) -> Result<(), Vec<Error>> {
4219        let mut errors = Vec::new();
4220
4221        // Structural checks (no type info needed)
4222        if let Err(structural_errors) = check_all_rule_references_exist(self) {
4223            errors.extend(structural_errors);
4224        }
4225        if let Err(depth_errors) = instance_list::check_instance_list_depths(
4226            &self.instance_lists,
4227            &self.data,
4228            limits.max_list_depth,
4229        ) {
4230            errors.extend(depth_errors);
4231        }
4232        if let Err(collision_errors) = check_data_and_rule_name_collisions(self) {
4233            errors.extend(collision_errors);
4234        }
4235
4236        // Compute the data-target reference evaluation (copy) order first: it
4237        // is both the type-resolution order for Phase 1 (targets before
4238        // dependents, so reference→reference chains resolve) and the runtime
4239        // prepop copy order. Rule-target references are resolved lazily at
4240        // evaluation time — they do not participate in either.
4241        let reference_order = match self.compute_data_reference_order() {
4242            Ok(order) => order,
4243            Err(circular_errors) => {
4244                errors.extend(circular_errors);
4245                return Err(errors);
4246            }
4247        };
4248        if let Err(circular_errors) = self.check_binding_reference_cycles() {
4249            errors.extend(circular_errors);
4250            return Err(errors);
4251        }
4252
4253        // Phase 1: Resolve data-target reference types now that all data
4254        // definitions (across all specs) are populated. Rule-target references
4255        // are resolved in Phase 4 once the target rule's type is inferred.
4256        if let Err(reference_errors) =
4257            self.resolve_data_reference_types(&reference_order, resolved_types)
4258        {
4259            errors.extend(reference_errors);
4260        }
4261
4262        // Resolve every Reference chain end once. Cycles were just rejected.
4263        // Used for rule-dep edges, normalize embeds, show promptability, eval guard.
4264        self.reference_ends = self.compute_reference_ends();
4265
4266        // Phase 2: Inject rule-rule dependency edges for rule-target references.
4267        // A rule R that reads a data path D where D is `Reference(target: rule T)`
4268        // must be evaluated AFTER T so the lazy resolver can read T's result.
4269        // This must happen before topological_sort so cycles through reference
4270        // paths are detected. List row sources contribute the same edges.
4271        self.add_rule_reference_dependency_edges();
4272        self.add_options_rule_dependency_edges();
4273        self.add_dynamic_import_pin_dependency_edges();
4274
4275        match self.link_spec_values() {
4276            Ok(member_errors) => errors.extend(member_errors),
4277            Err(cycle_errors) => {
4278                errors.extend(cycle_errors);
4279                return Err(errors);
4280            }
4281        }
4282
4283        // Phase 2b: Inject rule-rule dependency edges for rules a stored list
4284        // row forces at evaluation (nested references at materialization,
4285        // deferred field references at row-read). Needs `link_spec_values`'s
4286        // resolved SpecMember targets; must precede the final sort so cycles
4287        // through stored rows are reported here, not hit at evaluation.
4288        self.add_stored_row_dependency_edges();
4289
4290        if let Err(circular_errors) = self.sort_show_rules() {
4291            errors.extend(circular_errors);
4292            return Err(errors);
4293        }
4294
4295        let rule_order = match self.topological_sort() {
4296            Ok(order) => order,
4297            Err(circular_errors) => {
4298                errors.extend(circular_errors);
4299                return Err(errors);
4300            }
4301        };
4302
4303        // Continue to type inference and type checking even when structural
4304        // checks found errors.  This lets us report structural errors (e.g.
4305        // missing rule reference) alongside type errors (e.g. branch type
4306        // mismatch) in a single pass.
4307
4308        let has_rule_references = self.data.values().any(|def| {
4309            matches!(
4310                def,
4311                DataDefinition::Reference {
4312                    target: ReferenceTarget::Rule(_),
4313                    ..
4314                }
4315            )
4316        });
4317        let mut infer_cache = InferExpressionTypeCache::new();
4318        let (inferred_types, seed_errors) =
4319            infer_rule_types(self, &rule_order, resolved_types, &mut infer_cache);
4320        let rule_reference_errors =
4321            self.resolve_rule_reference_types(&inferred_types, resolved_types);
4322        let row_store_reference_errors = self.check_row_store_reference_types(&inferred_types);
4323        if has_rule_references {
4324            infer_cache.clear();
4325        }
4326        let type_errors = check_rule_types(
4327            self,
4328            &rule_order,
4329            &inferred_types,
4330            resolved_types,
4331            &mut infer_cache,
4332        );
4333
4334        errors.extend(seed_errors);
4335        if let Err(errs) = rule_reference_errors {
4336            errors.extend(errs);
4337        }
4338        if let Err(errs) = row_store_reference_errors {
4339            errors.extend(errs);
4340        }
4341        if let Err(errs) = type_errors {
4342            errors.extend(errs);
4343        }
4344        errors.extend(deferred_signature_errors(
4345            self,
4346            resolved_types,
4347            &inferred_types,
4348        ));
4349        errors.extend(self.check_unit_scale_target_types(resolved_types, &inferred_types));
4350
4351        if !errors.is_empty() {
4352            return Err(errors);
4353        }
4354
4355        // Phase 5: Scale dependencies need every node's type, so their edges join the
4356        // graph only now; the second sort orders scale rules first and reports cycles.
4357        let scale_errors = self.plan_unit_scales(resolved_types, &inferred_types, &mut infer_cache);
4358        if !scale_errors.is_empty() {
4359            return Err(scale_errors);
4360        }
4361        let rule_order = self.topological_sort()?;
4362
4363        errors.extend(self.validate_dynamic_import_rule_pins(&inferred_types));
4364        if !errors.is_empty() {
4365            return Err(errors);
4366        }
4367
4368        // Phase 6: Apply (only on full success)
4369        apply_inferred_types(self, inferred_types);
4370        self.rule_order = rule_order;
4371        self.data_reference_order = reference_order;
4372        Ok(())
4373    }
4374
4375    /// After rule types are inferred, `-> effective <rule>` pins must be date/datetime.
4376    fn validate_dynamic_import_rule_pins(
4377        &self,
4378        inferred_types: &HashMap<RulePath, Arc<LemmaType>>,
4379    ) -> Vec<Error> {
4380        let mut errors = Vec::new();
4381        for definition in self.data.values() {
4382            let DataDefinition::Import {
4383                pin: Some(ImportEffectivePin::Rule(path)),
4384                source,
4385                ..
4386            } = definition
4387            else {
4388                continue;
4389            };
4390            let Some(lemma_type) = inferred_types.get(path) else {
4391                errors.push(Error::validation_with_context(
4392                    format!(
4393                        "`-> effective {}` does not name a rule of this spec",
4394                        path.rule
4395                    ),
4396                    Some(source.clone()),
4397                    None::<String>,
4398                    Some(self.main_spec),
4399                    None,
4400                ));
4401                continue;
4402            };
4403            if !lemma_type.is_date() {
4404                errors.push(Error::validation_with_context(
4405                    format!(
4406                        "`-> effective` requires a date or datetime, got {}",
4407                        lemma_type.specifications
4408                    ),
4409                    Some(source.clone()),
4410                    None::<String>,
4411                    Some(self.main_spec),
4412                    None,
4413                ));
4414            }
4415        }
4416        errors
4417    }
4418}
4419
4420fn local_data_field_names(spec: &LemmaSpec) -> Vec<String> {
4421    spec.data
4422        .iter()
4423        .filter(|d| d.reference.is_local())
4424        .map(|d| d.reference.name.clone())
4425        .collect()
4426}
4427
4428fn is_uses_vs_data_clash(existing: &DataDefinition, incoming: &ParsedDataValue) -> bool {
4429    matches!(
4430        (existing, incoming),
4431        (
4432            DataDefinition::Import { .. },
4433            ParsedDataValue::Definition { .. }
4434        )
4435    ) || matches!(
4436        (existing, incoming),
4437        (
4438            DataDefinition::TypeDeclaration { .. } | DataDefinition::Value { .. },
4439            ParsedDataValue::Import { .. }
4440        )
4441    )
4442}
4443
4444fn qualified_type_name_from_definition(incoming: &ParsedDataValue) -> Option<&str> {
4445    let ParsedDataValue::Definition {
4446        base: Some(ParentType::Qualified { inner, .. }),
4447        ..
4448    } = incoming
4449    else {
4450        return None;
4451    };
4452    match inner.as_ref() {
4453        ParentType::Custom { name, .. } => Some(name.as_str()),
4454        _ => None,
4455    }
4456}
4457
4458fn import_spec_ref_for_clash<'a>(
4459    consumer_spec: &'a LemmaSpec,
4460    incoming: &'a LemmaData,
4461    existing: &DataDefinition,
4462) -> std::borrow::Cow<'a, ast::SpecRef> {
4463    if let ParsedDataValue::Import { spec_ref, .. } = &incoming.value {
4464        return std::borrow::Cow::Borrowed(spec_ref);
4465    }
4466    let alias = &incoming.reference.name;
4467    for row in &consumer_spec.data {
4468        if !row.reference.is_local() || row.reference.name != *alias {
4469            continue;
4470        }
4471        let ParsedDataValue::Import { spec_ref, .. } = &row.value else {
4472            continue;
4473        };
4474        return std::borrow::Cow::Borrowed(spec_ref);
4475    }
4476    let DataDefinition::Import { row, .. } = existing else {
4477        unreachable!("BUG: uses vs data clash but no Import side with recoverable spec_ref");
4478    };
4479    std::borrow::Cow::Owned(ast::SpecRef::same_repository(row.identity.spec.clone()))
4480}
4481
4482fn uses_vs_data_duplicate_message(
4483    consumer_spec: &LemmaSpec,
4484    incoming: &LemmaData,
4485    existing: &DataDefinition,
4486) -> (String, Option<String>) {
4487    match (existing, &incoming.value) {
4488        (DataDefinition::Import { .. }, ParsedDataValue::Definition { .. })
4489        | (
4490            DataDefinition::TypeDeclaration { .. } | DataDefinition::Value { .. },
4491            ParsedDataValue::Import { .. },
4492        ) => {}
4493        _ => unreachable!("uses_vs_data_duplicate_message requires a uses vs data clash"),
4494    };
4495    let alias = &incoming.reference.name;
4496    let spec_ref = import_spec_ref_for_clash(consumer_spec, incoming, existing);
4497    let target_label = spec_ref.to_string();
4498    let import_alias = format!("{alias}_spec");
4499    let message = format!(
4500        "You used the name `{alias}` for both a `uses` import (target {target_label}) and a `data` definition.",
4501    );
4502    let suggestion = match qualified_type_name_from_definition(&incoming.value) {
4503        Some(type_name) => format!(
4504            "Rename the `uses` import alias to `{import_alias}`. The qualified type path must use `{import_alias}.{type_name}`."
4505        ),
4506        _ => format!("Rename the `uses` import alias to `{import_alias}`."),
4507    };
4508    (message, Some(suggestion))
4509}
4510
4511impl<'a, 'i> GraphBuilder<'a, 'i> {
4512    fn build(
4513        context: &'a Context,
4514        repository: &Arc<LemmaRepository>,
4515        main_spec: &'a LemmaSpec,
4516        effective: &EffectiveDate,
4517        type_resolver: &TypeResolver<'a>,
4518        limits: &'a ResourceLimits,
4519        interners: &'i mut instance_list::PassInterners,
4520    ) -> Result<GraphBuildOk<'a>, Vec<Error>> {
4521        let mut builder = GraphBuilder {
4522            data: IndexMap::new(),
4523            rules: BTreeMap::new(),
4524            dropped_rules: BTreeSet::new(),
4525            context,
4526            local_types: Vec::new(),
4527            errors: Vec::new(),
4528            nested_errors: Vec::new(),
4529            nested_spec_failed: false,
4530            main_spec,
4531            limits,
4532            instance_lists: InstanceLists::new(SpecIdentity::of(
4533                repository,
4534                main_spec,
4535                effective.clone(),
4536            )),
4537            interners,
4538            self_list_row_rules: BTreeSet::new(),
4539        };
4540
4541        builder.build_spec(main_spec, repository, effective, type_resolver);
4542
4543        if builder.nested_spec_failed {
4544            return Err(builder.nested_errors);
4545        }
4546
4547        builder.resolve_unit_scales();
4548        builder.mark_self_list_row_rules_recursive();
4549
4550        Ok((
4551            builder.data,
4552            builder.rules,
4553            builder.dropped_rules,
4554            builder.errors,
4555            builder.local_types,
4556            builder.instance_lists,
4557        ))
4558    }
4559
4560    fn engine_error(&self, message: impl Into<String>, source: &Source) -> Error {
4561        Error::validation_with_context(
4562            message.into(),
4563            Some(source.clone()),
4564            None::<String>,
4565            Some(self.main_spec),
4566            None,
4567        )
4568    }
4569
4570    fn resolve_unit_ref(
4571        &self,
4572        current_spec: &LemmaSpec,
4573        unit_ref: &str,
4574    ) -> Result<(String, Arc<LemmaType>), String> {
4575        let resolved = find_types_by_spec(&self.local_types, current_spec)
4576            .ok_or_else(|| {
4577                format!(
4578                    "Unknown unit '{unit_ref}'. Declare it on a measure or ratio type, or import it with uses."
4579                )
4580            })?;
4581        resolved
4582            .unit_index
4583            .resolve_with_named_types(unit_ref, &resolved.resolved)
4584    }
4585
4586    /// Build a measure/ratio [`ValueKind::Range`] with each endpoint typed and stamped with its
4587    /// bare resolved unit. Same-unit and mixed-unit ranges both go through this path.
4588    fn measure_ratio_range_from_number_with_unit_endpoints(
4589        &self,
4590        current_spec: &LemmaSpec,
4591        left_mag: rust_decimal::Decimal,
4592        left_unit: &str,
4593        right_mag: rust_decimal::Decimal,
4594        right_unit: &str,
4595    ) -> Result<ValueKind, String> {
4596        let (left_bare, left_lt) = self.resolve_unit_ref(current_spec, left_unit)?;
4597        let (right_bare, right_lt) = self.resolve_unit_ref(current_spec, right_unit)?;
4598        let left_kind = number_with_unit_to_value_kind(left_mag, &left_bare, left_lt.as_ref())?;
4599        let right_kind = number_with_unit_to_value_kind(right_mag, &right_bare, right_lt.as_ref())?;
4600        let left_ty = Arc::new(left_lt.as_ref().clone().with_unit(left_bare.as_str()));
4601        let right_ty = Arc::new(right_lt.as_ref().clone().with_unit(right_bare.as_str()));
4602        range_type_specification_from_endpoints(left_ty.as_ref(), right_ty.as_ref()).ok_or_else(
4603            || {
4604                format!(
4605                    "Range endpoints must share a measure or ratio family (got '{}' and '{}')",
4606                    left_lt.name(),
4607                    right_lt.name()
4608                )
4609            },
4610        )?;
4611        Ok(ValueKind::Range(
4612            Box::new(TypedLiteral {
4613                value: left_kind,
4614                lemma_type: left_ty,
4615            }),
4616            Box::new(TypedLiteral {
4617                value: right_kind,
4618                lemma_type: right_ty,
4619            }),
4620        ))
4621    }
4622
4623    /// Fill [`UnitScale::Reference`] targets on every measure unit declared in
4624    /// this spec. Local names resolve against this spec; a uses-qualified name
4625    /// walks this builder's import rows. Imported types keep declaring-spec
4626    /// coordinates (local to the family spec).
4627    fn resolve_unit_scales(&mut self) {
4628        let spec = self.main_spec;
4629        let Some(spec_idx) = self
4630            .local_types
4631            .iter()
4632            .position(|(_, registered, _)| discovery::same_loaded_spec(registered, spec))
4633        else {
4634            return;
4635        };
4636        let type_names: Vec<String> = self.local_types[spec_idx]
4637            .2
4638            .resolved
4639            .keys()
4640            .cloned()
4641            .collect();
4642        for type_name in type_names {
4643            self.resolve_type_unit_scales(spec_idx, &type_name);
4644        }
4645    }
4646
4647    fn resolve_type_unit_scales(&mut self, spec_idx: usize, type_name: &str) {
4648        let lemma_type = Arc::clone(
4649            self.local_types[spec_idx]
4650                .2
4651                .resolved
4652                .get(type_name)
4653                .expect("BUG: type_name comes from resolved keys"),
4654        );
4655        let TypeSpecification::Measure { units, .. } = &lemma_type.specifications else {
4656            return;
4657        };
4658        let dynamic: Vec<(usize, Vec<String>, String)> = units
4659            .iter()
4660            .enumerate()
4661            .filter_map(|(idx, unit)| {
4662                let (segments, name) = unit.scale_reference_parts()?;
4663                if unit.resolved_scale_target().is_some() {
4664                    return None;
4665                }
4666                Some((idx, segments.to_vec(), name.to_string()))
4667            })
4668            .collect();
4669        if dynamic.is_empty() {
4670            return;
4671        }
4672        let spec = self.local_types[spec_idx].1;
4673        let source = spec
4674            .data
4675            .iter()
4676            .find(|data| data.reference.is_local() && data.reference.name == type_name)
4677            .map(|data| data.source_location.clone())
4678            .or_else(|| spec.data.first().map(|data| data.source_location.clone()))
4679            .or_else(|| spec.rules.first().map(|rule| rule.source_location.clone()))
4680            .expect("BUG: a spec that declares a measure type has a source location");
4681        let family_spec = lemma_type
4682            .measure_family()
4683            .map(|family| family.spec.as_str())
4684            .unwrap_or(spec.name.as_str());
4685        let mut resolved_targets: Vec<(usize, ReferenceTarget)> = Vec::new();
4686        for (idx, segments, name) in dynamic {
4687            let reference = ast::Reference { segments, name };
4688            let result = if family_spec == spec.name {
4689                self.resolve_scale_reference(spec, &reference, &source)
4690            } else {
4691                self.resolve_imported_scale_reference(family_spec, &reference, &source)
4692            };
4693            match result {
4694                Ok(target) => resolved_targets.push((idx, target)),
4695                Err(error) => self.errors.push(error),
4696            }
4697        }
4698        if resolved_targets.is_empty() {
4699            return;
4700        }
4701        let mut owned = lemma_type.as_ref().clone();
4702        let TypeSpecification::Measure { units, .. } = &mut owned.specifications else {
4703            unreachable!("BUG: measure type still a measure after clone");
4704        };
4705        for (idx, target) in resolved_targets {
4706            units.0[idx] = units.0[idx].clone().with_resolved_scale(target);
4707        }
4708        self.local_types[spec_idx]
4709            .2
4710            .resolved
4711            .insert(type_name.to_string(), Arc::new(owned));
4712    }
4713
4714    fn resolve_imported_scale_reference(
4715        &self,
4716        family_spec: &str,
4717        reference: &ast::Reference,
4718        source: &Source,
4719    ) -> Result<ReferenceTarget, Error> {
4720        let Some(template) = self
4721            .instance_lists
4722            .templates
4723            .values()
4724            .find(|plan| plan.spec_name == family_spec)
4725        else {
4726            return Err(self.engine_error(
4727                format!(
4728                    "Reference target '{}' not found in spec '{family_spec}'",
4729                    reference.name
4730                ),
4731                source,
4732            ));
4733        };
4734        if !reference.segments.is_empty() {
4735            return Err(self.engine_error(
4736                format!(
4737                    "unit scale reference '{reference}' on an imported measure must be local to '{family_spec}'"
4738                ),
4739                source,
4740            ));
4741        }
4742        member_in_template(template, &reference.name, |message| {
4743            self.engine_error(message, source)
4744        })
4745    }
4746
4747    fn resolve_scale_reference(
4748        &self,
4749        spec: &LemmaSpec,
4750        reference: &ast::Reference,
4751        source: &Source,
4752    ) -> Result<ReferenceTarget, Error> {
4753        if reference.segments.is_empty() {
4754            let is_data = spec
4755                .data
4756                .iter()
4757                .any(|d| d.reference.is_local() && d.reference.name == reference.name);
4758            let is_rule = spec.rules.iter().any(|r| r.name == reference.name);
4759            if is_data && is_rule {
4760                return Err(self.engine_error(
4761                    format!(
4762                        "Reference target '{}' is ambiguous: both a data and a rule in spec '{}'",
4763                        reference.name, spec.name
4764                    ),
4765                    source,
4766                ));
4767            }
4768            if is_data {
4769                let is_import = spec.data.iter().any(|d| {
4770                    d.reference.is_local()
4771                        && d.reference.name == reference.name
4772                        && matches!(d.value, ParsedDataValue::Import { .. })
4773                });
4774                if is_import {
4775                    return Err(self.engine_error(
4776                        format!(
4777                            "cannot supply spec instance '{}' as a value",
4778                            reference.name
4779                        ),
4780                        source,
4781                    ));
4782                }
4783                return Ok(ReferenceTarget::Data(DataPath::local(
4784                    reference.name.clone(),
4785                )));
4786            }
4787            if is_rule {
4788                return Ok(ReferenceTarget::Rule(RulePath::new(
4789                    Vec::new(),
4790                    reference.name.clone(),
4791                )));
4792            }
4793            return Err(self.engine_error(
4794                format!(
4795                    "Reference target '{}' not found in spec '{}'",
4796                    reference.name, spec.name
4797                ),
4798                source,
4799            ));
4800        }
4801
4802        let [alias] = reference.segments.as_slice() else {
4803            return Err(self.engine_error(
4804                format!(
4805                    "unit scale reference '{reference}' is not a data or rule path in spec '{}'",
4806                    spec.name
4807                ),
4808                source,
4809            ));
4810        };
4811        let Some(DataDefinition::Import { row, .. }) =
4812            self.data.get(&DataPath::local(alias.clone()))
4813        else {
4814            return Err(self.engine_error(
4815                format!(
4816                    "Reference target '{}' not found in spec '{}'",
4817                    reference, spec.name
4818                ),
4819                source,
4820            ));
4821        };
4822        let template = self
4823            .instance_lists
4824            .templates
4825            .get(row.identity.as_ref())
4826            .unwrap_or_else(|| {
4827                panic!(
4828                    "BUG: import '{alias}' has no template plan for '{}'",
4829                    row.identity.spec
4830                )
4831            });
4832        let hop = PathSegment::import(alias, row.identity.as_ref());
4833        match member_in_template(template, &reference.name, |message| {
4834            self.engine_error(message, source)
4835        })? {
4836            ReferenceTarget::Data(path) => {
4837                let mut segments = vec![hop];
4838                segments.extend(path.segments);
4839                Ok(ReferenceTarget::Data(DataPath {
4840                    segments,
4841                    data: path.data,
4842                }))
4843            }
4844            ReferenceTarget::Rule(path) => {
4845                let mut segments = vec![hop];
4846                segments.extend(path.segments);
4847                Ok(ReferenceTarget::Rule(RulePath {
4848                    segments,
4849                    rule: path.rule,
4850                }))
4851            }
4852        }
4853    }
4854
4855    fn process_meta_fields(&mut self, spec: &LemmaSpec) {
4856        let mut seen = HashSet::new();
4857        for field in &spec.meta_fields {
4858            if !seen.insert(field.key.clone()) {
4859                self.errors.push(self.engine_error(
4860                    format!("Duplicate meta key '{}'", field.key),
4861                    &field.source_location,
4862                ));
4863            }
4864        }
4865    }
4866
4867    fn resolve_spec_ref(
4868        &self,
4869        spec_ref: &ast::SpecRef,
4870        effective: &EffectiveDate,
4871        consumer_spec: &LemmaSpec,
4872        consumer_repository: &Arc<LemmaRepository>,
4873    ) -> Result<(Arc<LemmaRepository>, &'a LemmaSpec), Error> {
4874        discovery::resolve_spec_ref(
4875            self.context,
4876            spec_ref,
4877            consumer_repository,
4878            consumer_spec,
4879            effective,
4880            None,
4881        )
4882    }
4883
4884    fn push_missing_with_data_field_error(
4885        &mut self,
4886        import_row: &LemmaData,
4887        binding: &ast::UsesBinding,
4888        walk_spec: &LemmaSpec,
4889        field_name: &str,
4890    ) {
4891        let ParsedDataValue::Import { spec_ref, .. } = &import_row.value else {
4892            unreachable!("BUG: push_missing_with_data_field_error called on non-Import LemmaData");
4893        };
4894        let import_alias = &import_row.reference.name;
4895        let target_label = spec_ref.to_string();
4896        let did_you_mean = closest_name(field_name, &local_data_field_names(walk_spec));
4897        let message = format!("`{}` has no data field `{}`", walk_spec.name, field_name);
4898        let mut suggestion = format!(
4899            "`-> with` on import `{import_alias}` (target {target_label}) must name a data field on `{spec}`",
4900            spec = walk_spec.name,
4901        );
4902        if let Some(name) = did_you_mean {
4903            suggestion = format!("{suggestion}. Did you mean `{name}`?");
4904        }
4905        self.errors.push(Error::validation_with_context(
4906            message,
4907            Some(binding.source_location.clone()),
4908            Some(suggestion),
4909            Some(self.main_spec),
4910            None,
4911        ));
4912    }
4913
4914    /// Resolve a parsed [`ast::Reference`] appearing on the RHS of a `data x: ref`
4915    /// assignment against the scope of `containing_spec`. Returns an
4916    /// [`ReferenceTarget`] pointing at a data path or rule path. Errors push into
4917    /// `self.errors`; this function returns `None` on failure (and does not
4918    /// return a proper `Result` because it mirrors `resolve_path_segments`'s
4919    /// side-effecting convention so the two can compose cleanly).
4920    fn resolve_reference_target_in_spec(
4921        &mut self,
4922        reference: &ast::Reference,
4923        reference_source: &Source,
4924        containing_spec: &'a LemmaSpec,
4925        containing_repository: &Arc<LemmaRepository>,
4926        effective: &EffectiveDate,
4927    ) -> Option<ReferenceTarget> {
4928        if reference.segments.is_empty() {
4929            let data = containing_spec
4930                .data
4931                .iter()
4932                .find(|d| d.reference.is_local() && d.reference.name == reference.name);
4933            let is_rule = containing_spec
4934                .rules
4935                .iter()
4936                .any(|r| r.name == reference.name);
4937            if data.is_some() && is_rule {
4938                self.errors.push(self.engine_error(
4939                    format!(
4940                        "Reference target '{}' is ambiguous: both a data and a rule in spec '{}'",
4941                        reference.name, containing_spec.name
4942                    ),
4943                    reference_source,
4944                ));
4945                return None;
4946            }
4947            if let Some(data) = data {
4948                if matches!(data.value, ParsedDataValue::Import { .. }) {
4949                    self.errors.push(self.engine_error(
4950                        format!(
4951                            "cannot supply spec instance '{}' as a value",
4952                            reference.name
4953                        ),
4954                        reference_source,
4955                    ));
4956                    return None;
4957                }
4958                return Some(ReferenceTarget::Data(DataPath::local(
4959                    reference.name.clone(),
4960                )));
4961            }
4962            if is_rule {
4963                return Some(ReferenceTarget::Rule(RulePath::new(
4964                    Vec::new(),
4965                    reference.name.clone(),
4966                )));
4967            }
4968            self.errors.push(self.engine_error(
4969                format!(
4970                    "Reference target '{}' not found in spec '{}'",
4971                    reference.name, containing_spec.name
4972                ),
4973                reference_source,
4974            ));
4975            return None;
4976        }
4977
4978        let (resolved_segments, target_spec) = self.resolve_path_segments(
4979            &reference.segments,
4980            reference_source,
4981            Vec::new(),
4982            containing_spec,
4983            containing_repository,
4984            effective,
4985        )?;
4986
4987        let is_data = target_spec
4988            .data
4989            .iter()
4990            .any(|d| d.reference.is_local() && d.reference.name == reference.name);
4991        let is_rule = target_spec.rules.iter().any(|r| r.name == reference.name);
4992
4993        if is_data && is_rule {
4994            self.errors.push(self.engine_error(
4995                format!(
4996                    "Reference target '{}' is ambiguous: both a data and a rule in spec '{}'",
4997                    reference.name, target_spec.name
4998                ),
4999                reference_source,
5000            ));
5001            return None;
5002        }
5003        if is_data {
5004            if target_spec.data.iter().any(|field| {
5005                field.reference.name == reference.name
5006                    && field.reference.segments.is_empty()
5007                    && matches!(field.value, ParsedDataValue::Import { .. })
5008            }) {
5009                self.errors.push(self.engine_error(
5010                    format!(
5011                        "cannot supply spec instance '{}' as a value",
5012                        reference.name
5013                    ),
5014                    reference_source,
5015                ));
5016                return None;
5017            }
5018            return Some(ReferenceTarget::Data(DataPath {
5019                segments: resolved_segments,
5020                data: reference.name.clone(),
5021            }));
5022        }
5023        if is_rule {
5024            return Some(ReferenceTarget::Rule(RulePath {
5025                segments: resolved_segments,
5026                rule: reference.name.clone(),
5027            }));
5028        }
5029
5030        self.errors.push(self.engine_error(
5031            format!(
5032                "Reference target '{}' not found in spec '{}'",
5033                reference.name, target_spec.name
5034            ),
5035            reference_source,
5036        ));
5037        None
5038    }
5039
5040    /// Collect `uses` block bindings (`-> with`) declared on import rows in `spec`.
5041    /// Keys are relative to `spec`: `uses kind -> with salary: 1` is `["kind", "salary"]`.
5042    fn build_data_bindings(
5043        &mut self,
5044        spec: &'a LemmaSpec,
5045        spec_repository: &Arc<LemmaRepository>,
5046        effective: &EffectiveDate,
5047    ) -> Result<DataBindings, Vec<Error>> {
5048        let mut bindings: DataBindings = IndexMap::new();
5049        let mut errors: Vec<Error> = Vec::new();
5050
5051        for import_row in &spec.data {
5052            if !import_row.reference.is_local() {
5053                continue;
5054            }
5055            let ParsedDataValue::Import {
5056                bindings: uses_bindings,
5057                ..
5058            } = &import_row.value
5059            else {
5060                continue;
5061            };
5062            for binding in uses_bindings {
5063                let binding_reference =
5064                    ast::prefix_reference(&import_row.reference.name, &binding.path);
5065                let mut walk_spec = spec;
5066                let mut walk_repository = Arc::clone(spec_repository);
5067                let mut walk_effective = effective.clone();
5068                let mut binding_resolved = true;
5069
5070                for segment in &binding_reference.segments {
5071                    let Some(seg_data) = walk_spec.data.iter().find(|field| {
5072                        field.reference.segments.is_empty() && field.reference.name == *segment
5073                    }) else {
5074                        self.push_missing_with_data_field_error(
5075                            import_row, binding, walk_spec, segment,
5076                        );
5077                        binding_resolved = false;
5078                        break;
5079                    };
5080
5081                    match &seg_data.value {
5082                        ParsedDataValue::Import {
5083                            spec_ref,
5084                            effective_ref: Some(_),
5085                            ..
5086                        } => {
5087                            let targets = match self.dynamic_import_targets(
5088                                spec_ref,
5089                                walk_spec,
5090                                &walk_repository,
5091                                &binding.source_location,
5092                            ) {
5093                                Ok(targets) => targets,
5094                                Err(error) => {
5095                                    self.errors.push(error);
5096                                    binding_resolved = false;
5097                                    break;
5098                                }
5099                            };
5100                            let last_hop = binding_reference.segments.last() == Some(segment);
5101                            if last_hop {
5102                                let field = binding_reference.name.as_str();
5103                                let mut missing_on = None;
5104                                for target in &targets {
5105                                    if !target.spec.data.iter().any(|row| {
5106                                        row.reference.segments.is_empty()
5107                                            && row.reference.name == field
5108                                    }) {
5109                                        missing_on = Some(target.spec);
5110                                        break;
5111                                    }
5112                                }
5113                                if let Some(candidate) = missing_on {
5114                                    self.push_missing_with_data_field_error(
5115                                        import_row, binding, candidate, field,
5116                                    );
5117                                    binding_resolved = false;
5118                                    break;
5119                                }
5120                            }
5121                            let first = targets
5122                                .into_iter()
5123                                .next()
5124                                .expect("BUG: dynamic_import_targets returns at least one target");
5125                            walk_effective = first.at;
5126                            walk_repository = first.repository;
5127                            walk_spec = first.spec;
5128                        }
5129                        ParsedDataValue::Import { spec_ref, .. } => {
5130                            walk_spec = match self.resolve_spec_ref(
5131                                spec_ref,
5132                                &walk_effective,
5133                                walk_spec,
5134                                &walk_repository,
5135                            ) {
5136                                Ok((repository, arc)) => {
5137                                    walk_effective = spec_ref.at(&walk_effective);
5138                                    walk_repository = repository;
5139                                    arc
5140                                }
5141                                Err(error) => {
5142                                    self.errors.push(error);
5143                                    binding_resolved = false;
5144                                    break;
5145                                }
5146                            };
5147                        }
5148                        ParsedDataValue::Definition { .. }
5149                            if self.is_data_instance_list(
5150                                seg_data,
5151                                walk_spec,
5152                                &walk_repository,
5153                                &walk_effective,
5154                            ) =>
5155                        {
5156                            self.errors.push(Error::validation_with_context(
5157                                format!(
5158                                    "binding `{path}` replaces instance list `{segment}`; supply the whole list through evaluation data",
5159                                    path = binding_reference,
5160                                ),
5161                                Some(binding.source_location.clone()),
5162                                None::<String>,
5163                                Some(self.main_spec),
5164                                None,
5165                            ));
5166                            binding_resolved = false;
5167                            break;
5168                        }
5169                        _ => {
5170                            self.errors.push(Error::validation_with_context(
5171                                format!(
5172                                    "`{segment}` in `{spec}` is not an imported spec — `-> with` paths step through data fields that hold a `uses` import.",
5173                                    spec = walk_spec.name
5174                                ),
5175                                Some(binding.source_location.clone()),
5176                                Some(format!(
5177                                    "`-> with` on import `{alias}` must step through import data fields",
5178                                    alias = import_row.reference.name,
5179                                )),
5180                                Some(self.main_spec),
5181                                None,
5182                            ));
5183                            binding_resolved = false;
5184                            break;
5185                        }
5186                    }
5187                }
5188
5189                if !binding_resolved {
5190                    continue;
5191                }
5192
5193                if !walk_spec.data.iter().any(|field| {
5194                    field.reference.segments.is_empty()
5195                        && field.reference.name == binding_reference.name
5196                }) {
5197                    self.push_missing_with_data_field_error(
5198                        import_row,
5199                        binding,
5200                        walk_spec,
5201                        &binding_reference.name,
5202                    );
5203                    continue;
5204                }
5205
5206                let target_is_instance_list = walk_spec.data.iter().any(|field| {
5207                    field.reference.segments.is_empty()
5208                        && field.reference.name == binding_reference.name
5209                        && self.is_data_instance_list(
5210                            field,
5211                            walk_spec,
5212                            &walk_repository,
5213                            &walk_effective,
5214                        )
5215                });
5216
5217                let mut binding_key: Vec<String> = binding_reference.segments.clone();
5218                binding_key.push(binding_reference.name.clone());
5219
5220                let binding_value = match &binding.rhs {
5221                    ast::UsesBindingRhs::Literal(value) => {
5222                        if target_is_instance_list {
5223                            self.errors.push(Error::validation_with_context(
5224                                format!(
5225                                    "binding `{path}` replaces instance list `{name}` with a literal; list slots take `uses` aliases",
5226                                    path = binding_reference,
5227                                    name = binding_reference.name,
5228                                ),
5229                                Some(binding.source_location.clone()),
5230                                None::<String>,
5231                                Some(self.main_spec),
5232                                None,
5233                            ));
5234                            continue;
5235                        }
5236                        BindingValue::Literal(value.clone())
5237                    }
5238                    ast::UsesBindingRhs::References { targets } if target_is_instance_list => {
5239                        BindingValue::InstanceAliases(targets.clone())
5240                    }
5241                    ast::UsesBindingRhs::References { targets } => {
5242                        let [target] = targets.as_slice() else {
5243                            self.errors.push(Error::validation_with_context(
5244                                format!(
5245                                    "binding `{binding_reference}` targets a scalar slot but supplies {} references",
5246                                    targets.len()
5247                                ),
5248                                Some(binding.source_location.clone()),
5249                                None::<String>,
5250                                Some(self.main_spec),
5251                                None,
5252                            ));
5253                            continue;
5254                        };
5255                        let Some(resolved_target) = self.resolve_reference_target_in_spec(
5256                            target,
5257                            &binding.source_location,
5258                            spec,
5259                            spec_repository,
5260                            effective,
5261                        ) else {
5262                            continue;
5263                        };
5264                        BindingValue::Reference {
5265                            target: resolved_target,
5266                        }
5267                    }
5268                };
5269
5270                let source = binding.source_location.clone();
5271                if let Some((_, existing_source)) = bindings.get(&binding_key) {
5272                    errors.push(self.engine_error(
5273                        format!(
5274                            "Duplicate data binding for '{}' (previously bound at {}:{})",
5275                            binding_key.join("."),
5276                            existing_source.source_type,
5277                            existing_source.span.line
5278                        ),
5279                        &binding.source_location,
5280                    ));
5281                } else {
5282                    bindings.insert(binding_key, (binding_value, source));
5283                }
5284            }
5285        }
5286
5287        if !errors.is_empty() {
5288            return Err(errors);
5289        }
5290
5291        Ok(bindings)
5292    }
5293
5294    /// `data x: row_spec list` whose element names a loadable Spec (not a value-list type).
5295    fn is_data_instance_list(
5296        &self,
5297        data: &LemmaData,
5298        current_spec: &LemmaSpec,
5299        current_repository: &Arc<LemmaRepository>,
5300        effective: &EffectiveDate,
5301    ) -> bool {
5302        let Some(spec_ref) = discovery::data_instance_list_spec_ref(current_spec, data) else {
5303            return false;
5304        };
5305        discovery::resolve_spec_ref_or_self(
5306            self.context,
5307            &spec_ref,
5308            current_repository,
5309            current_spec,
5310            effective,
5311            None,
5312        )
5313        .is_ok()
5314    }
5315
5316    /// Reports and returns true when `data_path` is already taken in this spec.
5317    fn is_duplicate_data(
5318        &mut self,
5319        data: &LemmaData,
5320        data_path: &DataPath,
5321        current_spec: &LemmaSpec,
5322    ) -> bool {
5323        let Some(existing) = self.data.get(data_path) else {
5324            return false;
5325        };
5326        let (message, suggestion) = if is_uses_vs_data_clash(existing, &data.value) {
5327            uses_vs_data_duplicate_message(current_spec, data, existing)
5328        } else {
5329            (
5330                format!(
5331                    "The name '{}' is already used for data in this spec.",
5332                    data_path.data
5333                ),
5334                None,
5335            )
5336        };
5337        self.errors.push(Error::validation_with_context(
5338            message,
5339            Some(data.source_location.clone()),
5340            suggestion,
5341            Some(self.main_spec),
5342            None,
5343        ));
5344        true
5345    }
5346
5347    /// Add a single scalar data or import marker to the graph. Instance lists are
5348    /// opened by the spec frame ([`Self::open_instance_list`]).
5349    fn add_data(
5350        &mut self,
5351        data: &LemmaData,
5352        current_spec: &'a LemmaSpec,
5353        current_repository: &Arc<LemmaRepository>,
5354        effective: &EffectiveDate,
5355    ) {
5356        let data_path = DataPath::local(data.reference.name.clone());
5357        if self.is_duplicate_data(data, &data_path, current_spec) {
5358            return;
5359        }
5360
5361        if let ParsedDataValue::Definition {
5362            base: Some(ParentType::List { .. }),
5363            list,
5364            ..
5365        } = &data.value
5366        {
5367            let is_instance =
5368                self.is_data_instance_list(data, current_spec, current_repository, effective);
5369            if list.is_some() && !is_instance {
5370                self.errors.push(Error::validation_with_context(
5371                    "value lists are numbered; `-> key` is for instance lists".to_string(),
5372                    Some(data.source_location.clone()),
5373                    None::<String>,
5374                    Some(self.main_spec),
5375                    None,
5376                ));
5377                return;
5378            }
5379        }
5380
5381        let (original_schema_type, original_declared_suggestion, original_declared_value) = if matches!(
5382            &data.value,
5383            ParsedDataValue::Definition { .. }
5384        )
5385            && data.value.definition_needs_type_resolution()
5386            && !self.is_data_instance_list(data, current_spec, current_repository, effective)
5387        {
5388            let resolved = self
5389                .local_types
5390                .iter()
5391                .find(|(_, s, _)| discovery::same_loaded_spec(s, current_spec))
5392                .map(|(_, _, t)| t)
5393                .expect("BUG: no resolved types for spec during add_local_data");
5394            let lemma_type = Arc::clone(
5395                resolved
5396                    .resolved
5397                    .get(&data.reference.name)
5398                    .expect(
5399                        "BUG: type not in ResolvedSpecTypes.resolved. TypeResolver should have registered it",
5400                    ),
5401            );
5402            let declared = resolved
5403                .declared_suggestions
5404                .get(&data.reference.name)
5405                .cloned();
5406            let declared_value = resolved.declared_values.get(&data.reference.name).cloned();
5407            (Some(lemma_type), declared, declared_value)
5408        } else {
5409            (None, None, None)
5410        };
5411
5412        let effective_source = data.source_location.clone();
5413
5414        match &data.value {
5415            ParsedDataValue::Definition { .. }
5416                if self.is_data_instance_list(
5417                    data,
5418                    current_spec,
5419                    current_repository,
5420                    effective,
5421                ) =>
5422            {
5423                unreachable!("BUG: instance list '{data_path}' is opened by its spec frame")
5424            }
5425            ParsedDataValue::Definition { .. } if data.value.is_definition_literal_only() => {
5426                let ParsedDataValue::Definition {
5427                    value: Some(value), ..
5428                } = &data.value
5429                else {
5430                    unreachable!("BUG: literal-only Definition must carry value");
5431                };
5432                self.insert_literal_data(
5433                    data_path,
5434                    value,
5435                    original_schema_type,
5436                    effective_source,
5437                    current_spec,
5438                    DataOrigin::Declared,
5439                );
5440            }
5441            ParsedDataValue::Definition { .. } => {
5442                let resolved_type = original_schema_type.unwrap_or_else(|| {
5443                    unreachable!(
5444                        "BUG: Definition without schema — TypeResolver should have registered it"
5445                    )
5446                });
5447                let declared_suggestion = original_declared_suggestion;
5448                let declared_value = original_declared_value;
5449
5450                let is_generic_measure_range = matches!(
5451                    &resolved_type.specifications,
5452                    TypeSpecification::MeasureRange {
5453                        units,
5454                        decomposition,
5455                        ..
5456                    } if units.0.is_empty() && decomposition.is_none()
5457                );
5458
5459                if is_generic_measure_range {
5460                    let measure_endpoint_suggest = matches!(
5461                        &declared_suggestion,
5462                        Some(BoundValueKind {
5463                            value: ValueKind::Range(left, right),
5464                            ..
5465                        }) if matches!(
5466                            (&left.value, &right.value),
5467                            (ValueKind::Measure(_), ValueKind::Measure(_))
5468                        )
5469                    );
5470                    let measure_endpoint_fill = matches!(
5471                        &declared_value,
5472                        Some(BoundValueKind {
5473                            value: ValueKind::Range(left, right),
5474                            ..
5475                        }) if matches!(
5476                            (&left.value, &right.value),
5477                            (ValueKind::Measure(_), ValueKind::Measure(_))
5478                        )
5479                    );
5480                    if measure_endpoint_suggest || measure_endpoint_fill {
5481                        self.errors.push(self.engine_error(
5482                            "measure range requires unit declarations before a measure-endpoint suggest or with".to_string(),
5483                            &effective_source,
5484                        ));
5485                        return;
5486                    }
5487                }
5488
5489                self.data.insert(
5490                    data_path,
5491                    DataDefinition::TypeDeclaration {
5492                        resolved_type,
5493                        declared_suggestion,
5494                        declared_value,
5495                        source: effective_source,
5496                    },
5497                );
5498            }
5499            ParsedDataValue::ImportList { .. } => {
5500                panic!("BUG: uses list is rejected at parse and must not reach planning");
5501            }
5502            ParsedDataValue::Import { .. } => {
5503                unreachable!("BUG: `uses {data_path}` is added as an import row")
5504            }
5505        }
5506    }
5507
5508    /// `data x: spec list`: validates the slot, resolves its row spec and key, builds
5509    /// the row store, checks row keys and installs the slot. Rows are data checked
5510    /// against the row spec's template plan; nothing nests under a list.
5511    fn plan_instance_list(
5512        &mut self,
5513        data: &'a LemmaData,
5514        current_spec: &'a LemmaSpec,
5515        current_repository: &Arc<LemmaRepository>,
5516        effective: &EffectiveDate,
5517        bindings: &DataBindings,
5518    ) {
5519        let data_path = DataPath::local(data.reference.name.clone());
5520        if self.is_duplicate_data(data, &data_path, current_spec) {
5521            return;
5522        }
5523        let Some(spec_ref) = discovery::data_instance_list_spec_ref(current_spec, data) else {
5524            panic!("BUG: plan_instance_list on non-instance-list data '{data_path}'");
5525        };
5526        let list = match &data.value {
5527            ParsedDataValue::Definition {
5528                list: Some(list), ..
5529            } => list,
5530            ParsedDataValue::Definition { list: None, .. } => &EMPTY_INSTANCE_LIST,
5531            _ => panic!("BUG: plan_instance_list requires Definition on '{data_path}'"),
5532        };
5533        let source = data.source_location.clone();
5534        let (row_repository, row_spec) = match discovery::resolve_spec_ref_or_self(
5535            self.context,
5536            &spec_ref,
5537            current_repository,
5538            current_spec,
5539            effective,
5540            None,
5541        ) {
5542            Ok(pair) => pair,
5543            Err(e) => {
5544                self.errors.push(e);
5545                return;
5546            }
5547        };
5548        let at = spec_ref.at(effective);
5549        let Some(identity) = self.identity_at(&row_repository, row_spec, &at) else {
5550            return;
5551        };
5552
5553        if discovery::same_loaded_spec(row_spec, current_spec) && !list.rows.is_empty() {
5554            self.errors.push(self.engine_error(
5555                format!(
5556                    "`{}` is a list of `{}` inside `{}` itself: it cannot have source rows, \
5557                     since every row would hold the same rows forever; rows come from run data",
5558                    data_path.data, identity.spec, identity.spec
5559                ),
5560                &source,
5561            ));
5562            return;
5563        }
5564
5565        let template = if identity == self.instance_lists.self_identity {
5566            assert!(
5567                discovery::same_loaded_spec(row_spec, self.main_spec),
5568                "BUG: self-list row identity equals the planned spec's identity but is another spec"
5569            );
5570            None
5571        } else {
5572            match self.row_template(&identity, &row_repository, row_spec, &at) {
5573                Some(template) => Some(template),
5574                None => return,
5575            }
5576        };
5577        let key = match &template {
5578            Some(template) => self.instance_list_key(list, &template.data, &identity),
5579            None => self.instance_list_key(list, &self.data, &identity),
5580        };
5581        let key = match key {
5582            Ok(key) => key,
5583            Err(errors) => {
5584                self.errors.extend(errors);
5585                return;
5586            }
5587        };
5588
5589        let store = match &template {
5590            Some(template) => {
5591                let row_identity = Arc::new(identity.clone());
5592                let rows = self.written_rows(
5593                    &ListSite {
5594                        list_path: &data_path,
5595                        identity: &row_identity,
5596                        template,
5597                        spec: current_spec,
5598                        repository: current_repository,
5599                        effective,
5600                        bindings,
5601                    },
5602                    list,
5603                );
5604                self.row_store(rows, &key, &source, template)
5605            }
5606            None => Arc::new(RowStore::unkeyed(Vec::new())),
5607        };
5608
5609        let element = Arc::new(LemmaType::primitive(TypeSpecification::Spec {
5610            spec: identity.clone(),
5611        }));
5612        let mut resolved_type = LemmaType {
5613            name: Some(format!("{} list", identity.spec)),
5614            specifications: TypeSpecification::List {
5615                element,
5616                key: key.clone(),
5617                help: String::new(),
5618            },
5619            extends: TypeExtends::Primitive,
5620            options: TypeOptions::default(),
5621        };
5622        if let ParsedDataValue::Definition {
5623            constraints: Some(constraints),
5624            ..
5625        } = &data.value
5626        {
5627            let mut declared_suggestion = None;
5628            let mut declared_value = None;
5629            match apply_constraints_to_spec(
5630                current_spec,
5631                &data_path.data,
5632                &data_path.data,
5633                &mut resolved_type,
5634                constraints,
5635                &source,
5636                ConstraintSuggestionSink {
5637                    suggestion: &mut declared_suggestion,
5638                    value: &mut declared_value,
5639                    resolve_unit: None,
5640                },
5641            ) {
5642                Ok(()) => {
5643                    if let Some(raw) = declared_value {
5644                        self.errors.extend(instance_list_option_errors(
5645                            &resolved_type,
5646                            &raw,
5647                            &source,
5648                            current_spec,
5649                        ));
5650                    }
5651                }
5652                Err(errors) => self.errors.extend(errors),
5653            }
5654        }
5655        self.reject_spec_option_rows(list);
5656        let resolved_type = Arc::new(resolved_type);
5657        self.data.insert(
5658            data_path.clone(),
5659            DataDefinition::InstanceList {
5660                resolved_type,
5661                origin: DataOrigin::Declared,
5662                source,
5663            },
5664        );
5665        let source = InstanceListSource::new(identity, key, store);
5666        self.instance_lists.sources.insert(data_path, source);
5667    }
5668
5669    fn reject_spec_option_rows(&mut self, list: &ast::InstanceListBody) {
5670        for row in &list.option_rows {
5671            let ast::InstanceListRow::Fields {
5672                source_location, ..
5673            } = row
5674            else {
5675                unreachable!("BUG: -> option rows are field rows");
5676            };
5677            self.errors
5678                .push(self.engine_error(SPEC_LIST_HAS_NO_OPTIONS.to_string(), source_location));
5679        }
5680    }
5681
5682    /// Rows of `identity` as instance-list commands read them.
5683    ///
5684    /// `None` when the row Spec is not self and was never imported (`uses`), so no
5685    /// template plan exists. Callers must report a planning Error — never panicking.
5686    fn row_template_of(&self, identity: &SpecIdentity) -> Option<RowTemplate> {
5687        self.instance_lists.row_template_of(identity)
5688    }
5689
5690    fn missing_row_template_error(&self, identity: &SpecIdentity, source: &Source) -> Error {
5691        self.engine_error(
5692            format!(
5693                "instance list of `{}` needs `uses {}` before `-> key` or `-> with`",
5694                identity.spec, identity.spec
5695            ),
5696            source,
5697        )
5698    }
5699
5700    /// Top-level main-spec rules (forward refs while converting) then present
5701    /// graph rules.
5702    fn conversion_root_rules(&self) -> BTreeSet<RulePath> {
5703        let mut rules = BTreeSet::new();
5704        for rule in &self.main_spec.rules {
5705            rules.insert(RulePath {
5706                segments: Vec::new(),
5707                rule: rule.name.clone(),
5708            });
5709        }
5710        for path in self.rules.keys() {
5711            rules.insert(path.clone());
5712        }
5713        rules
5714    }
5715
5716    /// Member `parts` on rows of `identity` read through `template`, hop by hop.
5717    fn template_row_chain(
5718        &self,
5719        identity: &SpecIdentity,
5720        template: &RowTemplate,
5721        parts: &[&str],
5722    ) -> Option<Vec<RowHop>> {
5723        match template {
5724            RowTemplate::Plan(plan) => instance_list::row_end_chain(
5725                identity,
5726                instance_list::RowEndView::Plan(plan),
5727                &self.instance_lists.templates,
5728                parts,
5729            ),
5730            RowTemplate::Root => {
5731                let rules = self.conversion_root_rules();
5732                instance_list::row_end_chain(
5733                    identity,
5734                    instance_list::RowEndView::Root {
5735                        data: &self.data,
5736                        rules: &rules,
5737                    },
5738                    &self.instance_lists.templates,
5739                    parts,
5740                )
5741            }
5742        }
5743    }
5744
5745    /// Identity of `spec`, resolved at `at`, in the slice that holds `at`.
5746    fn identity_at(
5747        &mut self,
5748        repository: &Arc<LemmaRepository>,
5749        spec: &LemmaSpec,
5750        at: &EffectiveDate,
5751    ) -> Option<SpecIdentity> {
5752        match self
5753            .interners
5754            .slices
5755            .identity_at(self.context, repository, spec, at, self.limits)
5756        {
5757            Ok(identity) => Some(identity),
5758            Err(errors) => {
5759                self.nested_errors.extend(errors);
5760                self.nested_spec_failed = true;
5761                None
5762            }
5763        }
5764    }
5765
5766    /// Template plan of `identity`, shared pass-wide and recorded on this consumer.
5767    ///
5768    /// `at` is the planning instant for this edge (`spec_ref.at(consumer_effective)`);
5769    /// `identity` is the slice that holds it ([`Self::identity_at`]).
5770    fn row_template(
5771        &mut self,
5772        identity: &SpecIdentity,
5773        row_repository: &Arc<LemmaRepository>,
5774        row_spec: &'a LemmaSpec,
5775        at: &EffectiveDate,
5776    ) -> Option<Arc<ExecutionPlan>> {
5777        if let Some(template) = self.instance_lists.templates.get(identity) {
5778            return Some(Arc::clone(template));
5779        }
5780        if let Some(template) = self.interners.spec_templates.get(identity) {
5781            self.record_template(identity.clone(), Arc::clone(&template));
5782            return Some(template);
5783        }
5784        match super::get_or_build_template(
5785            self.context,
5786            row_repository,
5787            row_spec,
5788            at,
5789            self.limits,
5790            self.interners,
5791        ) {
5792            Ok(template) => {
5793                assert_eq!(
5794                    template.identity, *identity,
5795                    "BUG: the template built at an instant is the plan of the slice that holds it"
5796                );
5797                self.record_template(identity.clone(), Arc::clone(&template));
5798                Some(template)
5799            }
5800            Err(errors) => {
5801                self.nested_errors.extend(errors);
5802                self.nested_spec_failed = true;
5803                None
5804            }
5805        }
5806    }
5807
5808    /// Record `template` under `identity` and absorb every nested row-element plan it carries.
5809    fn record_template(&mut self, identity: SpecIdentity, template: Arc<ExecutionPlan>) {
5810        for (row_identity, row_plan) in &template.row_templates {
5811            self.instance_lists
5812                .templates
5813                .entry(row_identity.clone())
5814                .or_insert_with(|| Arc::clone(row_plan));
5815        }
5816        self.instance_lists
5817            .templates
5818            .entry(identity)
5819            .or_insert(template);
5820    }
5821
5822    /// `-> key a, b`: names and row-relative data paths of the key fields. `row_data` is
5823    /// the row spec's data: a template plan's, or this plan's for a self-list.
5824    fn instance_list_key(
5825        &self,
5826        list: &ast::InstanceListBody,
5827        row_data: &IndexMap<DataPath, DataDefinition>,
5828        identity: &SpecIdentity,
5829    ) -> Result<ListKey, Vec<Error>> {
5830        let Some(declared) = &list.key else {
5831            return Ok(ListKey::Numbered);
5832        };
5833        let mut key_fields: Vec<KeyField> = Vec::with_capacity(declared.fields.len());
5834        let mut errors = Vec::new();
5835        for field in &declared.fields {
5836            if !field.segments.is_empty() {
5837                errors.push(self.engine_error(
5838                    format!(
5839                        "key field `{field}` is a dotted path; a key names data declared on `{}` itself",
5840                        identity.spec
5841                    ),
5842                    &declared.source_location,
5843                ));
5844                continue;
5845            }
5846            if key_fields
5847                .iter()
5848                .any(|key_field| key_field.name == field.name)
5849            {
5850                errors.push(self.engine_error(
5851                    format!("`{}` appears twice in `-> key`", field.name),
5852                    &declared.source_location,
5853                ));
5854                continue;
5855            }
5856            let local = DataPath::local(field.name.clone());
5857            let Some(definition) = row_data.get(&local) else {
5858                errors.push(self.engine_error(
5859                    format!(
5860                        "key field `{}` is not data on `{}`",
5861                        field.name, identity.spec
5862                    ),
5863                    &declared.source_location,
5864                ));
5865                continue;
5866            };
5867            match definition {
5868                DataDefinition::Value { .. }
5869                | DataDefinition::TypeDeclaration { .. }
5870                | DataDefinition::Reference { .. } => {}
5871                DataDefinition::Import { .. } | DataDefinition::InstanceList { .. } => {
5872                    errors.push(self.engine_error(
5873                        format!(
5874                            "key field `{}` on `{}` is not a value",
5875                            field.name, identity.spec
5876                        ),
5877                        &declared.source_location,
5878                    ));
5879                    continue;
5880                }
5881            }
5882            key_fields.push(KeyField {
5883                name: field.name.clone(),
5884                path: local,
5885            });
5886        }
5887        if errors.is_empty() {
5888            Ok(ListKey::Fields(key_fields))
5889        } else {
5890            Err(errors)
5891        }
5892    }
5893    /// Rows written under `list`. A field row binds the written fields; `-> with ada`
5894    /// is written with every binding of `uses ada`.
5895    fn written_rows<'r>(
5896        &mut self,
5897        site: &ListSite<'_, 'a>,
5898        list: &'r ast::InstanceListBody,
5899    ) -> Vec<Arc<RowInstance>> {
5900        let mut written_fields: HashMap<&'r ast::Reference, WrittenField> = HashMap::new();
5901        let list_name: Arc<str> = Arc::from(site.list_path.data.as_str());
5902        let spec_name: Arc<str> = Arc::from(site.identity.spec.as_str());
5903        let repository: Option<Arc<str>> = site.identity.repository.as_deref().map(Arc::from);
5904        let row_prefix = |number: usize| {
5905            vec![PathSegment::list_row_shared(
5906                &list_name,
5907                number,
5908                &spec_name,
5909                repository.as_ref(),
5910            )]
5911        };
5912        let mut rows = Vec::new();
5913        let mut number = 0;
5914        for row in &list.rows {
5915            match row {
5916                ast::InstanceListRow::Fields {
5917                    bindings: fields,
5918                    source_location,
5919                } => {
5920                    number += 1;
5921                    if let Some(row) = self.field_stored_row(
5922                        site,
5923                        fields,
5924                        &mut written_fields,
5925                        source_location,
5926                        row_prefix(number),
5927                    ) {
5928                        rows.push(row);
5929                    }
5930                }
5931                ast::InstanceListRow::Values {
5932                    values,
5933                    source_location,
5934                } => {
5935                    for value in values {
5936                        number += 1;
5937                        if let Some(row) =
5938                            self.named_stored_row(site, value, source_location, row_prefix(number))
5939                        {
5940                            rows.push(row);
5941                        }
5942                    }
5943                }
5944            }
5945        }
5946        rows
5947    }
5948
5949    /// What the written field `path` names on rows of `site`.
5950    fn resolve_written_field(
5951        &self,
5952        site: &ListSite<'_, 'a>,
5953        path: &ast::Reference,
5954    ) -> WrittenField {
5955        let parts: Vec<&str> = path
5956            .segments
5957            .iter()
5958            .map(String::as_str)
5959            .chain(std::iter::once(path.name.as_str()))
5960            .collect();
5961        let Some(chain) = instance_list::row_end_chain(
5962            site.identity,
5963            instance_list::RowEndView::Plan(site.template),
5964            &self.instance_lists.templates,
5965            &parts,
5966        ) else {
5967            return WrittenField::NotData;
5968        };
5969        let last = chain.last().expect("BUG: a row end chain has a last hop");
5970        let RowEnd::Data(data_path) = &last.end else {
5971            return WrittenField::NotData;
5972        };
5973        let template = if chain.len() == 1 {
5974            Arc::clone(site.template)
5975        } else {
5976            self.template_of(&last.identity)
5977        };
5978        match template
5979            .data
5980            .get(data_path)
5981            .expect("BUG: row end chain data is template data")
5982        {
5983            DataDefinition::Import { row, .. } => WrittenField::Import {
5984                identity: Arc::clone(&row.identity),
5985            },
5986            DataDefinition::InstanceList { .. } => WrittenField::List,
5987            DataDefinition::Value { .. }
5988            | DataDefinition::TypeDeclaration { .. }
5989            | DataDefinition::Reference { .. } => WrittenField::Value {
5990                key: parts.iter().map(|part| (*part).to_string()).collect(),
5991            },
5992        }
5993    }
5994
5995    /// `-> with name: "Ada", salary: s`: a row whose written fields are bound. A
5996    /// dotted field (`item.price`) writes a field of the row spec's `uses item`.
5997    /// `written_fields` holds the fields earlier rows of the list resolved.
5998    fn field_stored_row<'r>(
5999        &mut self,
6000        site: &ListSite<'_, 'a>,
6001        fields: &'r [ast::RowBinding],
6002        written_fields: &mut HashMap<&'r ast::Reference, WrittenField>,
6003        source: &Source,
6004        prefix: Vec<PathSegment>,
6005    ) -> Option<Arc<RowInstance>> {
6006        let mut bindings: DataBindings = IndexMap::new();
6007        let mut failed = false;
6008        for binding in fields {
6009            let field = match written_fields.get(&binding.path) {
6010                Some(field) => field,
6011                None => {
6012                    let resolved = self.resolve_written_field(site, &binding.path);
6013                    written_fields.entry(&binding.path).or_insert(resolved)
6014                }
6015            };
6016            let key = match field {
6017                WrittenField::Value { key } => key,
6018                WrittenField::NotData => {
6019                    self.errors.push(self.engine_error(
6020                        format!(
6021                            "row field `{}` is not data on `{}`",
6022                            binding.path, site.identity.spec
6023                        ),
6024                        &binding.source_location,
6025                    ));
6026                    failed = true;
6027                    continue;
6028                }
6029                WrittenField::Import { identity } => {
6030                    let name = &binding.path;
6031                    self.errors.push(self.engine_error(
6032                        format!(
6033                            "row field `{name}` is a `uses` instance of `{}`; bind its fields with `{name}.<field>`",
6034                            identity.spec
6035                        ),
6036                        &binding.source_location,
6037                    ));
6038                    failed = true;
6039                    continue;
6040                }
6041                WrittenField::List => {
6042                    self.errors.push(self.engine_error(
6043                        format!(
6044                            "row field `{}` is a list on `{}`; a row binds values, not lists",
6045                            binding.path, site.identity.spec
6046                        ),
6047                        &binding.source_location,
6048                    ));
6049                    failed = true;
6050                    continue;
6051                }
6052            };
6053            if bindings.contains_key(key) {
6054                self.errors.push(self.engine_error(
6055                    format!("row field `{}` appears twice in one row", binding.path),
6056                    &binding.source_location,
6057                ));
6058                failed = true;
6059                continue;
6060            }
6061            let key = key.clone();
6062            let value = match &binding.rhs {
6063                WithRhs::Literal(value) => BindingValue::Literal(value.clone()),
6064                WithRhs::Reference { target } => match self.resolve_reference_target_in_spec(
6065                    target,
6066                    &binding.source_location,
6067                    site.spec,
6068                    site.repository,
6069                    site.effective,
6070                ) {
6071                    Some(target) => BindingValue::Reference { target },
6072                    None => {
6073                        failed = true;
6074                        continue;
6075                    }
6076                },
6077            };
6078            bindings.insert(key, (value, binding.source_location.clone()));
6079        }
6080        if failed {
6081            return None;
6082        }
6083        Some(self.binding_row(RowWrite {
6084            identity: Arc::clone(site.identity),
6085            template: Arc::clone(site.template),
6086            origin: RowOrigin::Written {
6087                instance: SpecInstance {
6088                    prefix: prefix.clone(),
6089                },
6090                source: source.clone(),
6091            },
6092            base: vec![None; site.template.data.len()],
6093            bindings,
6094            prefix,
6095            hops: 0,
6096        }))
6097    }
6098
6099    /// `-> with ada`: a row written with every binding of `uses ada`.
6100    fn named_stored_row(
6101        &mut self,
6102        site: &ListSite<'_, 'a>,
6103        value: &WithRhs,
6104        source: &Source,
6105        prefix: Vec<PathSegment>,
6106    ) -> Option<Arc<RowInstance>> {
6107        let list = &site.list_path.data;
6108        let target = match value {
6109            WithRhs::Reference { target } if target.segments.is_empty() => target,
6110            WithRhs::Reference { target } => {
6111                self.errors.push(self.engine_error(
6112                    format!(
6113                        "row `{target}` of `{list}` must name a `uses` instance of `{}` declared in this spec",
6114                        site.identity.spec
6115                    ),
6116                    source,
6117                ));
6118                return None;
6119            }
6120            WithRhs::Literal(literal) => {
6121                self.errors.push(self.engine_error(
6122                    format!(
6123                        "row `{literal}` of `{list}` is not a `{}` instance; write `-> with <field>: <value>` for an inline row",
6124                        site.identity.spec
6125                    ),
6126                    source,
6127                ));
6128                return None;
6129            }
6130        };
6131        let instance_identity = match self.data.get(&DataPath::local(target.name.clone())) {
6132            Some(DataDefinition::Import { row, .. }) => Arc::clone(&row.identity),
6133            Some(_) | None => {
6134                self.errors.push(self.engine_error(
6135                    format!(
6136                        "row `{target}` of `{list}` is not a `uses` instance of `{}`",
6137                        site.identity.spec
6138                    ),
6139                    source,
6140                ));
6141                return None;
6142            }
6143        };
6144        if instance_identity != *site.identity {
6145            self.errors.push(self.engine_error(
6146                format!(
6147                    "`{}` is a `{}` instance; rows of `{list}` are `{}`",
6148                    target.name, instance_identity, site.identity
6149                ),
6150                source,
6151            ));
6152            return None;
6153        }
6154        Some(self.binding_row(RowWrite {
6155            identity: instance_identity,
6156            template: Arc::clone(site.template),
6157            origin: RowOrigin::Written {
6158                instance: SpecInstance {
6159                    prefix: prefix.clone(),
6160                },
6161                source: source.clone(),
6162            },
6163            base: vec![None; site.template.data.len()],
6164            bindings: bindings_under(site.bindings, &target.name),
6165            prefix,
6166            hops: 0,
6167        }))
6168    }
6169
6170    /// Row spec identity and template plan of the list at `path` in `template`.
6171    fn nested_row_template(
6172        &self,
6173        template: &ExecutionPlan,
6174        path: &DataPath,
6175    ) -> (Arc<SpecIdentity>, Arc<ExecutionPlan>) {
6176        let source = template
6177            .instance_lists
6178            .get(path)
6179            .unwrap_or_else(|| panic!("BUG: list binding '{path}' is not a list on the row spec"));
6180        let plan = match self.row_template_of(&source.identity) {
6181            Some(RowTemplate::Plan(plan)) => plan,
6182            Some(RowTemplate::Root) => {
6183                panic!(
6184                    "BUG: copied rows of the root spec need an import cycle, rejected at discovery"
6185                )
6186            }
6187            None => panic!(
6188                "BUG: row template of '{}' was planned before its consumers",
6189                source.identity.spec
6190            ),
6191        };
6192        (Arc::new(source.identity.clone()), plan)
6193    }
6194
6195    /// Store of `rows`. Key values of a keyed list are written values and unique;
6196    /// a list whose keys fail that has an error and an empty index.
6197    fn row_store(
6198        &mut self,
6199        rows: Vec<Arc<RowInstance>>,
6200        key: &ListKey,
6201        source: &Source,
6202        template: &ExecutionPlan,
6203    ) -> Arc<RowStore> {
6204        let Some(store_key) = self.row_store_key(&rows, key, source, template) else {
6205            return Arc::new(RowStore::unkeyed(rows));
6206        };
6207        match RowStore::keyed(rows, store_key) {
6208            Ok(store) => Arc::new(store),
6209            Err((message, unkeyed)) => {
6210                self.errors.push(self.engine_error(message, source));
6211                Arc::new(unkeyed)
6212            }
6213        }
6214    }
6215
6216    /// Key fields of the keyed list `rows` are written in, when every row writes a value
6217    /// for each (or the template has a default). `None` for an unkeyed list, and with an
6218    /// error when a row does not write one.
6219    fn row_store_key(
6220        &mut self,
6221        rows: &[Arc<RowInstance>],
6222        key: &ListKey,
6223        source: &Source,
6224        template: &ExecutionPlan,
6225    ) -> Option<Vec<StoreKeyField>> {
6226        let ListKey::Fields(key_fields) = key else {
6227            return None;
6228        };
6229        let store_key: Vec<StoreKeyField> = key_fields
6230            .iter()
6231            .map(|key_field| StoreKeyField {
6232                name: key_field.name.clone(),
6233                slot: template
6234                    .data
6235                    .get_index_of(&key_field.path)
6236                    .expect("BUG: key field is template data"),
6237                field_type: instance_list::row_end_type(
6238                    template,
6239                    &RowEnd::Data(key_field.path.clone()),
6240                ),
6241                default: instance_list::default_of(&template.data, &key_field.path),
6242            })
6243            .collect();
6244        for (index, row) in rows.iter().enumerate() {
6245            for field in &store_key {
6246                let name = &field.name;
6247                match &row.bindings[field.slot] {
6248                    Some(RowBinding::Bound(OperationResult::Value(_))) => {}
6249                    Some(RowBinding::Bound(OperationResult::Veto(_))) => {
6250                        unreachable!("BUG: stored row literal binding is never a Veto")
6251                    }
6252                    Some(RowBinding::Rows { .. } | RowBinding::Instance(_)) => {
6253                        unreachable!("BUG: key fields are values, never lists or instances")
6254                    }
6255                    Some(RowBinding::Deferred { target, .. }) => {
6256                        let target = match target {
6257                            ReferenceTarget::Data(path) => path.input_key(),
6258                            ReferenceTarget::Rule(path) => path.input_key(),
6259                        };
6260                        self.errors.push(self.engine_error(
6261                            format!(
6262                                "key field `{name}` of row {} is `{target}`; key values must be written as values",
6263                                index + 1
6264                            ),
6265                            source,
6266                        ));
6267                        return None;
6268                    }
6269                    None if field.default.is_some() => {}
6270                    None => {
6271                        self.errors.push(self.engine_error(
6272                            format!("row {} has no value for key field `{name}`", index + 1),
6273                            source,
6274                        ));
6275                        return None;
6276                    }
6277                }
6278            }
6279        }
6280        Some(store_key)
6281    }
6282
6283    /// Inserts a literal-value data definition using the given literal.
6284    /// Shared between the literal path of `add_data` and the literal path of
6285    /// a binding-provided value (bindings can only be literals or references).
6286    fn insert_literal_data(
6287        &mut self,
6288        data_path: DataPath,
6289        value: &ast::Value,
6290        declared_schema_type: Option<Arc<LemmaType>>,
6291        effective_source: Source,
6292        current_spec: &LemmaSpec,
6293        origin: DataOrigin,
6294    ) {
6295        let Some((semantic_value, lemma_type)) =
6296            self.literal_to_typed(value, declared_schema_type, &effective_source, current_spec)
6297        else {
6298            return;
6299        };
6300        let literal_value = LiteralValue {
6301            value: semantic_value,
6302        };
6303        self.data.insert(
6304            data_path,
6305            DataDefinition::Value {
6306                value: literal_value,
6307                resolved_type: lemma_type,
6308                origin,
6309                source: effective_source,
6310            },
6311        );
6312    }
6313
6314    /// Converts an AST literal to its semantic value and resolved type, against
6315    /// `declared_schema_type` when the slot declares one. Errors are recorded; `None`
6316    /// means the literal did not convert.
6317    fn literal_to_typed(
6318        &mut self,
6319        value: &ast::Value,
6320        declared_schema_type: Option<Arc<LemmaType>>,
6321        effective_source: &Source,
6322        current_spec: &LemmaSpec,
6323    ) -> Option<(ValueKind, Arc<LemmaType>)> {
6324        let semantic_value = if let Some(ref schema) = declared_schema_type {
6325            match value {
6326                Value::Range(left, right)
6327                    if matches!(
6328                        &schema.specifications,
6329                        TypeSpecification::MeasureRange { .. }
6330                            | TypeSpecification::RatioRange { .. }
6331                    ) =>
6332                {
6333                    let element = schema.range_element_type();
6334                    let resolve = unit_tokens_for_spec(&self.local_types, current_spec);
6335                    let left_lit = match lift_range_endpoint(left, element.as_ref(), Some(&resolve))
6336                    {
6337                        Ok(lit) => lit,
6338                        Err(e) => {
6339                            self.errors.push(self.engine_error(e, effective_source));
6340                            return None;
6341                        }
6342                    };
6343                    let right_lit =
6344                        match lift_range_endpoint(right, element.as_ref(), Some(&resolve)) {
6345                            Ok(lit) => lit,
6346                            Err(e) => {
6347                                self.errors.push(self.engine_error(e, effective_source));
6348                                return None;
6349                            }
6350                        };
6351                    ValueKind::Range(Box::new(left_lit), Box::new(right_lit))
6352                }
6353                _ => {
6354                    let resolve = unit_tokens_for_spec(&self.local_types, current_spec);
6355                    match parser_value_to_value_kind(value, schema.as_ref(), Some(&resolve)) {
6356                        Ok(s) => s,
6357                        Err(e) => {
6358                            self.errors.push(self.engine_error(e, effective_source));
6359                            return None;
6360                        }
6361                    }
6362                }
6363            }
6364        } else {
6365            match value {
6366                Value::NumberWithUnit(magnitude, unit) => {
6367                    let (bare, lt) = match self.resolve_unit_ref(current_spec, unit) {
6368                        Ok(resolved) => resolved,
6369                        Err(message) => {
6370                            self.errors
6371                                .push(self.engine_error(message, effective_source));
6372                            return None;
6373                        }
6374                    };
6375                    match number_with_unit_to_value_kind(*magnitude, &bare, lt.as_ref()) {
6376                        Ok(s) => s,
6377                        Err(e) => {
6378                            self.errors.push(self.engine_error(e, effective_source));
6379                            return None;
6380                        }
6381                    }
6382                }
6383                Value::Range(left, right) => match (left.as_ref(), right.as_ref()) {
6384                    (
6385                        Value::NumberWithUnit(left_mag, left_unit),
6386                        Value::NumberWithUnit(right_mag, right_unit),
6387                    ) => {
6388                        match self.measure_ratio_range_from_number_with_unit_endpoints(
6389                            current_spec,
6390                            *left_mag,
6391                            left_unit,
6392                            *right_mag,
6393                            right_unit,
6394                        ) {
6395                            Ok(s) => s,
6396                            Err(e) => {
6397                                self.errors.push(self.engine_error(e, effective_source));
6398                                return None;
6399                            }
6400                        }
6401                    }
6402                    _ => match value_to_semantic(value) {
6403                        Ok(s) => s,
6404                        Err(e) => {
6405                            self.errors.push(self.engine_error(e, effective_source));
6406                            return None;
6407                        }
6408                    },
6409                },
6410                _ => match value_to_semantic(value) {
6411                    Ok(s) => s,
6412                    Err(e) => {
6413                        self.errors.push(self.engine_error(e, effective_source));
6414                        return None;
6415                    }
6416                },
6417            }
6418        };
6419        let schema_type: Arc<LemmaType> = match declared_schema_type {
6420            Some(schema) => schema,
6421            None => match value {
6422                Value::Text(_) => primitive_text_arc().clone(),
6423                Value::Number(_) => primitive_number_arc().clone(),
6424                Value::NumberWithUnit(_, unit) => match self.resolve_unit_ref(current_spec, unit) {
6425                    Ok((_, lt)) => lt,
6426                    Err(message) => {
6427                        self.errors
6428                            .push(self.engine_error(message, effective_source));
6429                        return None;
6430                    }
6431                },
6432                Value::Boolean(_) => primitive_boolean_arc().clone(),
6433                Value::Date(_) => primitive_date_arc().clone(),
6434                Value::Time(_) => primitive_time_arc().clone(),
6435                Value::Range(left_v, right_v) => match (left_v.as_ref(), right_v.as_ref()) {
6436                    (Value::NumberWithUnit(_, _), Value::NumberWithUnit(_, _)) => {
6437                        match &semantic_value {
6438                            ValueKind::Range(left, right) => {
6439                                let specs = range_type_specification_from_endpoints(
6440                                    left.lemma_type.as_ref(),
6441                                    right.lemma_type.as_ref(),
6442                                )
6443                                .unwrap_or_else(|| {
6444                                    unreachable!(
6445                                        "BUG: measure/ratio range endpoints must form a range type after resolve"
6446                                    )
6447                                });
6448                                Arc::new(LemmaType::primitive(specs))
6449                            }
6450                            _ => unreachable!(
6451                            "BUG: semantic range literal conversion returned non-range value kind"
6452                        ),
6453                        }
6454                    }
6455                    _ => match &semantic_value {
6456                        ValueKind::Range(left, right) => {
6457                            let endpoint_type = |endpoint: &TypedLiteral| -> Arc<LemmaType> {
6458                                match &endpoint.value {
6459                                    ValueKind::Number(_) => primitive_number_arc().clone(),
6460                                    ValueKind::Date(_) => primitive_date_arc().clone(),
6461                                    ValueKind::Time(_) => primitive_time_arc().clone(),
6462                                    ValueKind::Ratio(_) => primitive_ratio_arc().clone(),
6463                                    ValueKind::Measure(_) => Arc::clone(&endpoint.lemma_type),
6464                                    other => unreachable!(
6465                                        "BUG: untyped range endpoint kind for insert_literal_data: {other:?}"
6466                                    ),
6467                                }
6468                            };
6469                            let specs = range_type_specification_from_endpoints(
6470                                endpoint_type(left).as_ref(),
6471                                endpoint_type(right).as_ref(),
6472                            )
6473                            .unwrap_or_else(|| {
6474                                unreachable!(
6475                                    "BUG: attempted to construct a range literal from incompatible endpoint types"
6476                                )
6477                            });
6478                            Arc::new(LemmaType::primitive(specs))
6479                        }
6480                        _ => unreachable!(
6481                            "BUG: semantic range literal conversion returned non-range value kind"
6482                        ),
6483                    },
6484                },
6485            },
6486        };
6487        let lemma_type = match (value, &semantic_value) {
6488            (Value::NumberWithUnit(_, unit), ValueKind::Measure(_) | ValueKind::Ratio(_)) => {
6489                let bare = match self.resolve_unit_ref(current_spec, unit) {
6490                    Ok((bare, _)) => bare,
6491                    Err(message) => {
6492                        self.errors
6493                            .push(self.engine_error(message, effective_source));
6494                        return None;
6495                    }
6496                };
6497                Arc::new(schema_type.as_ref().clone().with_unit(bare))
6498            }
6499            _ => schema_type,
6500        };
6501        Some((semantic_value, lemma_type))
6502    }
6503
6504    /// Returns (path_segments, last_resolved_spec) on success.
6505    fn resolve_path_segments(
6506        &mut self,
6507        segments: &[String],
6508        reference_source: &Source,
6509        mut path_segments: Vec<PathSegment>,
6510        mut spec_context: &'a LemmaSpec,
6511        spec_context_repository: &Arc<LemmaRepository>,
6512        effective: &EffectiveDate,
6513    ) -> Option<(Vec<PathSegment>, &'a LemmaSpec)> {
6514        let mut last_arc: Option<&LemmaSpec> = None;
6515        let mut walk_effective = effective.clone();
6516        let mut context_repository = Arc::clone(spec_context_repository);
6517
6518        for segment in segments.iter() {
6519            let Some(data_ref) = spec_context
6520                .data
6521                .iter()
6522                .find(|data| data.reference.is_local() && data.reference.name == *segment)
6523            else {
6524                self.errors.push(
6525                    self.engine_error(format!("Data '{}' not found", segment), reference_source),
6526                );
6527                return None;
6528            };
6529
6530            if let ParsedDataValue::Import {
6531                spec_ref,
6532                effective_ref: Some(_),
6533                ..
6534            } = &data_ref.value
6535            {
6536                // Dynamic pin: path segments name the dependency, not a fixed version.
6537                // Interface validation already requires every candidate share members;
6538                // walk the first candidate for typing.
6539                let targets = match self.dynamic_import_targets(
6540                    spec_ref,
6541                    spec_context,
6542                    &context_repository,
6543                    reference_source,
6544                ) {
6545                    Ok(targets) => targets,
6546                    Err(e) => {
6547                        self.errors.push(e);
6548                        return None;
6549                    }
6550                };
6551                let first = targets
6552                    .into_iter()
6553                    .next()
6554                    .expect("BUG: dynamic_import_targets returns at least one target");
6555                walk_effective = first.at;
6556                path_segments.push(PathSegment {
6557                    uses: Arc::from(segment.as_str()),
6558                    row: None,
6559                    repository: first.repository.name.as_deref().map(Arc::from),
6560                    spec: Arc::from(first.spec.name.as_str()),
6561                });
6562                last_arc = Some(first.spec);
6563                spec_context = first.spec;
6564                context_repository = first.repository;
6565            } else if let ParsedDataValue::Import { spec_ref, .. } = &data_ref.value {
6566                let (repo, arc) = match self.resolve_spec_ref(
6567                    spec_ref,
6568                    &walk_effective,
6569                    spec_context,
6570                    &context_repository,
6571                ) {
6572                    Ok(pair) => pair,
6573                    Err(e) => {
6574                        self.errors.push(e);
6575                        return None;
6576                    }
6577                };
6578                walk_effective = spec_ref.at(&walk_effective);
6579                path_segments.push(PathSegment {
6580                    uses: Arc::from(segment.as_str()),
6581                    row: None,
6582                    repository: repo.name.as_deref().map(Arc::from),
6583                    spec: Arc::from(arc.name.as_str()),
6584                });
6585                last_arc = Some(arc);
6586                spec_context = arc;
6587                context_repository = repo;
6588            } else if matches!(
6589                &data_ref.value,
6590                ParsedDataValue::Definition { list: Some(_), .. }
6591            ) {
6592                self.errors.push(self.engine_error(
6593                    format!(
6594                        "`{segment}` is a list: there is no `{segment}.<field>` outside a command argument; \
6595                         pick a row first, for example `({segment} -> first).<field>`"
6596                    ),
6597                    reference_source,
6598                ));
6599                return None;
6600            } else {
6601                self.errors.push(self.engine_error(
6602                    format!("Data '{}' is not a spec reference", segment),
6603                    reference_source,
6604                ));
6605                return None;
6606            }
6607        }
6608
6609        let final_arc = last_arc.unwrap_or_else(|| {
6610            unreachable!(
6611                "BUG: resolve_path_segments called with empty segments should not reach here"
6612            )
6613        });
6614        Some((path_segments, final_arc))
6615    }
6616
6617    /// Ensure `spec` and every qualified-import target spec (transitively) are resolved into
6618    /// [`Self::local_types`] before the consumer reads cross-spec parent types.
6619    fn ensure_spec_types_resolved(
6620        &mut self,
6621        spec: &'a LemmaSpec,
6622        spec_repository: &Arc<LemmaRepository>,
6623        effective: &EffectiveDate,
6624        type_resolver: &TypeResolver<'a>,
6625    ) -> bool {
6626        let root = match self.enter_spec_types(spec, spec_repository, effective, type_resolver) {
6627            Ok(frame) => frame,
6628            Err(resolved) => return resolved,
6629        };
6630        let mut stack = vec![root];
6631        loop {
6632            let mut frame = stack
6633                .pop()
6634                .expect("BUG: spec types walk ends when its root frame finishes");
6635            if let Some((child, child_repository, child_effective)) =
6636                self.next_spec_import(&mut frame)
6637            {
6638                stack.push(frame);
6639                if let Ok(child_frame) =
6640                    self.enter_spec_types(child, &child_repository, &child_effective, type_resolver)
6641                {
6642                    stack.push(child_frame);
6643                }
6644                continue;
6645            }
6646            let resolved = self.finish_spec_types(&frame, type_resolver);
6647            if stack.is_empty() {
6648                return resolved;
6649            }
6650        }
6651    }
6652
6653    /// Entry of [`Self::ensure_spec_types_resolved`]: `Err` carries the result when there
6654    /// is nothing to walk.
6655    fn enter_spec_types(
6656        &self,
6657        spec: &'a LemmaSpec,
6658        spec_repository: &Arc<LemmaRepository>,
6659        effective: &EffectiveDate,
6660        type_resolver: &TypeResolver<'a>,
6661    ) -> Result<SpecTypesFrame<'a>, bool> {
6662        if self
6663            .local_types
6664            .iter()
6665            .any(|(_, s, _)| discovery::same_loaded_spec(s, spec))
6666        {
6667            return Err(true);
6668        }
6669        if !type_resolver.is_registered(spec) {
6670            panic!(
6671                "BUG: spec '{}' reachable from spec '{}' was not registered during dependency discovery",
6672                spec.name, self.main_spec.name
6673            );
6674        }
6675        if type_resolver.registration_failed(spec) {
6676            // The registration errors were already collected by
6677            // `register_all`; the spec cannot be built without its types.
6678            return Err(false);
6679        }
6680        Ok(SpecTypesFrame {
6681            spec,
6682            repository: Arc::clone(spec_repository),
6683            effective: effective.clone(),
6684            next_data: 0,
6685            pending_targets: VecDeque::new(),
6686        })
6687    }
6688
6689    /// Next import of `frame`'s spec that must be entered. Import errors are recorded and
6690    /// skipped.
6691    fn next_spec_import(
6692        &mut self,
6693        frame: &mut SpecTypesFrame<'a>,
6694    ) -> Option<(&'a LemmaSpec, Arc<LemmaRepository>, EffectiveDate)> {
6695        if let Some(dependency) = frame.pending_targets.pop_front() {
6696            return Some((dependency.spec, dependency.repository, dependency.at));
6697        }
6698        let spec = frame.spec;
6699        while let Some(data) = spec.data.get(frame.next_data) {
6700            frame.next_data += 1;
6701            if discovery::is_self_list(
6702                self.context,
6703                data,
6704                spec,
6705                &frame.repository,
6706                &frame.effective,
6707            ) {
6708                continue;
6709            }
6710            match &data.value {
6711                ParsedDataValue::ImportList { .. } => {
6712                    panic!("BUG: uses list is rejected at parse and must not reach planning");
6713                }
6714                ParsedDataValue::Import {
6715                    spec_ref,
6716                    effective_ref: Some(_),
6717                    ..
6718                } => {
6719                    match self.dynamic_import_targets(
6720                        spec_ref,
6721                        spec,
6722                        &frame.repository,
6723                        &data.source_location,
6724                    ) {
6725                        Ok(targets) => {
6726                            frame.pending_targets.extend(targets);
6727                            if let Some(dependency) = frame.pending_targets.pop_front() {
6728                                return Some((
6729                                    dependency.spec,
6730                                    dependency.repository,
6731                                    dependency.at,
6732                                ));
6733                            }
6734                        }
6735                        Err(error) => self.errors.push(error),
6736                    }
6737                }
6738                ParsedDataValue::Import { spec_ref, .. } => {
6739                    let import_effective = spec_ref.at(&frame.effective);
6740                    match self.resolve_spec_ref(
6741                        spec_ref,
6742                        &import_effective,
6743                        spec,
6744                        &frame.repository,
6745                    ) {
6746                        Ok((source_repo, source_arc)) => {
6747                            return Some((source_arc, source_repo, import_effective));
6748                        }
6749                        Err(error) => self.errors.push(error),
6750                    }
6751                }
6752                ParsedDataValue::Definition { .. } => {
6753                    let Some(spec_ref) = discovery::data_instance_list_spec_ref(spec, data) else {
6754                        continue;
6755                    };
6756                    let import_effective = spec_ref.at(&frame.effective);
6757                    match self.resolve_spec_ref(
6758                        &spec_ref,
6759                        &import_effective,
6760                        spec,
6761                        &frame.repository,
6762                    ) {
6763                        Ok((source_repo, source_arc)) => {
6764                            return Some((source_arc, source_repo, import_effective));
6765                        }
6766                        Err(error) => {
6767                            // Instance-list body (`Definition.list`) names a missing Spec:
6768                            // surface the resolve Error. Local-type value lists (`money list`
6769                            // with no instance body) fall through without Error here.
6770                            if matches!(
6771                                &data.value,
6772                                ParsedDataValue::Definition { list: Some(_), .. }
6773                            ) {
6774                                self.errors.push(error);
6775                            }
6776                        }
6777                    }
6778                }
6779            }
6780        }
6781        None
6782    }
6783
6784    /// Resolves and records the types of `frame`'s spec once all its imports are recorded.
6785    fn finish_spec_types(
6786        &mut self,
6787        frame: &SpecTypesFrame<'a>,
6788        type_resolver: &TypeResolver<'a>,
6789    ) -> bool {
6790        match type_resolver.resolve_and_validate(frame.spec, &frame.effective, &self.local_types) {
6791            Ok(resolved_types) => {
6792                self.local_types
6793                    .push((Arc::clone(&frame.repository), frame.spec, resolved_types));
6794                true
6795            }
6796            Err(errors) => {
6797                self.errors.extend(errors);
6798                false
6799            }
6800        }
6801    }
6802
6803    /// Plan `spec` as this graph: its data, its `uses` imports, its lists' rows and its
6804    /// rules. An import is a row over its spec's template plan, planned on its own;
6805    /// nothing of the imported spec is planned here.
6806    fn build_spec(
6807        &mut self,
6808        spec: &'a LemmaSpec,
6809        spec_repository: &Arc<LemmaRepository>,
6810        effective: &EffectiveDate,
6811        type_resolver: &TypeResolver<'a>,
6812    ) {
6813        self.process_meta_fields(spec);
6814        let bindings = match self.build_data_bindings(spec, spec_repository, effective) {
6815            Ok(bindings) => bindings,
6816            Err(errors) => {
6817                self.errors.extend(errors);
6818                IndexMap::new()
6819            }
6820        };
6821        if !self.ensure_spec_types_resolved(spec, spec_repository, effective, type_resolver) {
6822            return;
6823        }
6824
6825        let mut imports: HashMap<String, PlannedImport> = HashMap::new();
6826        for data in &spec.data {
6827            if !data.reference.is_local() || !matches!(data.value, ParsedDataValue::Import { .. }) {
6828                continue;
6829            }
6830            if let Some(import) = self.plan_import(data, spec, spec_repository, effective) {
6831                imports.entry(data.reference.name.clone()).or_insert(import);
6832            }
6833        }
6834        let bindings = self.resolve_instance_alias_bindings(bindings, spec, &imports);
6835
6836        for data in &spec.data {
6837            if !data.reference.is_local()
6838                || self.is_data_instance_list(data, spec, spec_repository, effective)
6839            {
6840                continue;
6841            }
6842            if matches!(data.value, ParsedDataValue::Import { .. }) {
6843                if let Some(planned) = imports.get(&data.reference.name) {
6844                    self.add_import(data, planned, &bindings, spec);
6845                }
6846                continue;
6847            }
6848            self.add_data(data, spec, spec_repository, effective);
6849        }
6850        for data in &spec.data {
6851            if data.reference.is_local()
6852                && self.is_data_instance_list(data, spec, spec_repository, effective)
6853            {
6854                self.plan_instance_list(data, spec, spec_repository, effective, &bindings);
6855            }
6856        }
6857
6858        let data_map: HashMap<String, &LemmaData> = spec
6859            .data
6860            .iter()
6861            .map(|data| (data.reference.name.clone(), data))
6862            .collect();
6863        let rule_names: HashSet<&str> = spec.rules.iter().map(|r| r.name.as_str()).collect();
6864        for rule in &spec.rules {
6865            self.add_rule(
6866                rule,
6867                spec,
6868                spec_repository,
6869                &data_map,
6870                &rule_names,
6871                effective,
6872            );
6873        }
6874    }
6875
6876    /// Every temporal body a `uses … -> effective` import can select, without requiring
6877    /// the dependency to be active at the consumer's planning instant.
6878    fn dynamic_import_targets(
6879        &mut self,
6880        spec_ref: &ast::SpecRef,
6881        consumer: &LemmaSpec,
6882        consumer_repository: &Arc<LemmaRepository>,
6883        source: &Source,
6884    ) -> Result<Vec<discovery::ResolvedDependencyEdge<'a>>, Error> {
6885        let resolved = discovery::resolve_spec_ref_repository(
6886            self.context,
6887            spec_ref,
6888            Some(source),
6889            consumer.name.as_str(),
6890            Some(consumer),
6891        )?;
6892        let repository = match &resolved.repository {
6893            Some(explicit) => Arc::clone(explicit),
6894            None => Arc::clone(consumer_repository),
6895        };
6896        let Some(dep_set) = self.context.spec_set(&repository, &spec_ref.name) else {
6897            return Err(self.engine_error(
6898                format!(
6899                    "dependency '{}' does not exist",
6900                    discovery::format_dep_qualified_name(&repository, &spec_ref.name)
6901                ),
6902                source,
6903            ));
6904        };
6905        let mut targets = Vec::new();
6906        for version in dep_set.iter_specs() {
6907            let version_slices =
6908                match self
6909                    .interners
6910                    .slices
6911                    .of(self.context, &repository, version, self.limits)
6912                {
6913                    Ok(slices) => slices,
6914                    Err(errors) => {
6915                        self.errors.extend(errors);
6916                        return Err(self.engine_error(
6917                            format!(
6918                                "failed to plan temporal slices of '{}'",
6919                                discovery::format_dep_qualified_name(&repository, &spec_ref.name)
6920                            ),
6921                            source,
6922                        ));
6923                    }
6924                };
6925            for start in version_slices.starts() {
6926                if discovery::same_loaded_spec(version, consumer) {
6927                    continue;
6928                }
6929                targets.push(discovery::ResolvedDependencyEdge {
6930                    repository: Arc::clone(&repository),
6931                    spec: version,
6932                    at: start.clone(),
6933                });
6934            }
6935        }
6936        if targets.is_empty() {
6937            return Err(self.engine_error(
6938                format!(
6939                    "'{}' depends on '{}' via `-> effective`, but that dependency has no temporal versions",
6940                    consumer.name,
6941                    discovery::format_dep_qualified_name(&repository, &spec_ref.name)
6942                ),
6943                source,
6944            ));
6945        }
6946        Ok(targets)
6947    }
6948
6949    /// Plan one `uses` import: a single body, or every candidate body for `-> effective`.
6950    fn plan_import(
6951        &mut self,
6952        data: &LemmaData,
6953        spec: &'a LemmaSpec,
6954        spec_repository: &Arc<LemmaRepository>,
6955        effective: &EffectiveDate,
6956    ) -> Option<PlannedImport> {
6957        let ParsedDataValue::Import {
6958            spec_ref,
6959            effective_ref,
6960            ..
6961        } = &data.value
6962        else {
6963            unreachable!("BUG: plan_import on non-import data");
6964        };
6965        let Some(pin_ref) = effective_ref else {
6966            let (repository, imported) =
6967                match self.resolve_spec_ref(spec_ref, effective, spec, spec_repository) {
6968                    Ok(pair) => pair,
6969                    Err(error) => {
6970                        self.errors.push(error);
6971                        return None;
6972                    }
6973                };
6974            let at = spec_ref.at(effective);
6975            let identity = self.identity_at(&repository, imported, &at)?;
6976            let template = self.row_template(&identity, &repository, imported, &at)?;
6977            return Some(PlannedImport::Static {
6978                identity: Arc::new(identity),
6979                template,
6980            });
6981        };
6982
6983        if !pin_ref.segments.is_empty() {
6984            self.errors.push(self.engine_error(
6985                "`-> effective` names a local data or rule of this spec, not a path through an import"
6986                    .to_string(),
6987                &data.source_location,
6988            ));
6989            return None;
6990        }
6991        if pin_ref.name == data.reference.name {
6992            self.errors.push(self.engine_error(
6993                format!(
6994                    "`-> effective {}` cannot name the import alias '{}'",
6995                    pin_ref.name, data.reference.name
6996                ),
6997                &data.source_location,
6998            ));
6999            return None;
7000        }
7001
7002        let pin = match self.resolve_effective_pin(pin_ref, spec, &data.source_location) {
7003            Ok(pin) => pin,
7004            Err(error) => {
7005                self.errors.push(error);
7006                return None;
7007            }
7008        };
7009
7010        let targets = match self.dynamic_import_targets(
7011            spec_ref,
7012            spec,
7013            spec_repository,
7014            &data.source_location,
7015        ) {
7016            Ok(targets) => targets,
7017            Err(error) => {
7018                self.errors.push(error);
7019                return None;
7020            }
7021        };
7022
7023        let mut candidates = Vec::new();
7024        for target in targets {
7025            let identity = self.identity_at(&target.repository, target.spec, &target.at)?;
7026            let template =
7027                self.row_template(&identity, &target.repository, target.spec, &target.at)?;
7028            candidates.push((Arc::new(identity), template));
7029        }
7030        Some(PlannedImport::Dynamic { pin, candidates })
7031    }
7032
7033    /// Resolve `-> effective <ref>` to a date/datetime data or rule of `spec`.
7034    /// Type is checked in [`Self::validate_effective_pin_type`] / after rule inference.
7035    fn resolve_effective_pin(
7036        &self,
7037        pin_ref: &ast::Reference,
7038        spec: &LemmaSpec,
7039        source: &Source,
7040    ) -> Result<ImportEffectivePin, Error> {
7041        if spec
7042            .data
7043            .iter()
7044            .any(|data| data.reference.is_local() && data.reference.name == pin_ref.name)
7045        {
7046            return Ok(ImportEffectivePin::Data(DataPath::local(
7047                pin_ref.name.clone(),
7048            )));
7049        }
7050        if spec.rules.iter().any(|rule| rule.name == pin_ref.name) {
7051            return Ok(ImportEffectivePin::Rule(RulePath::new(
7052                Vec::new(),
7053                pin_ref.name.clone(),
7054            )));
7055        }
7056        Err(self.engine_error(
7057            format!(
7058                "`-> effective {}` does not name data or a rule of '{}'",
7059                pin_ref.name, spec.name
7060            ),
7061            source,
7062        ))
7063    }
7064
7065    /// `uses alias: spec -> with ...`: the import row(s), bindings written over each
7066    /// candidate template's slots.
7067    fn add_import(
7068        &mut self,
7069        data: &LemmaData,
7070        planned: &PlannedImport,
7071        bindings: &DataBindings,
7072        spec: &LemmaSpec,
7073    ) {
7074        let alias = &data.reference.name;
7075        let data_path = DataPath::local(alias.clone());
7076        if self.is_duplicate_data(data, &data_path, spec) {
7077            return;
7078        }
7079        let under = bindings_under(bindings, alias);
7080        match planned {
7081            PlannedImport::Static { identity, template } => {
7082                let prefix = vec![PathSegment::import(alias, identity)];
7083                let row = self.binding_row(RowWrite {
7084                    base: vec![None; template.data.len()],
7085                    identity: Arc::clone(identity),
7086                    template: Arc::clone(template),
7087                    origin: RowOrigin::Import {
7088                        alias: Arc::from(alias.as_str()),
7089                    },
7090                    bindings: under,
7091                    prefix,
7092                    hops: 0,
7093                });
7094                self.data.insert(
7095                    data_path,
7096                    DataDefinition::Import {
7097                        row,
7098                        pin: None,
7099                        candidates: BTreeMap::new(),
7100                        source: data.source_location.clone(),
7101                    },
7102                );
7103            }
7104            PlannedImport::Dynamic { pin, candidates } => {
7105                if let Err(error) =
7106                    self.validate_effective_pin_type(pin, spec, &data.source_location)
7107                {
7108                    self.errors.push(error);
7109                    return;
7110                }
7111                let mut candidate_rows = BTreeMap::new();
7112                for (identity, template) in candidates {
7113                    for binding in under.keys() {
7114                        let slot = binding.last().expect("BUG: binding key is never empty");
7115                        let path = DataPath::local(slot.clone());
7116                        if !template.data.contains_key(&path)
7117                            && !template
7118                                .data
7119                                .keys()
7120                                .any(|data_path| data_path.input_key() == *slot)
7121                        {
7122                            // Binding paths may be multi-segment; binding_row reports missing slots.
7123                        }
7124                    }
7125                    let prefix = vec![PathSegment::import(alias, identity)];
7126                    let row = self.binding_row(RowWrite {
7127                        base: vec![None; template.data.len()],
7128                        identity: Arc::clone(identity),
7129                        template: Arc::clone(template),
7130                        origin: RowOrigin::Import {
7131                            alias: Arc::from(alias.as_str()),
7132                        },
7133                        bindings: under.clone(),
7134                        prefix,
7135                        hops: 0,
7136                    });
7137                    candidate_rows.insert(identity.as_ref().clone(), row);
7138                }
7139                let row = Arc::clone(
7140                    candidate_rows
7141                        .values()
7142                        .next()
7143                        .expect("BUG: dynamic import candidates are non-empty"),
7144                );
7145                self.data.insert(
7146                    data_path,
7147                    DataDefinition::Import {
7148                        row,
7149                        pin: Some(pin.clone()),
7150                        candidates: candidate_rows,
7151                        source: data.source_location.clone(),
7152                    },
7153                );
7154            }
7155        }
7156    }
7157
7158    fn validate_effective_pin_type(
7159        &self,
7160        pin: &ImportEffectivePin,
7161        spec: &LemmaSpec,
7162        source: &Source,
7163    ) -> Result<(), Error> {
7164        let lemma_type: Option<Arc<LemmaType>> = match pin {
7165            ImportEffectivePin::Data(path) => self
7166                .data
7167                .get(path)
7168                .and_then(|definition| definition.resolved_type_arc().map(Arc::clone))
7169                .or_else(|| {
7170                    // Pin data may be declared after the uses in source order; look at AST.
7171                    let name = &path.data;
7172                    spec.data.iter().find_map(|data| {
7173                        if data.reference.name != *name {
7174                            return None;
7175                        }
7176                        match &data.value {
7177                            ParsedDataValue::Definition {
7178                                base:
7179                                    Some(ParentType::Primitive {
7180                                        primitive: PrimitiveKind::Date,
7181                                    }),
7182                                ..
7183                            } => Some(Arc::clone(semantics::primitive_date_arc())),
7184                            _ => None,
7185                        }
7186                    })
7187                }),
7188            ImportEffectivePin::Rule(path) => {
7189                self.rules.get(path).map(|rule| Arc::clone(&rule.rule_type))
7190            }
7191        };
7192        let Some(lemma_type) = lemma_type else {
7193            // Rule types are filled later; allow when the rule exists in the AST.
7194            // Rule types are filled after imports; validated again once inferred.
7195            if matches!(pin, ImportEffectivePin::Rule(_)) {
7196                return Ok(());
7197            }
7198            return Err(self.engine_error(
7199                "`-> effective` must name date or datetime data of this spec".to_string(),
7200                source,
7201            ));
7202        };
7203        if lemma_type.is_date() {
7204            return Ok(());
7205        }
7206        Err(self.engine_error(
7207            format!(
7208                "`-> effective` requires a date or datetime, got {}",
7209                lemma_type.specifications
7210            ),
7211            source,
7212        ))
7213    }
7214
7215    /// The row `write` describes, with every row its bindings hold. A list binding's
7216    /// copies are rows owned by the writing plan; a binding through a nested `uses`
7217    /// (`-> with j.x: 2`) writes over that import's row, held as [`RowBinding::Instance`].
7218    /// A binding that does not type is an error and leaves its slot as the base has it.
7219    fn binding_row(&mut self, write: RowWrite) -> Arc<RowInstance> {
7220        /// A list binding of template slot `slot`, filled by the rows of its copies.
7221        struct PendingList {
7222            slot: usize,
7223            rows: Vec<Arc<RowInstance>>,
7224            row_type: Arc<LemmaType>,
7225            key: ListKey,
7226            template: Arc<ExecutionPlan>,
7227            source: Source,
7228        }
7229
7230        struct Pending {
7231            identity: Arc<SpecIdentity>,
7232            origin: RowOrigin,
7233            hops: u32,
7234            bindings: Vec<Option<RowBinding>>,
7235            lists: Vec<PendingList>,
7236            /// Row and slot that holds this row.
7237            parent: Option<(usize, usize)>,
7238        }
7239
7240        let mut pending: Vec<Pending> = Vec::new();
7241        let mut writes: Vec<(RowWrite, Option<(usize, usize)>)> = vec![(write, None)];
7242        while let Some((write, parent)) = writes.pop() {
7243            let row = pending.len();
7244            let mut bindings = write.base;
7245            let mut lists = Vec::new();
7246            let mut nested: IndexMap<String, DataBindings> = IndexMap::new();
7247            for (key, (value, source)) in &write.bindings {
7248                let (name, rest) = key.split_first().expect("BUG: binding key is never empty");
7249                let path = DataPath::local(name.clone());
7250                let (slot, _, definition) =
7251                    write.template.data.get_full(&path).unwrap_or_else(|| {
7252                        panic!(
7253                            "BUG: binding `{}` is not data of `{}`",
7254                            key.join("."),
7255                            write.template.identity.spec
7256                        )
7257                    });
7258                if !rest.is_empty() {
7259                    assert!(
7260                        matches!(definition, DataDefinition::Import { .. }),
7261                        "BUG: binding `{}` steps through `{name}`, which is not a `uses`",
7262                        key.join(".")
7263                    );
7264                    nested
7265                        .entry(name.clone())
7266                        .or_default()
7267                        .insert(rest.to_vec(), (value.clone(), source.clone()));
7268                    continue;
7269                }
7270                if matches!(definition, DataDefinition::Import { .. }) {
7271                    let bound = write
7272                        .prefix
7273                        .iter()
7274                        .map(PathSegment::input_key)
7275                        .chain(std::iter::once(name.clone()))
7276                        .collect::<Vec<_>>()
7277                        .join(".");
7278                    self.errors.push(self.engine_error(
7279                        format!("cannot bind import alias '{bound}' as a value"),
7280                        source,
7281                    ));
7282                    continue;
7283                }
7284                match value {
7285                    BindingValue::Literal(literal) => {
7286                        let field_type = definition
7287                            .resolved_type_arc()
7288                            .expect("BUG: a value slot of a template plan is typed");
7289                        match source_row_literal(literal, field_type, &write.template) {
7290                            Ok(bound) => {
7291                                bindings[slot] =
7292                                    Some(RowBinding::Bound(OperationResult::from_bound(bound)));
7293                            }
7294                            Err(message) => {
7295                                self.errors.push(self.engine_error(message, source));
7296                            }
7297                        }
7298                    }
7299                    BindingValue::Reference { target, .. } => {
7300                        bindings[slot] = Some(RowBinding::Deferred {
7301                            target: target.clone(),
7302                            hops: write.hops,
7303                        });
7304                    }
7305                    BindingValue::InstanceRows(copies) => {
7306                        let (nested_identity, nested_template) =
7307                            self.nested_row_template(&write.template, &path);
7308                        let row_type = Arc::new(LemmaType::primitive(TypeSpecification::Spec {
7309                            spec: nested_identity.as_ref().clone(),
7310                        }));
7311                        lists.push(PendingList {
7312                            slot,
7313                            rows: Vec::new(),
7314                            row_type,
7315                            key: write
7316                                .template
7317                                .instance_lists
7318                                .get(&path)
7319                                .expect(
7320                                    "BUG: a list binding slot is an instance list of its template",
7321                                )
7322                                .key
7323                                .clone(),
7324                            template: Arc::clone(&nested_template),
7325                            source: source.clone(),
7326                        });
7327                        let list_name: Arc<str> = Arc::from(name.as_str());
7328                        let spec_name: Arc<str> = Arc::from(nested_identity.spec.as_str());
7329                        let repository: Option<Arc<str>> =
7330                            nested_identity.repository.as_deref().map(Arc::from);
7331                        for (index, copy) in copies.iter().enumerate() {
7332                            if copy.identity != *nested_identity {
7333                                self.errors.push(self.engine_error(
7334                                    format!(
7335                                        "`{}` is a `{}` instance; rows of `{name}` are `{}`",
7336                                        copy.name, copy.identity, nested_identity
7337                                    ),
7338                                    &copy.source,
7339                                ));
7340                                continue;
7341                            }
7342                            let segment = PathSegment::list_row_shared(
7343                                &list_name,
7344                                index + 1,
7345                                &spec_name,
7346                                repository.as_ref(),
7347                            );
7348                            let prefix: Vec<PathSegment> =
7349                                write.prefix.iter().cloned().chain([segment]).collect();
7350                            writes.push((
7351                                RowWrite {
7352                                    identity: Arc::clone(&nested_identity),
7353                                    base: vec![None; nested_template.data.len()],
7354                                    template: Arc::clone(&nested_template),
7355                                    origin: RowOrigin::Written {
7356                                        instance: SpecInstance {
7357                                            prefix: prefix.clone(),
7358                                        },
7359                                        source: copy.source.clone(),
7360                                    },
7361                                    bindings: copy.bindings.clone(),
7362                                    prefix,
7363                                    hops: 0,
7364                                },
7365                                Some((row, slot)),
7366                            ));
7367                        }
7368                    }
7369                    BindingValue::InstanceAliases(_) => unreachable!(
7370                        "BUG: instance aliases are resolved to rows before rows are built"
7371                    ),
7372                }
7373            }
7374            for (name, sub) in nested {
7375                let path = DataPath::local(name.clone());
7376                let (slot, _, definition) = write
7377                    .template
7378                    .data
7379                    .get_full(&path)
7380                    .expect("BUG: nested binding slot is template data");
7381                let DataDefinition::Import {
7382                    row: template_row, ..
7383                } = definition
7384                else {
7385                    unreachable!("BUG: nested binding slot is a `uses`");
7386                };
7387                let base = match &bindings[slot] {
7388                    Some(RowBinding::Instance(written)) => written.bindings.to_vec(),
7389                    None => template_row.bindings.to_vec(),
7390                    Some(other) => panic!("BUG: `uses` slot `{name}` holds {other:?}"),
7391                };
7392                let identity = Arc::clone(&template_row.identity);
7393                let template = self.template_of(&identity);
7394                let prefix: Vec<PathSegment> = write
7395                    .prefix
7396                    .iter()
7397                    .cloned()
7398                    .chain([PathSegment::import(&name, &identity)])
7399                    .collect();
7400                writes.push((
7401                    RowWrite {
7402                        identity,
7403                        template,
7404                        origin: template_row.origin.clone(),
7405                        base,
7406                        bindings: sub,
7407                        prefix,
7408                        hops: write.hops + 1,
7409                    },
7410                    Some((row, slot)),
7411                ));
7412            }
7413            pending.push(Pending {
7414                identity: write.identity,
7415                origin: write.origin,
7416                hops: write.hops,
7417                bindings,
7418                lists,
7419                parent,
7420            });
7421        }
7422
7423        // Rows complete last to first: every row completes before the row holding it,
7424        // and each list's rows arrive in order.
7425        loop {
7426            let row = pending.pop().expect("BUG: the written row is pending");
7427            let mut bindings = row.bindings;
7428            for list in row.lists {
7429                let rows =
7430                    match self.row_store_key(&list.rows, &list.key, &list.source, &list.template) {
7431                        None => list.rows,
7432                        Some(store_key) => match RowStore::keyed(list.rows, store_key) {
7433                            Ok(store) => store.rows,
7434                            Err((message, unkeyed)) => {
7435                                self.errors.push(self.engine_error(message, &list.source));
7436                                unkeyed.rows
7437                            }
7438                        },
7439                    };
7440                bindings[list.slot] = Some(RowBinding::Rows {
7441                    rows,
7442                    row_type: list.row_type,
7443                    hops: row.hops,
7444                });
7445            }
7446            let built = Arc::new(RowInstance {
7447                identity: row.identity,
7448                origin: row.origin,
7449                bindings: bindings.into(),
7450            });
7451            let Some((holder, slot)) = row.parent else {
7452                assert!(
7453                    pending.is_empty(),
7454                    "BUG: only the written row has no holder"
7455                );
7456                return built;
7457            };
7458            let holder = &mut pending[holder];
7459            match holder.lists.iter_mut().find(|list| list.slot == slot) {
7460                Some(list) => list.rows.push(built),
7461                None => holder.bindings[slot] = Some(RowBinding::Instance(built)),
7462            }
7463        }
7464    }
7465
7466    /// Template plan of `identity`, recorded on this graph with a template that imports it.
7467    fn template_of(&self, identity: &SpecIdentity) -> Arc<ExecutionPlan> {
7468        Arc::clone(
7469            self.instance_lists
7470                .templates
7471                .get(identity)
7472                .unwrap_or_else(|| {
7473                    panic!(
7474                        "BUG: template of `{identity}` is recorded with the template importing it"
7475                    )
7476                }),
7477        )
7478    }
7479
7480    /// Replace every [`BindingValue::InstanceAliases`] by the row copies it names: one
7481    /// per alias, each a `uses` instance of `spec` with all of that instance's bindings.
7482    ///
7483    /// A copied instance's own list bindings name aliases too (`root -> with children:
7484    /// mid_node`, `mid_node -> with children: leaf`), so aliases are resolved in
7485    /// dependency order and each copy takes the resolved bindings of its alias. An alias
7486    /// that reaches itself is a planning error: no row can hold itself.
7487    fn resolve_instance_alias_bindings(
7488        &mut self,
7489        bindings: DataBindings,
7490        spec: &LemmaSpec,
7491        imports: &HashMap<String, PlannedImport>,
7492    ) -> DataBindings {
7493        let mut resolved_aliases: HashMap<String, DataBindings> = HashMap::new();
7494        let mut visiting: Vec<String> = Vec::new();
7495        // `true` once the alias's dependencies are on the stack above it.
7496        let mut stack: Vec<(String, bool)> = aliases_named(&bindings)
7497            .into_iter()
7498            .rev()
7499            .map(|alias| (alias, false))
7500            .collect();
7501        while let Some((alias, entered)) = stack.last().cloned() {
7502            if entered {
7503                stack.pop();
7504                visiting.pop();
7505                let own = bindings_under(&bindings, &alias);
7506                let resolved = self.resolve_alias_layer(own, spec, imports, &resolved_aliases);
7507                resolved_aliases.insert(alias, resolved);
7508                continue;
7509            }
7510            if resolved_aliases.contains_key(&alias) {
7511                stack.pop();
7512                continue;
7513            }
7514            if visiting.contains(&alias) {
7515                let chain: Vec<&str> = visiting
7516                    .iter()
7517                    .skip_while(|seen| **seen != alias)
7518                    .map(String::as_str)
7519                    .chain(std::iter::once(alias.as_str()))
7520                    .collect();
7521                let source = alias_binding_source(&bindings, &alias)
7522                    .expect("BUG: an alias on the resolution stack is named by a binding");
7523                self.errors.push(self.engine_error(
7524                    format!(
7525                        "Circular instance-list source wiring: {}",
7526                        chain.join(" -> ")
7527                    ),
7528                    &source,
7529                ));
7530                resolved_aliases.insert(alias, IndexMap::new());
7531                stack.pop();
7532                continue;
7533            }
7534            stack.last_mut().expect("BUG: stack top cloned above").1 = true;
7535            visiting.push(alias.clone());
7536            let dependencies = aliases_named(&bindings_under(&bindings, &alias));
7537            for dependency in dependencies.into_iter().rev() {
7538                if !resolved_aliases.contains_key(&dependency) {
7539                    stack.push((dependency, false));
7540                }
7541            }
7542        }
7543        self.resolve_alias_layer(bindings, spec, imports, &resolved_aliases)
7544    }
7545
7546    /// One binding map with its [`BindingValue::InstanceAliases`] replaced, each alias
7547    /// taking its bindings from `resolved_aliases`.
7548    fn resolve_alias_layer(
7549        &mut self,
7550        bindings: DataBindings,
7551        spec: &LemmaSpec,
7552        imports: &HashMap<String, PlannedImport>,
7553        resolved_aliases: &HashMap<String, DataBindings>,
7554    ) -> DataBindings {
7555        let mut resolved: DataBindings = IndexMap::with_capacity(bindings.len());
7556        for (key, (value, source)) in bindings {
7557            let BindingValue::InstanceAliases(aliases) = &value else {
7558                resolved.insert(key, (value, source));
7559                continue;
7560            };
7561            let mut rows = Vec::with_capacity(aliases.len());
7562            let mut failed = false;
7563            for alias in aliases {
7564                let is_local_import = alias.segments.is_empty()
7565                    && spec.data.iter().any(|data| {
7566                        data.reference.is_local()
7567                            && data.reference.name == alias.name
7568                            && matches!(data.value, ParsedDataValue::Import { .. })
7569                    });
7570                if !is_local_import {
7571                    let slot = key.last().expect("BUG: binding key is never empty");
7572                    self.errors.push(self.engine_error(
7573                        format!(
7574                            "binding `{}` replaces instance list `{slot}` with `{alias}`, which is not a `uses` instance of `{}`; list slots take `uses` aliases",
7575                            key.join("."),
7576                            spec.name
7577                        ),
7578                        &source,
7579                    ));
7580                    failed = true;
7581                    continue;
7582                }
7583                let Some(planned) = imports.get(&alias.name) else {
7584                    // The alias's import did not resolve: its error is recorded.
7585                    failed = true;
7586                    continue;
7587                };
7588                if planned.is_dynamic() {
7589                    self.errors.push(self.engine_error(
7590                        format!(
7591                            "cannot seal `uses {}` into a list: it uses `-> effective`",
7592                            alias.name
7593                        ),
7594                        &source,
7595                    ));
7596                    failed = true;
7597                    continue;
7598                }
7599                let identity = planned.primary_identity();
7600                let alias_bindings = resolved_aliases
7601                    .get(&alias.name)
7602                    .unwrap_or_else(|| {
7603                        panic!(
7604                            "BUG: alias `{}` named by a binding was not resolved",
7605                            alias.name
7606                        )
7607                    })
7608                    .clone();
7609                rows.push(RowCopy {
7610                    identity: identity.as_ref().clone(),
7611                    name: alias.name.clone(),
7612                    bindings: alias_bindings,
7613                    source: source.clone(),
7614                });
7615            }
7616            if failed {
7617                continue;
7618            }
7619            resolved.insert(key, (BindingValue::InstanceRows(rows), source));
7620        }
7621        resolved
7622    }
7623
7624    fn add_rule(
7625        &mut self,
7626        rule: &LemmaRule,
7627        current_spec: &'a LemmaSpec,
7628        current_repository: &Arc<LemmaRepository>,
7629        data_map: &HashMap<String, &LemmaData>,
7630        rule_names: &HashSet<&str>,
7631        effective: &EffectiveDate,
7632    ) {
7633        let rule_path = RulePath::new(Vec::new(), rule.name.clone());
7634
7635        if self.rules.contains_key(&rule_path) {
7636            let rule_source = &rule.source_location;
7637            self.errors.push(
7638                self.engine_error(format!("Duplicate rule '{}'", rule_path.rule), rule_source),
7639            );
7640            return;
7641        }
7642
7643        let piecewise_nodes = if rule.unless_clauses.is_empty() { 0 } else { 2 };
7644        let ast_nodes = rule.expression.table.len()
7645            + rule
7646                .unless_clauses
7647                .iter()
7648                .map(|clause| clause.condition.table.len() + clause.result.table.len())
7649                .sum::<usize>();
7650        let mut depends_on_rules = BTreeSet::new();
7651        let mut convert_ctx = RuleExpressionConversion {
7652            table: ExprTable::with_capacity(ast_nodes + piecewise_nodes),
7653            spec: current_spec,
7654            repository: current_repository,
7655            data_map,
7656            rule_names,
7657            rule: &rule.name,
7658            effective,
7659            depends_on_rules: &mut depends_on_rules,
7660            row_scopes: Vec::new(),
7661        };
7662
7663        let mut scratch = ConvertScratch {
7664            frames: Vec::with_capacity(ast_nodes),
7665            values: Vec::with_capacity(ast_nodes),
7666            children: Vec::new(),
7667        };
7668        let errors_before = self.errors.len();
7669        let Some(default_result) =
7670            self.convert_ast_expression(rule.expression.root(), &mut convert_ctx, &mut scratch)
7671        else {
7672            self.drop_unconverted_rule(rule_path, errors_before);
7673            return;
7674        };
7675        let mut branches = vec![(None, default_result)];
7676        let default_source = convert_ctx.table.source(default_result).cloned();
7677        let mut arms = Vec::with_capacity(rule.unless_clauses.len() + 1);
7678        if !rule.unless_clauses.is_empty() {
7679            let default_condition = convert_ctx.push(
7680                ExpressionKind::Literal(Box::new(TypedLiteral::from_bool(true))),
7681                default_source.clone(),
7682            );
7683            arms.push((default_condition, default_result));
7684        }
7685        for unless_clause in &rule.unless_clauses {
7686            let Some(condition) = self.convert_ast_expression(
7687                unless_clause.condition.root(),
7688                &mut convert_ctx,
7689                &mut scratch,
7690            ) else {
7691                self.drop_unconverted_rule(rule_path, errors_before);
7692                return;
7693            };
7694            let Some(result) = self.convert_ast_expression(
7695                unless_clause.result.root(),
7696                &mut convert_ctx,
7697                &mut scratch,
7698            ) else {
7699                self.drop_unconverted_rule(rule_path, errors_before);
7700                return;
7701            };
7702            branches.push((Some(condition), result));
7703            arms.push((condition, result));
7704        }
7705        let root = if arms.is_empty() {
7706            default_result
7707        } else {
7708            convert_ctx.push(ExpressionKind::Piecewise(arms), default_source)
7709        };
7710
7711        let expression = ExprTree::new(std::mem::take(&mut convert_ctx.table), root);
7712        let rule_node = RuleNode {
7713            expression,
7714            branches,
7715            source: rule.source_location.clone(),
7716            depends_on_rules,
7717            recursive: false,
7718            rule_type: Arc::new(LemmaType::veto_type()),
7719            scale_families: BTreeSet::new(),
7720            spec: current_spec,
7721        };
7722
7723        self.rules.insert(rule_path, rule_node);
7724    }
7725
7726    /// [`RuleNode::recursive`] for every local rule named on a self-list row during conversion.
7727    fn mark_self_list_row_rules_recursive(&mut self) {
7728        let targets: Vec<RulePath> = self.self_list_row_rules.iter().cloned().collect();
7729        for path in targets {
7730            if self.rules.contains_key(&path) {
7731                self.rules
7732                    .get_mut(&path)
7733                    .expect("BUG: self-list row rule missing from graph after contains_key")
7734                    .recursive = true;
7735            } else if self.dropped_rules.contains(&path) {
7736                continue;
7737            } else {
7738                panic!(
7739                    "BUG: self-list row rule '{path}' neither in graph nor dropped after conversion"
7740                )
7741            }
7742        }
7743    }
7744
7745    fn drop_unconverted_rule(&mut self, rule_path: RulePath, errors_before: usize) {
7746        if self.errors.len() <= errors_before {
7747            panic!("BUG: add_rule conversion failed without an error");
7748        }
7749        self.dropped_rules.insert(rule_path);
7750    }
7751
7752    /// Converts the subtree at `expr` into `ctx.table`, children before parents, with an
7753    /// explicit stack. List command arguments are converted in their row scope before the
7754    /// command is built, see [`ConvertFrame`].
7755    fn convert_ast_expression<'t>(
7756        &mut self,
7757        expr: AstRef<'t>,
7758        ctx: &mut RuleExpressionConversion<'a, '_>,
7759        scratch: &mut ConvertScratch<'t>,
7760    ) -> Option<ExprId> {
7761        let ConvertScratch {
7762            frames,
7763            values,
7764            children,
7765        } = scratch;
7766        frames.push(ConvertFrame::Enter(expr));
7767        while let Some(frame) = frames.pop() {
7768            match frame {
7769                ConvertFrame::Enter(node) => {
7770                    frames.push(ConvertFrame::Node(node));
7771                    let start = frames.len();
7772                    for_each_conversion_child(node.kind(), |child| {
7773                        frames.push(ConvertFrame::Enter(node.child(child)));
7774                    });
7775                    frames[start..].reverse();
7776                }
7777                ConvertFrame::Node(node) => {
7778                    let mut arity = 0;
7779                    for_each_conversion_child(node.kind(), |_| arity += 1);
7780                    children.clear();
7781                    let mut converted = true;
7782                    for value in values.drain(values.len() - arity..) {
7783                        match value {
7784                            Some(id) => children.push(id),
7785                            None => converted = false,
7786                        }
7787                    }
7788                    if !converted {
7789                        values.push(None);
7790                    } else if let ast::ExpressionKind::ListCommand { .. } = node.kind() {
7791                        self.start_list_command(node, one(children), ctx, frames);
7792                    } else {
7793                        values.push(self.convert_ast_node(node, children, ctx));
7794                    }
7795                }
7796                ConvertFrame::List(build) => {
7797                    self.advance_list_command(build, frames, values, ctx);
7798                }
7799                ConvertFrame::ListArgument(mut build, sink) => {
7800                    let converted = values
7801                        .pop()
7802                        .expect("BUG: list argument is converted before its frame resumes");
7803                    match (sink, converted) {
7804                        (ListArgumentSink::Positional, Some(id)) => build.converted.push(id),
7805                        (ListArgumentSink::Positional, None) => {
7806                            values.push(None);
7807                            continue;
7808                        }
7809                        (ListArgumentSink::Field(path), Some(id)) => build.fields.push((path, id)),
7810                        (ListArgumentSink::Field(_), None) => build.failed = true,
7811                    }
7812                    frames.push(ConvertFrame::List(build));
7813                }
7814                ConvertFrame::PopRowScope => {
7815                    ctx.row_scopes.pop();
7816                }
7817            }
7818        }
7819        let converted = values
7820            .pop()
7821            .expect("BUG: conversion leaves one result for its root");
7822        assert!(
7823            values.is_empty(),
7824            "BUG: conversion left operands without a parent"
7825        );
7826        converted
7827    }
7828
7829    /// Pushes the frame that converts the arguments of the list command at `node`, whose list
7830    /// operand converted to `list`.
7831    fn start_list_command<'t>(
7832        &mut self,
7833        node: AstRef<'t>,
7834        list: ExprId,
7835        ctx: &RuleExpressionConversion<'a, '_>,
7836        frames: &mut Vec<ConvertFrame<'t>>,
7837    ) {
7838        let ast::ExpressionKind::ListCommand {
7839            list: list_source,
7840            command,
7841        } = node.kind()
7842        else {
7843            unreachable!("BUG: list command frame started for a non-list-command node")
7844        };
7845        let source = node
7846            .source()
7847            .expect("BUG: AST expression missing source location")
7848            .clone();
7849        let level = u32::try_from(ctx.row_scopes.len()).expect("BUG: row scope depth exceeds u32");
7850        let value_predicate = matches!(
7851            command,
7852            ast::ListCommand::Any {
7853                form: ast::ListPredicate::Value { .. }
7854            } | ast::ListCommand::All {
7855                form: ast::ListPredicate::Value { .. }
7856            } | ast::ListCommand::Where {
7857                form: ast::ListPredicate::Value { .. }
7858            }
7859        );
7860        let mut missing_row_template = false;
7861        let row_scope = if value_predicate {
7862            Some(RowScope::Value { level, list })
7863        } else {
7864            self.instance_list_identity(
7865                ExprRef {
7866                    table: &ctx.table,
7867                    id: list,
7868                },
7869                ctx.spec,
7870            )
7871            .and_then(|identity| {
7872                let Some(template) = self.row_template_of(&identity) else {
7873                    self.errors
7874                        .push(self.missing_row_template_error(&identity, &source));
7875                    missing_row_template = true;
7876                    return None;
7877                };
7878                let list_path = match node.child(*list_source).kind() {
7879                    ast::ExpressionKind::Reference(reference) => Some(reference.full_path()),
7880                    _ => None,
7881                };
7882                Some(RowScope::Instance {
7883                    list_path,
7884                    identity,
7885                    template,
7886                    level,
7887                })
7888            })
7889        };
7890        let row_level = row_scope.as_ref().map(RowScope::level);
7891        let pending = list_command_arguments(command, &row_scope);
7892        frames.push(ConvertFrame::List(Box::new(ListCommandBuild {
7893            node,
7894            source,
7895            list,
7896            row_level,
7897            row_scope,
7898            pending,
7899            converted: Vec::new(),
7900            fields: Vec::new(),
7901            failed: missing_row_template,
7902        })));
7903    }
7904
7905    /// Converts the next argument of a list command, or builds the command once all are done.
7906    fn advance_list_command<'t>(
7907        &mut self,
7908        mut build: Box<ListCommandBuild<'t>>,
7909        frames: &mut Vec<ConvertFrame<'t>>,
7910        values: &mut Vec<Option<ExprId>>,
7911        ctx: &mut RuleExpressionConversion<'a, '_>,
7912    ) {
7913        let node = build.node;
7914        let Some(argument) = build.pending.pop_front() else {
7915            let ast::ExpressionKind::ListCommand { command, .. } = node.kind() else {
7916                unreachable!("BUG: list command frame holds a list command node")
7917            };
7918            let op = self.finish_list_command(
7919                command,
7920                &build.row_scope,
7921                std::mem::take(&mut build.converted),
7922                std::mem::take(&mut build.fields),
7923                build.failed,
7924                &build.source,
7925            );
7926            let source = Some(build.source.clone());
7927            let converted = op.map(|op| {
7928                ctx.push(
7929                    ExpressionKind::ListCommand {
7930                        list: build.list,
7931                        op,
7932                        row_level: build.row_level,
7933                    },
7934                    source,
7935                )
7936            });
7937            values.push(converted);
7938            return;
7939        };
7940        match argument {
7941            ListArgument::Enclosing(child) => {
7942                frames.push(ConvertFrame::ListArgument(
7943                    build,
7944                    ListArgumentSink::Positional,
7945                ));
7946                frames.push(ConvertFrame::Enter(node.child(child)));
7947            }
7948            ListArgument::InRows(child) => {
7949                let scoped = build.row_scope.clone();
7950                frames.push(ConvertFrame::ListArgument(
7951                    build,
7952                    ListArgumentSink::Positional,
7953                ));
7954                if let Some(scope) = scoped {
7955                    ctx.row_scopes.push(scope);
7956                    frames.push(ConvertFrame::PopRowScope);
7957                }
7958                frames.push(ConvertFrame::Enter(node.child(child)));
7959            }
7960            ListArgument::Field { name, value } => match self.row_field_path(&build, &name) {
7961                Some(path) => {
7962                    frames.push(ConvertFrame::ListArgument(
7963                        build,
7964                        ListArgumentSink::Field(path),
7965                    ));
7966                    frames.push(ConvertFrame::Enter(node.child(value)));
7967                }
7968                None => {
7969                    build.failed = true;
7970                    frames.push(ConvertFrame::List(build));
7971                }
7972            },
7973        }
7974    }
7975
7976    fn convert_ast_node(
7977        &mut self,
7978        expr: AstRef<'_>,
7979        children: &[ExprId],
7980        ctx: &mut RuleExpressionConversion<'a, '_>,
7981    ) -> Option<ExprId> {
7982        let expr_src = expr
7983            .source()
7984            .expect("BUG: AST expression missing source location");
7985        let source = expr.source().cloned();
7986        match expr.kind() {
7987            ast::ExpressionKind::ImplicitRowValue => {
7988                let scope = ctx
7989                    .row_scopes
7990                    .iter()
7991                    .rev()
7992                    .find_map(|scope| match scope {
7993                        RowScope::Value { level, list } => Some((*level, *list)),
7994                        RowScope::Instance { .. } => None,
7995                    })
7996                    .expect("BUG: implicit row value converted outside a value predicate");
7997                Some(ctx.push(
7998                    ExpressionKind::RowValue {
7999                        level: scope.0,
8000                        list: scope.1,
8001                    },
8002                    source,
8003                ))
8004            }
8005            ast::ExpressionKind::Reference(r) => {
8006                let expr_source = expr_src;
8007                if !ctx.row_scopes.is_empty() {
8008                    match self.resolve_row_field(r, expr_source, ctx) {
8009                        RowFieldLookup::Field { field, member } => {
8010                            let ExpressionKind::RowField { identity, end, .. } = field.as_ref()
8011                            else {
8012                                unreachable!("BUG: a row field lookup holds a RowField read");
8013                            };
8014                            self.note_row_end_read(identity, end);
8015                            let field = ctx.push(*field, source.clone());
8016                            let Some((path, name)) = member else {
8017                                return Some(field);
8018                            };
8019                            return Some(ctx.push(
8020                                ExpressionKind::SpecMember {
8021                                    base: field,
8022                                    path,
8023                                    name,
8024                                },
8025                                source.clone(),
8026                            ));
8027                        }
8028                        RowFieldLookup::Failed => return None,
8029                        RowFieldLookup::NotARowField => {}
8030                    }
8031                }
8032                if let Some((head, rest)) = r.segments.split_first() {
8033                    if ctx.rule_names.contains(head.as_str()) && !ctx.data_map.contains_key(head) {
8034                        let base = RulePath::new(Vec::new(), head.clone());
8035                        ctx.depends_on_rules.insert(base.clone());
8036                        let base = ctx.push(ExpressionKind::RulePath(base), source.clone());
8037                        self.note_row_member_read(base, rest, &r.name, ctx);
8038                        return Some(ctx.push(
8039                            ExpressionKind::SpecMember {
8040                                base,
8041                                path: rest.to_vec(),
8042                                name: r.name.clone(),
8043                            },
8044                            source.clone(),
8045                        ));
8046                    }
8047                }
8048                let target_arc_opt = if r.segments.is_empty() {
8049                    None
8050                } else {
8051                    let (_, arc) = self.resolve_path_segments(
8052                        &r.segments,
8053                        expr_source,
8054                        Vec::new(),
8055                        ctx.spec,
8056                        ctx.repository,
8057                        ctx.effective,
8058                    )?;
8059                    Some(arc)
8060                };
8061
8062                let (is_data, is_rule, target_spec_name_opt) = match &target_arc_opt {
8063                    None => {
8064                        let is_data = ctx.data_map.contains_key(&r.name);
8065                        let is_rule = ctx.rule_names.contains(r.name.as_str());
8066                        (is_data, is_rule, None)
8067                    }
8068                    Some(target_arc) => {
8069                        let target_spec = target_arc;
8070                        let target_data_names: HashSet<&str> = target_spec
8071                            .data
8072                            .iter()
8073                            .filter(|f| f.reference.is_local())
8074                            .map(|f| f.reference.name.as_str())
8075                            .collect();
8076                        let target_rule_names: HashSet<&str> =
8077                            target_spec.rules.iter().map(|r| r.name.as_str()).collect();
8078                        let is_data = target_data_names.contains(r.name.as_str());
8079                        let is_rule = target_rule_names.contains(r.name.as_str());
8080                        (is_data, is_rule, Some(target_spec.name.as_str()))
8081                    }
8082                };
8083
8084                if is_data && is_rule {
8085                    self.errors.push(self.engine_error(
8086                        format!("'{}' is both a data and a rule", r.name),
8087                        expr_source,
8088                    ));
8089                    return None;
8090                }
8091                if (is_data || is_rule) && !r.segments.is_empty() {
8092                    let (head, rest) = r
8093                        .segments
8094                        .split_first()
8095                        .expect("BUG: segments checked non-empty");
8096                    let base = ctx.push(
8097                        ExpressionKind::DataPath(DataPath::local(head.clone())),
8098                        source.clone(),
8099                    );
8100                    return Some(ctx.push(
8101                        ExpressionKind::SpecMember {
8102                            base,
8103                            path: rest.to_vec(),
8104                            name: r.name.clone(),
8105                        },
8106                        source.clone(),
8107                    ));
8108                }
8109                if is_data {
8110                    let data_path = DataPath::local(r.name.clone());
8111                    return Some(ctx.push(ExpressionKind::DataPath(data_path), source.clone()));
8112                }
8113                if is_rule {
8114                    let rule_path = RulePath::new(Vec::new(), r.name.clone());
8115                    ctx.depends_on_rules.insert(rule_path.clone());
8116                    return Some(ctx.push(ExpressionKind::RulePath(rule_path), source.clone()));
8117                }
8118                let msg = match target_spec_name_opt {
8119                    Some(s) => format!("Reference '{}' not found in spec '{}'", r.name, s),
8120                    None => format!("Reference '{}' not found", r.name),
8121                };
8122                self.errors.push(self.engine_error(msg, expr_source));
8123                None
8124            }
8125
8126            ast::ExpressionKind::Logical {
8127                connective, steps, ..
8128            } => {
8129                let (first, steps) = convert_chain_steps(steps, children);
8130                Some(ctx.push(
8131                    ExpressionKind::Logical {
8132                        connective: *connective,
8133                        first,
8134                        steps,
8135                    },
8136                    source,
8137                ))
8138            }
8139
8140            ast::ExpressionKind::Arithmetic { steps, .. } => {
8141                let (first, steps) = convert_chain_steps(steps, children);
8142                Some(ctx.push(ExpressionKind::Arithmetic { first, steps }, source))
8143            }
8144
8145            ast::ExpressionKind::Comparison(_, op, _) => {
8146                let (left, right) = two(children);
8147                Some(ctx.push(ExpressionKind::Comparison(left, op.clone(), right), source))
8148            }
8149
8150            ast::ExpressionKind::UnitConversion(_, target) => {
8151                let converted_value = one(children);
8152
8153                let resolved_spec_types = self
8154                    .local_types
8155                    .iter()
8156                    .find(|(_, s, _)| discovery::same_loaded_spec(s, ctx.spec))
8157                    .map(|(_, _, t)| t);
8158                let unit_index = resolved_spec_types.map(|dt| &dt.unit_index);
8159                let resolved_map = resolved_spec_types.map(|dt| &dt.resolved);
8160                let semantic_target =
8161                    match conversion_target_to_semantic(target, unit_index, resolved_map) {
8162                        Ok(t) => t,
8163                        Err(msg) => {
8164                            let full_msg = unit_index
8165                                .map(|idx| {
8166                                    let mut valid: Vec<&str> =
8167                                        idx.keys().map(String::as_str).collect();
8168                                    valid.sort_unstable();
8169                                    format!("{} Valid units: {}", msg, valid.join(", "))
8170                                })
8171                                .unwrap_or(msg);
8172                            self.errors.push(Error::validation_with_context(
8173                                full_msg,
8174                                expr.source().cloned(),
8175                                None::<String>,
8176                                Some(self.main_spec),
8177                                None,
8178                            ));
8179                            return None;
8180                        }
8181                    };
8182
8183                Some(ctx.push(
8184                    ExpressionKind::UnitConversion(converted_value, semantic_target),
8185                    source,
8186                ))
8187            }
8188
8189            ast::ExpressionKind::LogicalNegation(_, neg_type) => {
8190                let converted_operand = one(children);
8191                Some(ctx.push(
8192                    ExpressionKind::LogicalNegation(converted_operand, neg_type.clone()),
8193                    source,
8194                ))
8195            }
8196
8197            ast::ExpressionKind::MathematicalComputation(op, _) => {
8198                let converted_operand = one(children);
8199                Some(ctx.push(
8200                    ExpressionKind::MathematicalComputation(op.clone(), converted_operand),
8201                    source,
8202                ))
8203            }
8204
8205            ast::ExpressionKind::StepRounding { mode, .. } => {
8206                let mut children = children.iter();
8207                let operand = *children
8208                    .next()
8209                    .expect("BUG: step rounding always has an operand");
8210                let step = children.next().copied();
8211                Some(ctx.push(
8212                    ExpressionKind::StepRounding {
8213                        operand,
8214                        mode: *mode,
8215                        step,
8216                    },
8217                    source,
8218                ))
8219            }
8220
8221            ast::ExpressionKind::RangeBound(bound, _) => {
8222                let converted_operand = one(children);
8223                Some(ctx.push(
8224                    ExpressionKind::RangeBound(*bound, converted_operand),
8225                    source,
8226                ))
8227            }
8228
8229            ast::ExpressionKind::Literal(value) => {
8230                let semantic_value = match value {
8231                    Value::NumberWithUnit(magnitude, unit) => {
8232                        let (bare, lt) = match self.resolve_unit_ref(ctx.spec, unit) {
8233                            Ok(resolved) => resolved,
8234                            Err(message) => {
8235                                self.errors.push(self.engine_error(message, expr_src));
8236                                return None;
8237                            }
8238                        };
8239                        match number_with_unit_to_value_kind(*magnitude, &bare, lt.as_ref()) {
8240                            Ok(v) => v,
8241                            Err(e) => {
8242                                self.errors.push(self.engine_error(e, expr_src));
8243                                return None;
8244                            }
8245                        }
8246                    }
8247                    Value::Range(left, right) => match (left.as_ref(), right.as_ref()) {
8248                        (
8249                            Value::NumberWithUnit(left_mag, left_unit),
8250                            Value::NumberWithUnit(right_mag, right_unit),
8251                        ) => {
8252                            match self.measure_ratio_range_from_number_with_unit_endpoints(
8253                                ctx.spec, *left_mag, left_unit, *right_mag, right_unit,
8254                            ) {
8255                                Ok(v) => v,
8256                                Err(e) => {
8257                                    self.errors.push(self.engine_error(e, expr_src));
8258                                    return None;
8259                                }
8260                            }
8261                        }
8262                        _ => match value_to_semantic(value) {
8263                            Ok(v) => v,
8264                            Err(e) => {
8265                                self.errors.push(self.engine_error(e, expr_src));
8266                                return None;
8267                            }
8268                        },
8269                    },
8270                    _ => match value_to_semantic(value) {
8271                        Ok(v) => v,
8272                        Err(e) => {
8273                            self.errors.push(self.engine_error(e, expr_src));
8274                            return None;
8275                        }
8276                    },
8277                };
8278                let lemma_type: Arc<LemmaType> = match value {
8279                    Value::Text(_) => primitive_text_arc().clone(),
8280                    Value::Number(_) => primitive_number_arc().clone(),
8281                    Value::NumberWithUnit(_, unit) => match self.resolve_unit_ref(ctx.spec, unit) {
8282                        Ok((bare, lt)) => match &semantic_value {
8283                            ValueKind::Measure(_) | ValueKind::Ratio(_) => {
8284                                Arc::new(lt.as_ref().clone().with_unit(bare))
8285                            }
8286                            _ => lt,
8287                        },
8288                        Err(message) => {
8289                            self.errors.push(self.engine_error(message, expr_src));
8290                            return None;
8291                        }
8292                    },
8293                    Value::Boolean(_) => primitive_boolean_arc().clone(),
8294                    Value::Date(_) => primitive_date_arc().clone(),
8295                    Value::Time(_) => primitive_time_arc().clone(),
8296                    Value::Range(left_v, right_v) => match (left_v.as_ref(), right_v.as_ref()) {
8297                        (Value::NumberWithUnit(_, _), Value::NumberWithUnit(_, _)) => {
8298                            match &semantic_value {
8299                                ValueKind::Range(left, right) => {
8300                                    let specs = range_type_specification_from_endpoints(
8301                                    left.lemma_type.as_ref(),
8302                                    right.lemma_type.as_ref(),
8303                                )
8304                                .unwrap_or_else(|| {
8305                                    unreachable!(
8306                                        "BUG: measure/ratio range endpoints must form a range type after resolve"
8307                                    )
8308                                });
8309                                    Arc::new(LemmaType::primitive(specs))
8310                                }
8311                                _ => unreachable!(
8312                                    "BUG: semantic range literal conversion returned non-range value kind"
8313                                ),
8314                            }
8315                        }
8316                        _ => match &semantic_value {
8317                            ValueKind::Range(left, right) => {
8318                                let endpoint_type = |endpoint: &TypedLiteral| -> Arc<LemmaType> {
8319                                    match &endpoint.value {
8320                                        ValueKind::Number(_) => primitive_number_arc().clone(),
8321                                        ValueKind::Date(_) => primitive_date_arc().clone(),
8322                                        ValueKind::Time(_) => primitive_time_arc().clone(),
8323                                        ValueKind::Ratio(_) => {
8324                                            semantics::primitive_ratio_arc().clone()
8325                                        }
8326                                        ValueKind::Measure(_) => Arc::clone(&endpoint.lemma_type),
8327                                        other => unreachable!(
8328                                            "BUG: untyped range endpoint kind in expression conversion: {other:?}"
8329                                        ),
8330                                    }
8331                                };
8332                                let specs = range_type_specification_from_endpoints(
8333                                    endpoint_type(left).as_ref(),
8334                                    endpoint_type(right).as_ref(),
8335                                )
8336                                .unwrap_or_else(|| {
8337                                    unreachable!(
8338                                        "BUG: attempted to construct a range literal from incompatible endpoint types"
8339                                    )
8340                                });
8341                                Arc::new(LemmaType::primitive(specs))
8342                            }
8343                            _ => unreachable!(
8344                                "BUG: semantic range literal conversion returned non-range value kind"
8345                            ),
8346                        },
8347                    },
8348                };
8349                let literal_value = TypedLiteral {
8350                    value: semantic_value,
8351                    lemma_type,
8352                };
8353                Some(ctx.push(
8354                    ExpressionKind::Literal(Box::new(literal_value)),
8355                    source.clone(),
8356                ))
8357            }
8358
8359            ast::ExpressionKind::Veto(veto_expression) => {
8360                Some(ctx.push(ExpressionKind::Veto(veto_expression.clone()), source))
8361            }
8362
8363            ast::ExpressionKind::ResultIsVeto(_) => {
8364                let converted = one(children);
8365                Some(ctx.push(ExpressionKind::ResultIsVeto(converted), source))
8366            }
8367
8368            ast::ExpressionKind::Now => Some(ctx.push(ExpressionKind::Now, source)),
8369
8370            ast::ExpressionKind::DateRelative(kind, _) => {
8371                let converted_date = one(children);
8372                Some(ctx.push(ExpressionKind::DateRelative(*kind, converted_date), source))
8373            }
8374
8375            ast::ExpressionKind::DateCalendar(kind, unit, _) => {
8376                let converted_date = one(children);
8377                Some(ctx.push(
8378                    ExpressionKind::DateCalendar(*kind, *unit, converted_date),
8379                    source,
8380                ))
8381            }
8382
8383            ast::ExpressionKind::RangeLiteral(..) => {
8384                let (left, right) = two(children);
8385                Some(ctx.push(ExpressionKind::RangeLiteral(left, right), source))
8386            }
8387
8388            ast::ExpressionKind::PastFutureRange(kind, _) => {
8389                let converted_offset = one(children);
8390                Some(ctx.push(
8391                    ExpressionKind::PastFutureRange(*kind, converted_offset),
8392                    source,
8393                ))
8394            }
8395
8396            ast::ExpressionKind::RangeContainment(..) => {
8397                let (converted_value, converted_range) = two(children);
8398                Some(ctx.push(
8399                    ExpressionKind::RangeContainment(converted_value, converted_range),
8400                    source,
8401                ))
8402            }
8403
8404            ast::ExpressionKind::ListType { element } => {
8405                self.convert_list_type_expression(element, expr, ctx)
8406            }
8407
8408            ast::ExpressionKind::ListCommand { .. } => {
8409                unreachable!("BUG: list commands are converted by convert_ast_expression")
8410            }
8411
8412            ast::ExpressionKind::Member { path, .. } => {
8413                let converted_base = one(children);
8414                self.note_row_member_read(converted_base, &path.segments, &path.name, ctx);
8415                Some(ctx.push(
8416                    ExpressionKind::SpecMember {
8417                        base: converted_base,
8418                        path: path.segments.clone(),
8419                        name: path.name.clone(),
8420                    },
8421                    source,
8422                ))
8423            }
8424        }
8425    }
8426
8427    /// Row spec of the rows a converted list expression yields, when it is an instance list.
8428    ///
8429    /// `spec` is the spec being converted: its rules that are not converted
8430    /// yet are read from source.
8431    fn instance_list_identity(
8432        &self,
8433        list: ExprRef<'_, ExpressionKind>,
8434        spec: &LemmaSpec,
8435    ) -> Option<SpecIdentity> {
8436        let scope = SourceScope { spec };
8437        self.search_identity(
8438            IdentitySearch::new(SearchKind::List, Pending::Sem(list), Continuation::Answer),
8439            &scope,
8440        )
8441    }
8442
8443    /// Row spec of the single row a converted expression yields (`list -> first`), when
8444    /// it picks a row of an instance list.
8445    fn row_value_identity<'e>(
8446        &'e self,
8447        expression: ExprRef<'e, ExpressionKind>,
8448        scope: &SourceScope<'e>,
8449    ) -> Option<SpecIdentity> {
8450        self.search_identity(
8451            IdentitySearch::new(
8452                SearchKind::Row,
8453                Pending::Sem(expression),
8454                Continuation::Answer,
8455            ),
8456            scope,
8457        )
8458    }
8459
8460    /// Runs `root` and the searches it nests, depth-first with explicit stacks. A search
8461    /// that runs out of work yields nothing to its parent, which goes on with its own work.
8462    fn search_identity<'e>(
8463        &'e self,
8464        root: IdentitySearch<'e>,
8465        scope: &SourceScope<'e>,
8466    ) -> Option<SpecIdentity> {
8467        let mut searches = vec![root];
8468        while let Some(search) = searches.last_mut() {
8469            let Some(item) = search.pending.pop() else {
8470                searches.pop();
8471                continue;
8472            };
8473            let step = match search.kind {
8474                SearchKind::List => self.list_identity_step(item, scope, search),
8475                SearchKind::Row => self.row_identity_step(item, scope, search),
8476            };
8477            let mut found = match step {
8478                SearchStep::Next => continue,
8479                SearchStep::Nested(nested) => {
8480                    searches.push(nested);
8481                    continue;
8482                }
8483                SearchStep::Found(identity) => identity,
8484            };
8485            loop {
8486                let done = searches
8487                    .pop()
8488                    .expect("BUG: the search that found an identity is on the stack");
8489                match done.on_found {
8490                    Continuation::Answer => return Some(found),
8491                    Continuation::RowOfList => {}
8492                    Continuation::Member { path, name } => {
8493                        let parent = searches
8494                            .last_mut()
8495                            .expect("BUG: a member search nests in a list search");
8496                        let chain = self.row_template_of(&found).and_then(|template| {
8497                            let mut parts: Vec<&str> = path.iter().map(String::as_str).collect();
8498                            parts.push(name);
8499                            self.template_row_chain(&found, &template, &parts)
8500                        });
8501                        match chain.and_then(|chain| {
8502                            let last = chain.last().expect("BUG: a row end chain has a last hop");
8503                            self.row_end_list_identity(
8504                                &last.identity,
8505                                &last.end,
8506                                &mut parent.pending,
8507                            )
8508                        }) {
8509                            Some(identity) => found = identity,
8510                            None => break,
8511                        }
8512                    }
8513                }
8514            }
8515        }
8516        None
8517    }
8518
8519    /// One work item of a [`SearchKind::List`] search: pipeline commands that keep the row
8520    /// spec, rule bodies, conditional arms and member reads on a row.
8521    fn list_identity_step<'e>(
8522        &'e self,
8523        item: Pending<'e>,
8524        scope: &SourceScope<'e>,
8525        search: &mut IdentitySearch<'e>,
8526    ) -> SearchStep<'e> {
8527        let identity = match item {
8528            Pending::Sem(list) => match list.kind() {
8529                ExpressionKind::DataPath(path) => self.instance_list_data_identity(path),
8530                ExpressionKind::Literal(literal) => match &literal.lemma_type.specifications {
8531                    TypeSpecification::List { element, .. } => match &element.specifications {
8532                        TypeSpecification::Spec { spec } => Some(spec.clone()),
8533                        _ => None,
8534                    },
8535                    _ => None,
8536                },
8537                ExpressionKind::ListCommand {
8538                    list: inner, op, ..
8539                } => match op {
8540                    ListOp::Predicate {
8541                        kind: ListPredKind::Where,
8542                        ..
8543                    }
8544                    | ListOp::Sort { .. }
8545                    | ListOp::Top { .. }
8546                    | ListOp::With { .. }
8547                    | ListOp::WithRow { .. }
8548                    | ListOp::Key { .. } => {
8549                        search.pending.push(Pending::Sem(list.child(*inner)));
8550                        None
8551                    }
8552                    ListOp::Predicate { .. }
8553                    | ListOp::First
8554                    | ListOp::Last
8555                    | ListOp::Count
8556                    | ListOp::Find { .. }
8557                    | ListOp::Reduce { .. } => None,
8558                },
8559                ExpressionKind::RulePath(rule) => {
8560                    search.pending.push(Pending::Rule(rule.clone()));
8561                    None
8562                }
8563                ExpressionKind::Piecewise(arms) => {
8564                    for (_, result) in arms.iter().rev() {
8565                        search.pending.push(Pending::Sem(list.child(*result)));
8566                    }
8567                    None
8568                }
8569                ExpressionKind::RowField { identity, end, .. } => {
8570                    self.row_end_list_identity(identity, end, &mut search.pending)
8571                }
8572                ExpressionKind::SpecMember {
8573                    base, path, name, ..
8574                } => {
8575                    return SearchStep::Nested(search.nested(
8576                        SearchKind::Row,
8577                        Pending::Sem(list.child(*base)),
8578                        Continuation::Member { path, name },
8579                    ));
8580                }
8581                _ => None,
8582            },
8583            Pending::Ast(expr) => match expr.kind() {
8584                ast::ExpressionKind::Reference(reference) if reference.segments.is_empty() => {
8585                    let is_data = scope.spec.data.iter().any(|data| {
8586                        data.reference.is_local() && data.reference.name == reference.name
8587                    });
8588                    if is_data {
8589                        self.instance_list_data_identity(&DataPath::local(reference.name.clone()))
8590                    } else {
8591                        search.pending.push(Pending::Rule(RulePath::new(
8592                            Vec::new(),
8593                            reference.name.clone(),
8594                        )));
8595                        None
8596                    }
8597                }
8598                ast::ExpressionKind::ListCommand {
8599                    list,
8600                    command:
8601                        ast::ListCommand::Where { .. }
8602                        | ast::ListCommand::Sort { .. }
8603                        | ast::ListCommand::Top { .. }
8604                        | ast::ListCommand::With { .. }
8605                        | ast::ListCommand::Key { .. },
8606                } => {
8607                    search.pending.push(Pending::Ast(expr.child(*list)));
8608                    None
8609                }
8610                _ => None,
8611            },
8612            Pending::Rule(rule) => {
8613                if !search.enter(&rule) {
8614                    None
8615                } else if let Some(node) = self.rules.get(&rule) {
8616                    if let Some((_, body)) = node.branches.first() {
8617                        search.pending.push(Pending::Sem(node.expression.at(*body)));
8618                    }
8619                    None
8620                } else if !rule.segments.is_empty() {
8621                    None
8622                } else {
8623                    if let Some(source_rule) = scope.spec.rules.iter().find(|r| r.name == rule.rule)
8624                    {
8625                        search
8626                            .pending
8627                            .push(Pending::Ast(source_rule.expression.root()));
8628                    }
8629                    None
8630                }
8631            }
8632        };
8633        match identity {
8634            Some(identity) => SearchStep::Found(identity),
8635            None => SearchStep::Next,
8636        }
8637    }
8638
8639    /// One work item of a [`SearchKind::Row`] search: `-> first` / `-> last` / `-> find`
8640    /// hand the list to a nested list search; rule bodies and conditional arms are followed.
8641    fn row_identity_step<'e>(
8642        &'e self,
8643        item: Pending<'e>,
8644        scope: &SourceScope<'e>,
8645        search: &mut IdentitySearch<'e>,
8646    ) -> SearchStep<'e> {
8647        match item {
8648            Pending::Sem(expression) => match expression.kind() {
8649                ExpressionKind::ListCommand {
8650                    list,
8651                    op: ListOp::First | ListOp::Last | ListOp::Find { .. },
8652                    ..
8653                } => SearchStep::Nested(search.nested(
8654                    SearchKind::List,
8655                    Pending::Sem(expression.child(*list)),
8656                    Continuation::RowOfList,
8657                )),
8658                ExpressionKind::RulePath(rule) => {
8659                    search.pending.push(Pending::Rule(rule.clone()));
8660                    SearchStep::Next
8661                }
8662                ExpressionKind::Piecewise(arms) => {
8663                    for (_, result) in arms.iter().rev() {
8664                        search.pending.push(Pending::Sem(expression.child(*result)));
8665                    }
8666                    SearchStep::Next
8667                }
8668                ExpressionKind::DataPath(path) => match self.data.get(path) {
8669                    Some(DataDefinition::Import { row, .. }) => {
8670                        SearchStep::Found(row.identity.as_ref().clone())
8671                    }
8672                    _ => SearchStep::Next,
8673                },
8674                _ => SearchStep::Next,
8675            },
8676            Pending::Ast(expr) => match expr.kind() {
8677                ast::ExpressionKind::Reference(reference) if reference.segments.is_empty() => {
8678                    search.pending.push(Pending::Rule(RulePath::new(
8679                        Vec::new(),
8680                        reference.name.clone(),
8681                    )));
8682                    SearchStep::Next
8683                }
8684                ast::ExpressionKind::ListCommand {
8685                    list,
8686                    command:
8687                        ast::ListCommand::First | ast::ListCommand::Last | ast::ListCommand::Find { .. },
8688                } => SearchStep::Nested(search.nested(
8689                    SearchKind::List,
8690                    Pending::Ast(expr.child(*list)),
8691                    Continuation::RowOfList,
8692                )),
8693                _ => SearchStep::Next,
8694            },
8695            Pending::Rule(rule) => {
8696                if !search.enter(&rule) {
8697                } else if let Some(node) = self.rules.get(&rule) {
8698                    for (_, body) in node.branches.iter().rev() {
8699                        search.pending.push(Pending::Sem(node.expression.at(*body)));
8700                    }
8701                } else if rule.segments.is_empty() {
8702                    if let Some(source_rule) = scope.spec.rules.iter().find(|r| r.name == rule.rule)
8703                    {
8704                        for unless in source_rule.unless_clauses.iter().rev() {
8705                            search.pending.push(Pending::Ast(unless.result.root()));
8706                        }
8707                        search
8708                            .pending
8709                            .push(Pending::Ast(source_rule.expression.root()));
8710                    }
8711                }
8712                SearchStep::Next
8713            }
8714        }
8715    }
8716
8717    /// Row spec of the instance list field `end` names on rows of `identity`. A local
8718    /// rule of the spec being planned is searched through `pending`.
8719    fn row_end_list_identity<'e>(
8720        &'e self,
8721        identity: &SpecIdentity,
8722        end: &RowEnd,
8723        pending: &mut Vec<Pending<'e>>,
8724    ) -> Option<SpecIdentity> {
8725        let own_rows = *identity == self.instance_lists.self_identity;
8726        let lemma_type: &LemmaType = match end {
8727            RowEnd::Data(field) if own_rows => self.data.get(field)?.resolved_type_arc()?,
8728            RowEnd::Data(field) => self
8729                .instance_lists
8730                .templates
8731                .get(identity)?
8732                .data
8733                .get(field)?
8734                .resolved_type_arc()?,
8735            RowEnd::Rule(rule) if own_rows => {
8736                pending.push(Pending::Rule(rule.clone()));
8737                return None;
8738            }
8739            RowEnd::Rule(rule) => {
8740                &self
8741                    .instance_lists
8742                    .templates
8743                    .get(identity)?
8744                    .rules
8745                    .get(rule)?
8746                    .rule_type
8747            }
8748        };
8749        if !lemma_type.is_list() {
8750            return None;
8751        }
8752        lemma_type
8753            .list_element_lemma_type()
8754            .spec_identity()
8755            .cloned()
8756    }
8757
8758    fn instance_list_data_identity(&self, path: &DataPath) -> Option<SpecIdentity> {
8759        let definition = self.data.get(path)?;
8760        match &definition.resolved_type_arc()?.specifications {
8761            TypeSpecification::List { element, .. } => match &element.specifications {
8762                TypeSpecification::Spec { spec } => Some(spec.clone()),
8763                _ => None,
8764            },
8765            _ => None,
8766        }
8767    }
8768
8769    /// `base.path.name` where `base` may be a row of a self-list: a rule read there is
8770    /// recursion through the list, as for [`ExpressionKind::RowField`].
8771    fn note_row_member_read(
8772        &mut self,
8773        base: ExprId,
8774        path: &[String],
8775        name: &str,
8776        ctx: &mut RuleExpressionConversion<'a, '_>,
8777    ) {
8778        let scope = SourceScope { spec: ctx.spec };
8779        let base = ExprRef {
8780            table: &ctx.table,
8781            id: base,
8782        };
8783        let Some(identity) = self.row_value_identity(base, &scope) else {
8784            return;
8785        };
8786        if identity != self.instance_lists.self_identity {
8787            return;
8788        }
8789        let mut parts: Vec<&str> = path.iter().map(String::as_str).collect();
8790        parts.push(name);
8791        if let Some(chain) = self.template_row_chain(&identity, &RowTemplate::Root, &parts) {
8792            let first = chain.first().expect("BUG: a row end chain has a first hop");
8793            self.note_row_end_read(&first.identity, &first.end);
8794        }
8795    }
8796
8797    /// A row field read of a self-list: Rule ends and data that reference a rule
8798    /// mark the target recursive. Never a same-instance dependency edge.
8799    fn note_row_end_read(&mut self, identity: &SpecIdentity, end: &RowEnd) {
8800        match end {
8801            RowEnd::Rule(rule) => self.note_row_rule_read(identity, rule),
8802            RowEnd::Data(path) => {
8803                if let Some(DataDefinition::Reference {
8804                    target: ReferenceTarget::Rule(rule),
8805                    ..
8806                }) = self.data.get(path)
8807                {
8808                    let rule = rule.clone();
8809                    self.note_row_rule_read(identity, &rule);
8810                }
8811            }
8812        }
8813    }
8814
8815    /// Rule `rule` read on a row of `identity`. On a self-list row this names a child-row
8816    /// field (a rule of this plan, under its own hops or an import's), never a
8817    /// same-instance [`RuleNode::depends_on_rules`] edge. The target is marked
8818    /// [`RuleNode::recursive`] after conversion.
8819    fn note_row_rule_read(&mut self, identity: &SpecIdentity, rule: &RulePath) {
8820        if *identity != self.instance_lists.self_identity {
8821            return;
8822        }
8823        self.self_list_row_rules.insert(rule.clone());
8824    }
8825
8826    /// Resolve `r` as a field of a row of an enclosing instance-list command.
8827    fn resolve_row_field(
8828        &mut self,
8829        r: &ast::Reference,
8830        source: &Source,
8831        ctx: &RuleExpressionConversion<'a, '_>,
8832    ) -> RowFieldLookup {
8833        let written = r.full_path();
8834        for scope in ctx.row_scopes.iter().rev() {
8835            let RowScope::Instance {
8836                list_path,
8837                identity,
8838                template,
8839                level,
8840            } = scope
8841            else {
8842                continue;
8843            };
8844            let Some(list_path) = list_path else {
8845                continue;
8846            };
8847            let Some(rest_parts) = written.strip_prefix(list_path.as_slice()) else {
8848                continue;
8849            };
8850            if rest_parts.is_empty() {
8851                continue;
8852            }
8853            let rest: Vec<&str> = rest_parts.iter().map(String::as_str).collect();
8854            let list_name = list_path.join(".");
8855            return match self.template_row_chain(identity, template, &rest) {
8856                Some(chain) => {
8857                    row_field_lookup(*level, identity, chain, &rest, Some(list_path.clone()))
8858                }
8859                None => {
8860                    let rest_display = rest.join(".");
8861                    let message = format!(
8862                        "no data, rule or uses `{rest_display}` on rows of `{list_name}` (`{}`)",
8863                        identity.spec
8864                    );
8865                    self.errors.push(self.engine_error(message, source));
8866                    RowFieldLookup::Failed
8867                }
8868            };
8869        }
8870        let field_parts: Vec<&str> = r
8871            .segments
8872            .iter()
8873            .map(String::as_str)
8874            .chain(std::iter::once(r.name.as_str()))
8875            .collect();
8876        let row_matches: Vec<(&RowScope, Vec<RowHop>)> = ctx
8877            .row_scopes
8878            .iter()
8879            .rev()
8880            .filter_map(|scope| {
8881                let RowScope::Instance {
8882                    template, identity, ..
8883                } = scope
8884                else {
8885                    return None;
8886                };
8887                self.template_row_chain(identity, template, &field_parts)
8888                    .map(|chain| (scope, chain))
8889            })
8890            .collect();
8891        let Some((scope, chain)) = row_matches.first() else {
8892            return RowFieldLookup::NotARowField;
8893        };
8894        let head = r.segments.first().unwrap_or(&r.name);
8895        let enclosing = ctx.data_map.contains_key(head)
8896            || (ctx.rule_names.contains(head.as_str()) && head.as_str() != ctx.rule);
8897        if enclosing || row_matches.len() > 1 {
8898            let mut owners: Vec<String> = Vec::new();
8899            if enclosing {
8900                owners.push(format!("on `{}`", ctx.spec.name));
8901            }
8902            for (other, _) in &row_matches {
8903                let RowScope::Instance {
8904                    list_path,
8905                    identity,
8906                    ..
8907                } = other
8908                else {
8909                    unreachable!("BUG: row matches contain only instance scopes")
8910                };
8911                owners.push(match list_path {
8912                    Some(path) => format!("on rows of `{}`", path.join(".")),
8913                    None => format!("on `{}` rows", identity.spec),
8914                });
8915            }
8916            let RowScope::Instance {
8917                list_path,
8918                identity,
8919                ..
8920            } = scope
8921            else {
8922                unreachable!("BUG: row match is an instance scope")
8923            };
8924            let qualifier = list_path
8925                .as_ref()
8926                .map(|path| path.join("."))
8927                .unwrap_or_else(|| identity.spec.clone());
8928            let field = field_parts.join(".");
8929            self.errors.push(self.engine_error(
8930                format!(
8931                    "`{field}` exists {}; write `{qualifier}.{field}`",
8932                    owners.join(" and ")
8933                ),
8934                source,
8935            ));
8936            return RowFieldLookup::Failed;
8937        }
8938        let RowScope::Instance {
8939            identity, level, ..
8940        } = scope
8941        else {
8942            unreachable!("BUG: row match is an instance scope")
8943        };
8944        row_field_lookup(*level, identity, chain.clone(), &field_parts, None)
8945    }
8946
8947    fn convert_list_type_expression(
8948        &mut self,
8949        element: &ParentType,
8950        expr: AstRef<'_>,
8951        ctx: &mut RuleExpressionConversion<'a, '_>,
8952    ) -> Option<ExprId> {
8953        let (_, _, types) = self
8954            .local_types
8955            .iter()
8956            .find(|(_, spec, _)| discovery::same_loaded_spec(spec, ctx.spec))
8957            .expect("BUG: rule spec types resolved before rule conversion");
8958        let (_, list_type) = types
8959            .rule_list_types
8960            .iter()
8961            .find(|(written, _)| written == element)
8962            .expect("BUG: rule list types resolved with the spec's types");
8963        let list_type = Arc::clone(list_type);
8964        let source = expr
8965            .source()
8966            .expect("BUG: AST expression missing source location");
8967        if let TypeSpecification::List { element: row, .. } = &list_type.specifications {
8968            if let TypeSpecification::Spec { spec: identity } = &row.specifications {
8969                // Rows this rule builds (`item list -> with ...`) run their rules in a
8970                // row context of the row spec's template, which `uses item` in this
8971                // spec brings in; without it, row commands report the missing `uses`.
8972                if *identity != self.instance_lists.self_identity && self.imports_in_scope(identity)
8973                {
8974                    let spec_ref = discovery::list_element_spec_ref(ctx.spec, element)
8975                        .expect("BUG: a rule list type of spec rows names the row spec");
8976                    let (row_repository, row_spec) = match discovery::resolve_spec_ref_or_self(
8977                        self.context,
8978                        &spec_ref,
8979                        ctx.repository,
8980                        ctx.spec,
8981                        ctx.effective,
8982                        Some(source.clone()),
8983                    ) {
8984                        Ok(pair) => pair,
8985                        Err(error) => {
8986                            self.errors.push(error);
8987                            return None;
8988                        }
8989                    };
8990                    let at = spec_ref.at(ctx.effective);
8991                    let resolved = self.identity_at(&row_repository, row_spec, &at)?;
8992                    assert_eq!(
8993                        resolved, *identity,
8994                        "BUG: rule list type `{element}` resolved to another slice than its type"
8995                    );
8996                    self.row_template(identity, &row_repository, row_spec, &at)?;
8997                }
8998            }
8999        }
9000        Some(ctx.push(
9001            ExpressionKind::Literal(Box::new(TypedLiteral {
9002                value: ValueKind::List(Vec::new().into()),
9003                lemma_type: list_type,
9004            })),
9005            Some(source.clone()),
9006        ))
9007    }
9008
9009    /// Whether the spec being planned has a `uses` of `identity`.
9010    fn imports_in_scope(&self, identity: &SpecIdentity) -> bool {
9011        self.data.values().any(|definition| {
9012            matches!(definition, DataDefinition::Import { row, .. } if *row.identity == *identity)
9013        })
9014    }
9015
9016    /// Path of the data field that `-> with` row field `name` names. Records the error and
9017    /// returns `None` when it is not data of the row spec or repeats a field of the row.
9018    fn row_field_path(&mut self, build: &ListCommandBuild<'_>, name: &str) -> Option<DataPath> {
9019        let scope = build
9020            .row_scope
9021            .as_ref()
9022            .expect("BUG: field arguments are queued only with a row scope");
9023        let RowScope::Instance {
9024            template, identity, ..
9025        } = scope
9026        else {
9027            panic!("BUG: field arguments are queued only for instance rows")
9028        };
9029        let field = self
9030            .template_row_chain(identity, template, &[name])
9031            .and_then(|chain| match chain.as_slice() {
9032                [RowHop {
9033                    end: RowEnd::Data(path),
9034                    ..
9035                }] => Some(path.clone()),
9036                _ => None,
9037            });
9038        let Some(path) = field else {
9039            self.errors.push(self.engine_error(
9040                format!("row field `{name}` is not data on `{}`", identity.spec),
9041                &build.source,
9042            ));
9043            return None;
9044        };
9045        if build.fields.iter().any(|(existing, _)| *existing == path) {
9046            self.errors.push(self.engine_error(
9047                format!("row field `{name}` appears twice in one row"),
9048                &build.source,
9049            ));
9050            return None;
9051        }
9052        Some(path)
9053    }
9054
9055    /// Planned list command from its converted arguments, which are in the order of
9056    /// [`list_command_arguments`]. `None` when a rule-level error was recorded.
9057    fn finish_list_command(
9058        &mut self,
9059        command: &ast::ListCommand,
9060        row_scope: &Option<RowScope>,
9061        converted: Vec<ExprId>,
9062        fields: Vec<(DataPath, ExprId)>,
9063        failed: bool,
9064        source: &Source,
9065    ) -> Option<ListOp> {
9066        let mut converted = converted.into_iter();
9067        match command {
9068            ast::ListCommand::Find { .. } => Some(ListOp::Find {
9069                keys: converted.collect(),
9070            }),
9071            ast::ListCommand::First => Some(ListOp::First),
9072            ast::ListCommand::Last => Some(ListOp::Last),
9073            ast::ListCommand::Count => Some(ListOp::Count),
9074            ast::ListCommand::Sum { argument } => Some(ListOp::Reduce {
9075                kind: ListReduce::Sum,
9076                argument: optional_argument(argument, &mut converted),
9077            }),
9078            ast::ListCommand::Avg { argument } => Some(ListOp::Reduce {
9079                kind: ListReduce::Avg,
9080                argument: optional_argument(argument, &mut converted),
9081            }),
9082            ast::ListCommand::Product { argument } => Some(ListOp::Reduce {
9083                kind: ListReduce::Product,
9084                argument: optional_argument(argument, &mut converted),
9085            }),
9086            ast::ListCommand::Min { argument } => Some(ListOp::Reduce {
9087                kind: ListReduce::Min,
9088                argument: optional_argument(argument, &mut converted),
9089            }),
9090            ast::ListCommand::Max { argument } => Some(ListOp::Reduce {
9091                kind: ListReduce::Max,
9092                argument: optional_argument(argument, &mut converted),
9093            }),
9094            ast::ListCommand::Any { form } => {
9095                Some(list_predicate_op(form, ListPredKind::Any, &mut converted))
9096            }
9097            ast::ListCommand::All { form } => {
9098                Some(list_predicate_op(form, ListPredKind::All, &mut converted))
9099            }
9100            ast::ListCommand::Where { form } => {
9101                Some(list_predicate_op(form, ListPredKind::Where, &mut converted))
9102            }
9103            ast::ListCommand::Sort {
9104                descending,
9105                argument,
9106            } => Some(ListOp::Sort {
9107                descending: *descending,
9108                argument: optional_argument(argument, &mut converted),
9109            }),
9110            ast::ListCommand::Top { .. } => Some(ListOp::Top {
9111                n: next_argument(&mut converted),
9112            }),
9113            ast::ListCommand::With { rows } => match (rows, row_scope) {
9114                (ast::ListWithRows::Values(_), None) => Some(ListOp::With {
9115                    rows: converted.collect(),
9116                }),
9117                (ast::ListWithRows::Values(_), Some(_)) => {
9118                    // Named `uses` instances as rows: converted operands are Spec values.
9119                    (!failed).then_some(ListOp::With {
9120                        rows: converted.collect(),
9121                    })
9122                }
9123                (ast::ListWithRows::FieldPairs(_), None) => {
9124                    if !failed {
9125                        self.errors.push(self.engine_error(
9126                            "`-> with field: value` appends a row to an instance list; this is a value list, write `-> with <value>`",
9127                            source,
9128                        ));
9129                    }
9130                    None
9131                }
9132                (ast::ListWithRows::FieldPairs(_), Some(scope)) => {
9133                    let RowScope::Instance { identity, .. } = scope else {
9134                        unreachable!(
9135                            "BUG: `-> with field: value` converted under a value-list scope"
9136                        )
9137                    };
9138                    (!failed).then_some(ListOp::WithRow {
9139                        identity: identity.clone(),
9140                        fields,
9141                    })
9142                }
9143            },
9144            ast::ListCommand::Key { fields } => {
9145                let paths: Vec<DataPath> = fields
9146                    .iter()
9147                    .map(|field| DataPath {
9148                        segments: Vec::new(),
9149                        data: field.to_string(),
9150                    })
9151                    .collect();
9152                Some(ListOp::Key { fields: paths })
9153            }
9154        }
9155    }
9156}
9157
9158static EMPTY_INSTANCE_LIST: ast::InstanceListBody = ast::InstanceListBody {
9159    key: None,
9160    rows: Vec::new(),
9161    option_rows: Vec::new(),
9162};
9163
9164/// What a written row field path names on the list's row spec.
9165enum WrittenField {
9166    /// A value field; `key` is its binding key.
9167    Value {
9168        key: Vec<String>,
9169    },
9170    NotData,
9171    /// A `uses` instance of `identity`, bound field by field.
9172    Import {
9173        identity: Arc<SpecIdentity>,
9174    },
9175    List,
9176}
9177
9178/// The `data x: spec list` whose rows are being built, in the spec that declares it.
9179struct ListSite<'s, 'a> {
9180    list_path: &'s DataPath,
9181    /// Row spec identity, shared by the rows.
9182    identity: &'s Arc<SpecIdentity>,
9183    /// Row spec template: names and types of row fields.
9184    template: &'s Arc<ExecutionPlan>,
9185    spec: &'a LemmaSpec,
9186    /// Repository the declaring spec is loaded under.
9187    repository: &'s Arc<LemmaRepository>,
9188    effective: &'s EffectiveDate,
9189    /// Bindings of the declaring spec, aliases resolved (relative keys).
9190    bindings: &'s DataBindings,
9191}
9192
9193/// One row written by the plan being built: `bindings` (keys relative to the row)
9194/// written over `base`, the slots the row has before them.
9195struct RowWrite {
9196    identity: Arc<SpecIdentity>,
9197    template: Arc<ExecutionPlan>,
9198    origin: RowOrigin,
9199    base: Vec<Option<RowBinding>>,
9200    bindings: DataBindings,
9201    /// Hops naming the row from the writing plan (`i`, `staff.2`, `i.j`); rows of its
9202    /// list bindings are named under it.
9203    prefix: Vec<PathSegment>,
9204    /// Instances between the row's owner and the writing plan.
9205    hops: u32,
9206}
9207
9208/// Aliases named by the [`BindingValue::InstanceAliases`] values of `bindings`, in
9209/// binding order, each once.
9210fn aliases_named(bindings: &DataBindings) -> Vec<String> {
9211    let mut names: Vec<String> = Vec::new();
9212    for (value, _) in bindings.values() {
9213        let BindingValue::InstanceAliases(aliases) = value else {
9214            continue;
9215        };
9216        for alias in aliases {
9217            if !names.contains(&alias.name) {
9218                names.push(alias.name.clone());
9219            }
9220        }
9221    }
9222    names
9223}
9224
9225/// Source of the first binding whose [`BindingValue::InstanceAliases`] names `alias`.
9226fn alias_binding_source(bindings: &DataBindings, alias: &str) -> Option<Source> {
9227    bindings.values().find_map(|(value, source)| match value {
9228        BindingValue::InstanceAliases(aliases) if aliases.iter().any(|a| a.name == alias) => {
9229            Some(source.clone())
9230        }
9231        _ => None,
9232    })
9233}
9234
9235/// Bindings under `alias` with the alias hop stripped: what the import `alias` and a
9236/// copy of it are written with.
9237fn bindings_under(bindings: &DataBindings, alias: &str) -> DataBindings {
9238    bindings
9239        .iter()
9240        .filter(|(key, _)| key.len() > 1 && key[0] == alias)
9241        .map(|(key, value)| (key[1..].to_vec(), value.clone()))
9242        .collect()
9243}
9244
9245/// Literal source-row value read against the template field type.
9246fn source_row_literal(
9247    value: &Value,
9248    field_type: &Arc<LemmaType>,
9249    template: &ExecutionPlan,
9250) -> Result<BoundValueKind, String> {
9251    source_row_literal_in(
9252        value,
9253        field_type,
9254        template.expression_unit_index(),
9255        &template.resolved_types.resolved,
9256    )
9257}
9258
9259fn source_row_literal_in(
9260    value: &Value,
9261    field_type: &Arc<LemmaType>,
9262    unit_index: &UnitIndex,
9263    named_types: &IndexMap<String, Arc<LemmaType>>,
9264) -> Result<BoundValueKind, String> {
9265    let typed = typed_literal_from_parsed(value, field_type, unit_index, named_types)?;
9266    execution_plan::validate_value_against_type(
9267        typed.lemma_type.as_ref(),
9268        &LiteralValue {
9269            value: typed.value.clone(),
9270        },
9271        unit_index,
9272    )?;
9273    Ok(BoundValueKind {
9274        value: typed.value,
9275        unit: typed.lemma_type.unit().map(Arc::from),
9276    })
9277}
9278
9279fn unit_tokens_for_spec<'b>(
9280    types: &'b ResolvedTypesMap,
9281    spec: &LemmaSpec,
9282) -> UnitTokenResolver<'b> {
9283    match find_types_by_spec(types, spec) {
9284        Some(resolved) => UnitTokenResolver::Declared {
9285            unit_index: &resolved.unit_index,
9286            named_types: &resolved.resolved,
9287        },
9288        None => UnitTokenResolver::Undeclared,
9289    }
9290}
9291
9292fn member_in_template(
9293    template: &ExecutionPlan,
9294    name: &str,
9295    error: impl Fn(String) -> Error,
9296) -> Result<ReferenceTarget, Error> {
9297    let data_path = DataPath::local(name.to_string());
9298    let rule_path = RulePath::new(Vec::new(), name.to_string());
9299    let is_data = template.data.contains_key(&data_path);
9300    let is_rule = template.rules.contains_key(&rule_path);
9301    if is_data && is_rule {
9302        return Err(error(format!(
9303            "Reference target '{name}' is ambiguous: both a data and a rule in spec '{}'",
9304            template.spec_name
9305        )));
9306    }
9307    if is_data {
9308        return Ok(ReferenceTarget::Data(data_path));
9309    }
9310    if is_rule {
9311        return Ok(ReferenceTarget::Rule(rule_path));
9312    }
9313    Err(error(format!(
9314        "Reference target '{name}' not found in spec '{}'",
9315        template.spec_name
9316    )))
9317}
9318
9319/// Scale target of each dynamic unit the types of `resolved_types` declare, in the
9320/// coordinates of the spec they belong to: a unit inherited through `uses` hops has
9321/// its target prefixed by those hops.
9322fn collect_unit_scale_targets(
9323    resolved_types: &ResolvedSpecTypes,
9324    data: &IndexMap<DataPath, DataDefinition>,
9325) -> IndexMap<(TypeFamily, String), ReferenceTarget> {
9326    let mut targets = IndexMap::new();
9327    for lemma_type in resolved_types.resolved.values() {
9328        let TypeSpecification::Measure { units, .. } = &lemma_type.specifications else {
9329            continue;
9330        };
9331        let Some(family) = lemma_type.measure_family().cloned() else {
9332            continue;
9333        };
9334        for unit in units.iter() {
9335            let Some(declared) = unit.resolved_scale_target() else {
9336                if unit.is_dynamic() {
9337                    panic!(
9338                        "BUG: dynamic unit '{}' on '{}' never went through scale resolution",
9339                        unit.name,
9340                        lemma_type.name()
9341                    );
9342                }
9343                continue;
9344            };
9345            let import_path = resolved_types
9346                .unit_index
9347                .owners_for(&unit.name)
9348                .iter()
9349                .find(|owner| owner.lemma_type().same_measure_family(lemma_type.as_ref()))
9350                .unwrap_or_else(|| {
9351                    panic!(
9352                        "BUG: dynamic unit '{}' on '{}' has no same-family owner in the unit index",
9353                        unit.name,
9354                        lemma_type.name()
9355                    )
9356                })
9357                .import_path();
9358            let consumer = prefix_scale_target(declared.clone(), import_path, data);
9359            targets.insert((family.clone(), unit.name.clone()), consumer);
9360        }
9361    }
9362    targets
9363}
9364
9365/// Nodes of `unsorted` (those a topological sort left unordered) that lie on a cycle.
9366/// A node that only depends on a cycle is left unordered too, but is not part of it.
9367fn nodes_on_cycles<'k, K: Ord>(
9368    unsorted: &[&'k K],
9369    dependencies: impl Fn(&'k K) -> Vec<&'k K>,
9370) -> Vec<&'k K> {
9371    let remaining: BTreeSet<&K> = unsorted.iter().copied().collect();
9372    unsorted
9373        .iter()
9374        .copied()
9375        .filter(|&start| {
9376            let mut seen: BTreeSet<&K> = BTreeSet::new();
9377            let mut stack = dependencies(start);
9378            while let Some(node) = stack.pop() {
9379                if node == start {
9380                    return true;
9381                }
9382                if remaining.contains(node) && seen.insert(node) {
9383                    stack.extend(dependencies(node));
9384                }
9385            }
9386            false
9387        })
9388        .collect()
9389}
9390
9391fn prefix_scale_target(
9392    target: ReferenceTarget,
9393    import_path: &[String],
9394    data: &IndexMap<DataPath, DataDefinition>,
9395) -> ReferenceTarget {
9396    if import_path.is_empty() {
9397        return target;
9398    }
9399    let mut hops = Vec::new();
9400    for alias in import_path {
9401        let Some(DataDefinition::Import { row, .. }) = data.get(&DataPath::local(alias.clone()))
9402        else {
9403            panic!("BUG: unit owner import path '{alias}' is not a uses slot of this plan");
9404        };
9405        hops.push(PathSegment::import(alias, row.identity.as_ref()));
9406    }
9407    match target {
9408        ReferenceTarget::Data(path) => {
9409            hops.extend(path.segments);
9410            ReferenceTarget::Data(DataPath {
9411                segments: hops,
9412                data: path.data,
9413            })
9414        }
9415        ReferenceTarget::Rule(path) => {
9416            hops.extend(path.segments);
9417            ReferenceTarget::Rule(RulePath {
9418                segments: hops,
9419                rule: path.rule,
9420            })
9421        }
9422    }
9423}
9424
9425/// Family and unit of every unscaled measure in `value` of type `lemma_type`,
9426/// including range endpoints and the items of a value list.
9427fn unscaled_units(value: &ValueKind, lemma_type: &LemmaType) -> Vec<(TypeFamily, String)> {
9428    let mut found = Vec::new();
9429    let mut pending: Vec<(&ValueKind, &LemmaType)> = vec![(value, lemma_type)];
9430    while let Some((value, lemma_type)) = pending.pop() {
9431        match value {
9432            ValueKind::UnscaledMeasure { unit, .. } => {
9433                let family = lemma_type.measure_family().unwrap_or_else(|| {
9434                    panic!(
9435                        "BUG: unscaled measure in unit '{unit}' typed '{}' without a measure family",
9436                        lemma_type.name()
9437                    )
9438                });
9439                found.push((family.clone(), unit.to_string()));
9440            }
9441            ValueKind::Range(left, right) => {
9442                pending.push((&right.value, right.lemma_type.as_ref()));
9443                pending.push((&left.value, left.lemma_type.as_ref()));
9444            }
9445            ValueKind::List(rows) => {
9446                if rows.stored().is_none() {
9447                    for item in rows.values().iter().rev() {
9448                        pending.push((&item.value, item.lemma_type.as_ref()));
9449                    }
9450                }
9451            }
9452            _ => {}
9453        }
9454    }
9455    found
9456}
9457
9458/// Find resolved types for a spec by name. Since per-slice resolution registers
9459/// at most one version per spec name, this is a simple name match.
9460fn find_types_by_spec<'b>(
9461    types: &'b ResolvedTypesMap,
9462    spec: &LemmaSpec,
9463) -> Option<&'b ResolvedSpecTypes> {
9464    types
9465        .iter()
9466        .find(|(_, s, _)| discovery::same_loaded_spec(s, spec))
9467        .map(|(_, _, t)| t)
9468}
9469
9470/// The one measure scope of a plan: the main spec's resolved unit index and
9471/// signature index, which already merge every `uses` import. Graph inference,
9472/// NormalForm cell stamping and evaluation all resolve arithmetic in it, for
9473/// imported rules too, so a rule has exactly one type inside a plan.
9474fn arithmetic_scope<'b>(resolved_types: &'b ResolvedTypesMap, graph: &Graph) -> MeasureScope<'b> {
9475    let main_spec = graph.main_spec();
9476    let types = find_types_by_spec(resolved_types, main_spec).unwrap_or_else(|| {
9477        panic!(
9478            "BUG: main spec '{}' typed before its types were resolved",
9479            main_spec.name
9480        )
9481    });
9482    MeasureScope {
9483        unit_index: &types.unit_index,
9484        signature_index: &types.signature_index,
9485        named_types: &types.resolved,
9486    }
9487}
9488
9489/// True iff an anonymous measure at the rule boundary must be rejected.
9490///
9491/// Planning promotes anonymous intermediates to named types when a unique in-scope type
9492/// shares the decomposition. Any anonymous measure that still reaches a rule boundary
9493/// cannot be serialized on the API (no declared unit map to emit).
9494fn anonymous_rule_boundary_requires_rejection() -> bool {
9495    true
9496}
9497
9498/// Build a precise error message for an anonymous measure that survives to a rule boundary.
9499///
9500/// The boundary check rejects such intermediates: a rule must produce a named type so its
9501/// result is unambiguous. If multiple in-scope types share the decomposition, the message
9502/// names them as a hint to `as <type>` cast the result.
9503fn anonymous_rule_boundary_error(
9504    rule_path: &RulePath,
9505    spec: &LemmaSpec,
9506    graph: &Graph,
9507    resolved_types: &ResolvedTypesMap,
9508    decomposition: &BaseMeasureVector,
9509    branch_index: Option<usize>,
9510) -> String {
9511    let candidates_hint = match find_unique_measure_type_in_unit_index(
9512        arithmetic_scope(resolved_types, graph).unit_index,
9513        decomposition,
9514    ) {
9515        DecompositionMatch::Multiple(family_names) => format!(
9516            " Multiple measure families in scope share these dimensions: {}. Give the rule an explicit named type.",
9517            family_names.join(", ")
9518        ),
9519        _ => String::new(),
9520    };
9521    match branch_index {
9522        Some(index) => format!(
9523            "Unless clause {} in rule '{}' (spec '{}') returns an anonymous intermediate with              unresolved dimensions {:?}. Give the rule a named measure or ratio type with              declared units, or rewrite the expression so dimensions resolve to a named type in scope.{}",
9524            index, rule_path.rule, spec.name, decomposition, candidates_hint
9525        ),
9526        None => format!(
9527            "Rule '{}' in spec '{}' returns an anonymous intermediate with unresolved              dimensions {:?}. Give the rule a named measure or ratio type with declared units,              or rewrite the expression so dimensions resolve to a named type in scope.{}",
9528            rule_path.rule, spec.name, decomposition, candidates_hint
9529        ),
9530    }
9531}
9532
9533// =============================================================================
9534// Phase 1: Pure type inference (no validation, no error collection)
9535// =============================================================================
9536
9537/// `RulePresence::Dropped` here means conversion already recorded a planning error for
9538/// the rule (see `Graph::rule_presence`). The dependent gets an undetermined type; the
9539/// error is reported once.
9540fn inferred_or_dropped(
9541    graph: &Graph,
9542    computed_rule_types: &HashMap<RulePath, Arc<LemmaType>>,
9543    rule_path: &RulePath,
9544) -> Arc<LemmaType> {
9545    if let Some(lemma_type) = computed_rule_types.get(rule_path) {
9546        return Arc::clone(lemma_type);
9547    }
9548    match graph.rule_presence(rule_path) {
9549        RulePresence::Dropped => Arc::new(LemmaType::undetermined_type()),
9550        RulePresence::Present => {
9551            panic!("BUG: rule type not inferred before dependent in topological order")
9552        }
9553        RulePresence::Absent => {
9554            panic!("BUG: rule '{rule_path}' neither in graph nor dropped")
9555        }
9556    }
9557}
9558
9559/// Type of `expression`, without validation; `LemmaType::undetermined_type()` when it cannot
9560/// be determined (e.g. unknown data). Every node of the table up to `expression` is typed in
9561/// table order, each reading its operands' types from the typed prefix, so nothing recurses.
9562fn infer_expression_type(
9563    expression: ExprRef<'_, ExpressionKind>,
9564    graph: &Graph,
9565    computed_rule_types: &HashMap<RulePath, Arc<LemmaType>>,
9566    resolved_types: &ResolvedTypesMap,
9567    cache: &mut InferExpressionTypeCache,
9568) -> Arc<LemmaType> {
9569    let key = expression.table as *const ExprTable<ExpressionKind> as usize;
9570    let types = cache
9571        .tables
9572        .entry(key)
9573        .or_insert_with(|| Vec::with_capacity(expression.table.len()));
9574    if types.is_empty() {
9575        cache.typed.push(key);
9576    }
9577    for index in types.len()..=expression.id.index() {
9578        let node = expression.child(ExprId::from_index(index));
9579        let lemma_type =
9580            infer_expression_type_uncached(node, graph, computed_rule_types, resolved_types, types);
9581        types.push(lemma_type);
9582    }
9583    Arc::clone(&types[expression.id.index()])
9584}
9585
9586/// Type of an operand of the node being typed. Operands precede their parent in the table,
9587/// so they are always in the typed prefix.
9588fn cached_expression_type(
9589    cache: &[Arc<LemmaType>],
9590    expression: ExprRef<'_, ExpressionKind>,
9591) -> Arc<LemmaType> {
9592    Arc::clone(
9593        cache
9594            .get(expression.id.index())
9595            .expect("BUG: operand typed before its parent"),
9596    )
9597}
9598
9599fn infer_expression_type_uncached(
9600    expression: ExprRef<'_, ExpressionKind>,
9601    graph: &Graph,
9602    computed_rule_types: &HashMap<RulePath, Arc<LemmaType>>,
9603    resolved_types: &ResolvedTypesMap,
9604    cache: &[Arc<LemmaType>],
9605) -> Arc<LemmaType> {
9606    match expression.kind() {
9607        ExpressionKind::Literal(literal_value) => Arc::clone(&literal_value.lemma_type),
9608
9609        ExpressionKind::DataPath(data_path) => {
9610            infer_data_type(data_path, graph, computed_rule_types)
9611        }
9612
9613        ExpressionKind::RulePath(rule_path) => {
9614            inferred_or_dropped(graph, computed_rule_types, rule_path)
9615        }
9616
9617        ExpressionKind::Logical { first, steps, .. } => steps.iter().fold(
9618            cached_expression_type(cache, expression.child(*first)),
9619            |left_type, step| {
9620                let right_type = cached_expression_type(cache, expression.child(step.operand));
9621                logical_connective_type(&left_type, &right_type)
9622            },
9623        ),
9624
9625        ExpressionKind::LogicalNegation(operand, _) => {
9626            let operand_type = cached_expression_type(cache, expression.child(*operand));
9627            logical_not_type(&operand_type)
9628        }
9629
9630        ExpressionKind::Comparison(left, _, right) => {
9631            let left_type = cached_expression_type(cache, expression.child(*left));
9632            let right_type = cached_expression_type(cache, expression.child(*right));
9633            comparison_type(&left_type, &right_type)
9634        }
9635
9636        ExpressionKind::Arithmetic { first, steps } => {
9637            let scope = arithmetic_scope(resolved_types, graph);
9638            steps.iter().fold(
9639                cached_expression_type(cache, expression.child(*first)),
9640                |left_type, step| {
9641                    let right = expression.child(step.operand);
9642                    let right_type = cached_expression_type(cache, right);
9643                    arithmetic_step_type(left_type, &step.op, right, right_type, &scope)
9644                },
9645            )
9646        }
9647
9648        ExpressionKind::UnitConversion(source_expression, target) => {
9649            let source_type = cached_expression_type(cache, expression.child(*source_expression));
9650            unit_conversion_type(&source_type, target)
9651        }
9652
9653        ExpressionKind::MathematicalComputation(op, operand) => {
9654            let operand_type = cached_expression_type(cache, expression.child(*operand));
9655            math_op_type(op, &operand_type, &arithmetic_scope(resolved_types, graph))
9656        }
9657
9658        ExpressionKind::StepRounding { operand, step, .. } => step_rounding_type(
9659            &cached_expression_type(cache, expression.child(*operand)),
9660            step.map(|step| cached_expression_type(cache, expression.child(step)))
9661                .as_ref(),
9662        ),
9663
9664        ExpressionKind::RangeBound(_, operand) => {
9665            let operand_type = cached_expression_type(cache, expression.child(*operand));
9666            if operand_type.vetoed() {
9667                return Arc::new(LemmaType::veto_type());
9668            }
9669            if operand_type.is_undetermined() {
9670                return Arc::new(LemmaType::undetermined_type());
9671            }
9672            range_bound_result_type(
9673                operand_type.as_ref(),
9674                &arithmetic_scope(resolved_types, graph),
9675            )
9676        }
9677
9678        ExpressionKind::Veto(_) => Arc::new(LemmaType::veto_type()),
9679
9680        ExpressionKind::ResultIsVeto(operand) => {
9681            cached_expression_type(cache, expression.child(*operand));
9682            result_is_veto_type()
9683        }
9684
9685        ExpressionKind::Now => primitive_date_arc().clone(),
9686
9687        ExpressionKind::DateRelative(_, date_expr)
9688        | ExpressionKind::DateCalendar(_, _, date_expr) => {
9689            let date_type = cached_expression_type(cache, expression.child(*date_expr));
9690            date_predicate_type(&date_type)
9691        }
9692
9693        ExpressionKind::RangeContainment(value, range) => {
9694            let value_type = cached_expression_type(cache, expression.child(*value));
9695            let range_type = cached_expression_type(cache, expression.child(*range));
9696            if value_type.vetoed() || range_type.vetoed() {
9697                return Arc::new(LemmaType::veto_type());
9698            }
9699            primitive_boolean_arc().clone()
9700        }
9701
9702        ExpressionKind::RangeLiteral(left, right) => {
9703            let left_type = cached_expression_type(cache, expression.child(*left));
9704            let right_type = cached_expression_type(cache, expression.child(*right));
9705            if left_type.vetoed() || right_type.vetoed() {
9706                return Arc::new(LemmaType::veto_type());
9707            }
9708            if left_type.is_undetermined() || right_type.is_undetermined() {
9709                return Arc::new(LemmaType::undetermined_type());
9710            }
9711            infer_range_type_from_endpoint_types(left_type.as_ref(), right_type.as_ref())
9712        }
9713
9714        ExpressionKind::PastFutureRange(_, offset_expr) => {
9715            let offset_type = cached_expression_type(cache, expression.child(*offset_expr));
9716            past_future_range_type(&offset_type)
9717        }
9718
9719        ExpressionKind::SpecMember { base, path, name } => {
9720            let base_type = cached_expression_type(cache, expression.child(*base));
9721            if base_type.vetoed() {
9722                return Arc::new(LemmaType::veto_type());
9723            }
9724            if !base_type.is_spec() {
9725                return Arc::new(LemmaType::undetermined_type());
9726            }
9727            match row_member_end(graph, &base_type, path, name) {
9728                Some((fields, end)) => fields.end_type(&end, computed_rule_types),
9729                None => Arc::new(LemmaType::undetermined_type()),
9730            }
9731        }
9732
9733        ExpressionKind::RowField { identity, end, .. } => row_fields(graph, identity)
9734            .map(|fields| fields.end_type(end, computed_rule_types))
9735            .unwrap_or_else(|| Arc::new(LemmaType::undetermined_type())),
9736        ExpressionKind::RowValue { list, .. } => {
9737            let list_type = cached_expression_type(cache, expression.child(*list));
9738            list_element_type(&list_type, graph, resolved_types)
9739        }
9740
9741        ExpressionKind::Piecewise(arms) => {
9742            let mut result_type: Option<Arc<LemmaType>> = None;
9743            for (condition, result) in arms {
9744                let condition_type = cached_expression_type(cache, expression.child(*condition));
9745                if !condition_type.is_boolean() && !condition_type.is_undetermined() {
9746                    return Arc::new(LemmaType::undetermined_type());
9747                }
9748                let arm_result_type = cached_expression_type(cache, expression.child(*result));
9749                match &result_type {
9750                    None => result_type = Some(arm_result_type),
9751                    Some(existing) if *existing == arm_result_type => {}
9752                    Some(_)
9753                        if arm_result_type.is_undetermined()
9754                            || result_type.as_ref().is_some_and(|t| t.is_undetermined()) =>
9755                    {
9756                        result_type = Some(Arc::new(LemmaType::undetermined_type()));
9757                    }
9758                    Some(_) => return Arc::new(LemmaType::undetermined_type()),
9759                }
9760            }
9761            result_type.unwrap_or_else(|| Arc::new(LemmaType::undetermined_type()))
9762        }
9763
9764        ExpressionKind::ListCommand { list, op, .. } => infer_list_command_type(
9765            expression,
9766            expression.child(*list),
9767            op,
9768            graph,
9769            resolved_types,
9770            cache,
9771        ),
9772    }
9773}
9774
9775/// Fields of rows of one row spec while the consumer is typed.
9776#[derive(Clone, Copy)]
9777enum RowFields<'g> {
9778    /// Row spec with its own template plan.
9779    Plan(&'g ExecutionPlan),
9780    /// Self-list: rows are the spec being typed.
9781    Root(&'g Graph<'g>),
9782}
9783
9784impl<'g> RowFields<'g> {
9785    /// Member `parts` on rows of `identity` (the rows these fields are of), hop by hop.
9786    fn chain(
9787        &self,
9788        graph: &Graph<'_>,
9789        identity: &SpecIdentity,
9790        parts: &[&str],
9791    ) -> Option<Vec<RowHop>> {
9792        let templates = &graph.instance_lists().templates;
9793        match self {
9794            RowFields::Plan(plan) => instance_list::row_end_chain(
9795                identity,
9796                instance_list::RowEndView::Plan(plan),
9797                templates,
9798                parts,
9799            ),
9800            RowFields::Root(root) => {
9801                // Typing runs after conversion; present rules only (not main_spec
9802                // forward refs). Membership matches the completed plan.
9803                let rules: BTreeSet<RulePath> = root.rules().keys().cloned().collect();
9804                instance_list::row_end_chain(
9805                    identity,
9806                    instance_list::RowEndView::Root {
9807                        data: root.data(),
9808                        rules: &rules,
9809                    },
9810                    templates,
9811                    parts,
9812                )
9813            }
9814        }
9815    }
9816
9817    /// Type of `end`. A rule of the spec being typed reads `computed_rule_types`:
9818    /// typing order puts it first, or it is the recursive rule's seed.
9819    fn end_type(
9820        &self,
9821        end: &RowEnd,
9822        computed_rule_types: &HashMap<RulePath, Arc<LemmaType>>,
9823    ) -> Arc<LemmaType> {
9824        match (self, end) {
9825            (RowFields::Plan(plan), _) => instance_list::row_end_type(plan, end),
9826            (RowFields::Root(graph), RowEnd::Data(path)) => {
9827                if let Some(DataDefinition::Reference {
9828                    target: ReferenceTarget::Rule(rule),
9829                    ..
9830                }) = graph.data().get(path)
9831                {
9832                    return inferred_or_dropped(graph, computed_rule_types, rule);
9833                }
9834                instance_list::row_data_type(graph.data(), path)
9835            }
9836            (RowFields::Root(graph), RowEnd::Rule(rule)) => {
9837                inferred_or_dropped(graph, computed_rule_types, rule)
9838            }
9839        }
9840    }
9841
9842    fn default(&self, path: &DataPath) -> Option<TypedLiteral> {
9843        match self {
9844            RowFields::Plan(plan) => instance_list::default_of(&plan.data, path),
9845            RowFields::Root(graph) => instance_list::default_of(graph.data(), path),
9846        }
9847    }
9848}
9849
9850/// Fields of rows of row spec `identity`. Every instance-list identity has a template
9851/// plan, except a self-list's, whose rows are the spec being typed.
9852fn row_fields<'g>(graph: &'g Graph<'g>, identity: &SpecIdentity) -> Option<RowFields<'g>> {
9853    if *identity == graph.instance_lists().self_identity {
9854        return Some(RowFields::Root(graph));
9855    }
9856    graph
9857        .instance_lists()
9858        .templates
9859        .get(identity)
9860        .map(|plan| RowFields::Plan(plan.as_ref()))
9861}
9862
9863/// Member `path.name` read on a base of spec type: the fields of the spec of the last
9864/// hop and the end read there.
9865fn row_member_end<'g>(
9866    graph: &'g Graph<'g>,
9867    base_type: &LemmaType,
9868    path: &[String],
9869    name: &str,
9870) -> Option<(RowFields<'g>, RowEnd)> {
9871    let TypeSpecification::Spec { spec: identity } = &base_type.specifications else {
9872        return None;
9873    };
9874    let is_row = *identity == graph.instance_lists().self_identity
9875        || graph.instance_lists().templates.contains_key(identity);
9876    if !is_row {
9877        return None;
9878    }
9879    let fields = row_fields(graph, identity)
9880        .expect("BUG: row member identity already checked for a template");
9881    let mut parts: Vec<&str> = path.iter().map(String::as_str).collect();
9882    parts.push(name);
9883    let last = fields
9884        .chain(graph, identity, &parts)?
9885        .pop()
9886        .expect("BUG: a row end chain has a last hop");
9887    let fields = row_fields(graph, &last.identity)
9888        .expect("BUG: every hop of a row end chain has template fields");
9889    Some((fields, last.end))
9890}
9891
9892fn infer_list_command_type(
9893    node: ExprRef<'_, ExpressionKind>,
9894    list: ExprRef<'_, ExpressionKind>,
9895    op: &ListOp,
9896    graph: &Graph,
9897    resolved_types: &ResolvedTypesMap,
9898    cache: &[Arc<LemmaType>],
9899) -> Arc<LemmaType> {
9900    let list_type = cached_expression_type(cache, list);
9901    if list_type.vetoed() {
9902        return Arc::new(LemmaType::veto_type());
9903    }
9904    if list_type.is_undetermined() {
9905        return Arc::new(LemmaType::undetermined_type());
9906    }
9907    if !list_type.is_list() {
9908        return Arc::new(LemmaType::undetermined_type());
9909    }
9910    let instance_rows = list_has_instance_rows(&list_type);
9911    let kind = match op {
9912        ListOp::Count => ListCommandResultKind::Count,
9913        ListOp::First | ListOp::Last | ListOp::Find { .. } => ListCommandResultKind::Element,
9914        ListOp::Reduce {
9915            argument: Some(_), ..
9916        } if instance_rows => ListCommandResultKind::InstanceReduce,
9917        ListOp::Reduce { argument: None, .. } if !instance_rows => ListCommandResultKind::Element,
9918        ListOp::Reduce { .. } => return Arc::new(LemmaType::undetermined_type()),
9919        ListOp::Sort {
9920            argument: Some(_), ..
9921        } if !instance_rows => return Arc::new(LemmaType::undetermined_type()),
9922        ListOp::Predicate { kind, .. } => match kind {
9923            ListPredKind::Any | ListPredKind::All => ListCommandResultKind::BooleanPredicate,
9924            ListPredKind::Where => ListCommandResultKind::KeepList,
9925        },
9926        ListOp::Sort { .. }
9927        | ListOp::Top { .. }
9928        | ListOp::With { .. }
9929        | ListOp::WithRow { .. }
9930        | ListOp::Key { .. } => ListCommandResultKind::KeepList,
9931    };
9932    let first_argument = match op {
9933        ListOp::Reduce {
9934            argument: Some(argument),
9935            ..
9936        } => Some(cached_expression_type(cache, node.child(*argument))),
9937        _ => None,
9938    };
9939    let result = list_command_result_type(
9940        &list_type,
9941        kind,
9942        first_argument.as_ref(),
9943        &arithmetic_scope(resolved_types, graph),
9944    );
9945    if let ListOp::Key { fields } = op {
9946        refine_list_key_type(&result, fields)
9947    } else {
9948        result
9949    }
9950}
9951
9952fn list_element_type(
9953    list_type: &LemmaType,
9954    graph: &Graph,
9955    resolved_types: &ResolvedTypesMap,
9956) -> Arc<LemmaType> {
9957    resolve_anonymous_measure(
9958        list_type.list_element_lemma_type(),
9959        &arithmetic_scope(resolved_types, graph),
9960    )
9961}
9962
9963/// Infer the type of a data reference without producing errors.
9964/// Returns `LemmaType::undetermined_type()` when the data cannot be found. A `uses` slot
9965/// is an instance of its spec.
9966///
9967/// For rule-target references the reference's stored `resolved_type` is still
9968/// the LHS-only placeholder (or fully `undetermined`) at the time
9969/// [`infer_rule_types`] runs — that field is filled by
9970/// [`Graph::resolve_rule_reference_types`] AFTER this pass. We therefore
9971/// look the target rule's inferred type up in `computed_rule_types`.
9972fn infer_data_type(
9973    data_path: &DataPath,
9974    graph: &Graph,
9975    computed_rule_types: &HashMap<RulePath, Arc<LemmaType>>,
9976) -> Arc<LemmaType> {
9977    let Some(definition) = graph.data().get(data_path) else {
9978        return Arc::new(LemmaType::undetermined_type());
9979    };
9980    match definition {
9981        DataDefinition::Value { resolved_type, .. } => Arc::clone(resolved_type),
9982        DataDefinition::TypeDeclaration { resolved_type, .. } => Arc::clone(resolved_type),
9983        DataDefinition::Reference {
9984            target: ReferenceTarget::Rule(target_rule),
9985            resolved_type,
9986            ..
9987        } => {
9988            if !resolved_type.is_undetermined() {
9989                Arc::clone(resolved_type)
9990            } else {
9991                inferred_or_dropped(graph, computed_rule_types, target_rule)
9992            }
9993        }
9994        DataDefinition::Reference { resolved_type, .. }
9995        | DataDefinition::InstanceList { resolved_type, .. } => Arc::clone(resolved_type),
9996        DataDefinition::Import { row, .. } => {
9997            Arc::new(LemmaType::primitive(TypeSpecification::Spec {
9998                spec: row.identity.as_ref().clone(),
9999            }))
10000        }
10001    }
10002}
10003
10004// =============================================================================
10005// Phase 2: Pure type checking (validation only, no mutation, returns Result)
10006// =============================================================================
10007
10008fn engine_error_at_graph(graph: &Graph, source: &Source, message: impl Into<String>) -> Error {
10009    Error::validation_with_context(
10010        message.into(),
10011        Some(source.clone()),
10012        None::<String>,
10013        Some(graph.main_spec),
10014        None,
10015    )
10016}
10017
10018fn check_logical_operands(
10019    graph: &Graph,
10020    connective: ast::LogicalConnective,
10021    left_type: &LemmaType,
10022    right_type: &LemmaType,
10023    source: &Source,
10024) -> Result<(), Vec<Error>> {
10025    if left_type.vetoed() || right_type.vetoed() {
10026        return Ok(());
10027    }
10028    if !left_type.is_boolean() {
10029        return Err(vec![engine_error_at_graph(
10030            graph,
10031            source,
10032            format!(
10033                "Logical {} requires boolean left operand, got {}",
10034                connective.keyword().to_ascii_uppercase(),
10035                left_type
10036            ),
10037        )]);
10038    }
10039    if !right_type.is_boolean() {
10040        return Err(vec![engine_error_at_graph(
10041            graph,
10042            source,
10043            format!(
10044                "Logical {} requires boolean right operand, got {}",
10045                connective.keyword().to_ascii_uppercase(),
10046                right_type
10047            ),
10048        )]);
10049    }
10050    Ok(())
10051}
10052
10053fn check_logical_operand(
10054    graph: &Graph,
10055    operand_type: &LemmaType,
10056    source: &Source,
10057) -> Result<(), Vec<Error>> {
10058    if operand_type.vetoed() {
10059        return Ok(());
10060    }
10061    if !operand_type.is_boolean() {
10062        Err(vec![engine_error_at_graph(
10063            graph,
10064            source,
10065            format!(
10066                "Logical negation requires boolean operand, got {}",
10067                operand_type
10068            ),
10069        )])
10070    } else {
10071        Ok(())
10072    }
10073}
10074
10075fn spec_value_not_operable(
10076    graph: &Graph,
10077    left_type: &LemmaType,
10078    right_type: &LemmaType,
10079    source: &Source,
10080) -> Option<Error> {
10081    let spec_type = if left_type.is_spec() {
10082        Some(left_type)
10083    } else if right_type.is_spec() {
10084        Some(right_type)
10085    } else {
10086        None
10087    };
10088    spec_type.map(|spec_type| {
10089        engine_error_at_graph(
10090            graph,
10091            source,
10092            format!(
10093                "spec instance is not a value here (got {}); read a field or use `is veto`",
10094                spec_type.name()
10095            ),
10096        )
10097    })
10098}
10099
10100fn check_range_containment_types(
10101    graph: &Graph,
10102    value_type: &LemmaType,
10103    range_type: &LemmaType,
10104    source: &Source,
10105) -> Result<(), Vec<Error>> {
10106    if value_type.vetoed() || range_type.vetoed() {
10107        return Ok(());
10108    }
10109    if !range_type.is_range() {
10110        return Err(vec![engine_error_at_graph(
10111            graph,
10112            source,
10113            format!(
10114                "Right side of 'in' must be a range, got type '{}'",
10115                range_type.name()
10116            ),
10117        )]);
10118    }
10119    let compatible = (range_type.is_date_range() && value_type.is_date())
10120        || (range_type.is_time_range() && (value_type.is_time() || value_type.is_date()))
10121        || (range_type.is_number_range() && value_type.is_number())
10122        || (range_type.is_measure_range()
10123            && value_type.is_measure()
10124            && measure_range_matches_measure(range_type, value_type))
10125        || (range_type.is_ratio_range() && value_type.is_ratio())
10126        || (range_type.is_calendar_like_range() && value_type.is_calendar_like());
10127    if !compatible {
10128        return Err(vec![engine_error_at_graph(
10129            graph,
10130            source,
10131            format!(
10132                "Cannot test whether {} is in {}.",
10133                value_type.name(),
10134                range_type.name()
10135            ),
10136        )]);
10137    }
10138    Ok(())
10139}
10140
10141fn check_comparison_types(
10142    graph: &Graph,
10143    left_type: &LemmaType,
10144    op: &ComparisonComputation,
10145    right_type: &LemmaType,
10146    source: &Source,
10147) -> Result<(), Vec<Error>> {
10148    if left_type.vetoed() || right_type.vetoed() {
10149        return Ok(());
10150    }
10151    if let Some(error) = spec_value_not_operable(graph, left_type, right_type, source) {
10152        return Err(vec![error]);
10153    }
10154    let is_equality_only = matches!(op, ComparisonComputation::Is | ComparisonComputation::IsNot);
10155
10156    if left_type.is_range() {
10157        if range_matches_measure_type(left_type, right_type) {
10158            return Ok(());
10159        }
10160        return Err(vec![engine_error_at_graph(
10161            graph,
10162            source,
10163            format!("Cannot compare {} with {}", left_type, right_type),
10164        )]);
10165    }
10166
10167    if left_type.is_boolean() && right_type.is_boolean() {
10168        if !is_equality_only {
10169            return Err(vec![engine_error_at_graph(
10170                graph,
10171                source,
10172                format!("Can only use 'is' and 'is not' with booleans (got {})", op),
10173            )]);
10174        }
10175        return Ok(());
10176    }
10177
10178    if left_type.is_text() && right_type.is_text() {
10179        if !is_equality_only {
10180            return Err(vec![engine_error_at_graph(
10181                graph,
10182                source,
10183                format!("Can only use 'is' and 'is not' with text (got {})", op),
10184            )]);
10185        }
10186        return Ok(());
10187    }
10188
10189    if left_type.is_number() && right_type.is_number() {
10190        return Ok(());
10191    }
10192
10193    if left_type.is_ratio() && right_type.is_ratio() {
10194        return Ok(());
10195    }
10196
10197    if left_type.is_date() && right_type.is_date() {
10198        return Ok(());
10199    }
10200
10201    if left_type.is_time() && right_type.is_time() {
10202        return Ok(());
10203    }
10204
10205    if left_type.is_measure() && right_type.is_measure() {
10206        if left_type.is_anonymous_measure() || right_type.is_anonymous_measure() {
10207            return Err(vec![engine_error_at_graph(
10208                graph,
10209                source,
10210                format!(
10211                    "Cannot compare anonymous measures {} and {}.",
10212                    left_type.name(),
10213                    right_type.name()
10214                ),
10215            )]);
10216        }
10217        let same_decomposition = match (
10218            left_type.measure_type_decomposition(),
10219            right_type.measure_type_decomposition(),
10220        ) {
10221            (Some(ld), Some(rd)) => ld == rd,
10222            _ => false,
10223        };
10224        if !left_type.same_measure_family(right_type)
10225            && !left_type.compatible_with_anonymous_measure(right_type)
10226            && !same_decomposition
10227        {
10228            return Err(vec![engine_error_at_graph(
10229                graph,
10230                source,
10231                format!(
10232                    "Cannot compare unrelated measure types: {} and {}",
10233                    left_type.name(),
10234                    right_type.name()
10235                ),
10236            )]);
10237        }
10238        return Ok(());
10239    }
10240
10241    if left_type.is_duration_like() && right_type.is_duration_like() {
10242        return Ok(());
10243    }
10244    if left_type.is_calendar_like() && right_type.is_calendar_like() {
10245        return Ok(());
10246    }
10247    if left_type.is_calendar_like() && right_type.is_number() {
10248        return Ok(());
10249    }
10250    if left_type.is_number() && right_type.is_calendar_like() {
10251        return Ok(());
10252    }
10253
10254    Err(vec![engine_error_at_graph(
10255        graph,
10256        source,
10257        format!("Cannot compare {} with {}", left_type, right_type),
10258    )])
10259}
10260
10261/// Literal zero on the right of `/` or `%` is rejected at planning time (runtime data divisors may Veto).
10262fn arithmetic_literal_zero_divisor_planning_errors(
10263    graph: &Graph,
10264    right: ExprRef<'_, ExpressionKind>,
10265    operator: &ArithmeticComputation,
10266    source: &Source,
10267) -> Result<(), Vec<Error>> {
10268    if !matches!(
10269        operator,
10270        ArithmeticComputation::Divide | ArithmeticComputation::Modulo
10271    ) {
10272        return Ok(());
10273    }
10274
10275    if let ExpressionKind::Literal(literal) = right.kind() {
10276        if let ValueKind::Number(number) = &literal.value {
10277            if rational_is_zero(number) {
10278                return Err(vec![engine_error_at_graph(
10279                    graph,
10280                    source,
10281                    format!("Cannot apply '{}' with a zero divisor literal.", operator),
10282                )]);
10283            }
10284        }
10285    }
10286
10287    Ok(())
10288}
10289
10290enum IntegerExponent {
10291    NotLiteral,
10292    Value(i32),
10293    OutOfRange,
10294}
10295
10296fn integer_exponent_literal(expr: ExprRef<'_, ExpressionKind>) -> IntegerExponent {
10297    let ExpressionKind::Literal(literal) = expr.kind() else {
10298        return IntegerExponent::NotLiteral;
10299    };
10300    let ValueKind::Number(number) = &literal.value else {
10301        return IntegerExponent::NotLiteral;
10302    };
10303    if !number.is_integer() {
10304        return IntegerExponent::NotLiteral;
10305    }
10306    match number.numer_to_i32() {
10307        Some(exponent) => IntegerExponent::Value(exponent),
10308        None => IntegerExponent::OutOfRange,
10309    }
10310}
10311
10312/// Result type of one arithmetic step. A measure to an integer literal uses
10313/// [`quantity_integer_power_type`]; every other pair uses the shared kernel.
10314fn arithmetic_step_type(
10315    left_type: Arc<LemmaType>,
10316    op: &ArithmeticComputation,
10317    right: ExprRef<'_, ExpressionKind>,
10318    right_type: Arc<LemmaType>,
10319    scope: &MeasureScope<'_>,
10320) -> Arc<LemmaType> {
10321    if *op == ArithmeticComputation::Power && left_type.is_measure() {
10322        match integer_exponent_literal(right) {
10323            IntegerExponent::Value(exponent) => {
10324                if measure_power_scale(left_type.as_ref(), exponent).is_none() {
10325                    return Arc::new(LemmaType::undetermined_type());
10326                }
10327                return quantity_integer_power_type(&left_type, exponent, scope);
10328            }
10329            IntegerExponent::OutOfRange => return Arc::new(LemmaType::undetermined_type()),
10330            IntegerExponent::NotLiteral => {}
10331        }
10332    }
10333    compute_arithmetic_result_type(left_type, op, right_type, scope)
10334}
10335
10336fn measure_power_scale(left: &LemmaType, exponent: i32) -> Option<BaseMeasureVector> {
10337    let decomposition = left
10338        .measure_type_decomposition()
10339        .expect("BUG: measure power requires a resolved decomposition");
10340    scale_decomposition(decomposition, exponent)
10341}
10342
10343fn measure_power_cap_error(
10344    graph: &Graph,
10345    left_type: &LemmaType,
10346    op: &ArithmeticComputation,
10347    right: ExprRef<'_, ExpressionKind>,
10348    source: &Source,
10349) -> Option<Error> {
10350    if *op != ArithmeticComputation::Power || !left_type.is_measure() {
10351        return None;
10352    }
10353    let overflow = match integer_exponent_literal(right) {
10354        IntegerExponent::OutOfRange => true,
10355        IntegerExponent::Value(exponent) => measure_power_scale(left_type, exponent).is_none(),
10356        IntegerExponent::NotLiteral => false,
10357    };
10358    if !overflow {
10359        return None;
10360    }
10361    Some(engine_error_at_graph(
10362        graph,
10363        source,
10364        format!(
10365            "Cannot apply '^' to {} and number: a resulting unit exponent exceeds the maximum of {}.",
10366            left_type.name(),
10367            MAX_UNIT_EXPONENT
10368        ),
10369    ))
10370}
10371
10372fn arithmetic_power_exponent_planning_errors(
10373    graph: &Graph,
10374    right: ExprRef<'_, ExpressionKind>,
10375    left_type: &LemmaType,
10376    operator: &ArithmeticComputation,
10377    source: &Source,
10378) -> Result<(), Vec<Error>> {
10379    if *operator != ArithmeticComputation::Power {
10380        return Ok(());
10381    }
10382    // Measure ^ non-integer-literal is rejected: fractional dimensions are undefined,
10383    // and variable exponents cannot be statically verified to be integers at plan time.
10384    if left_type.is_measure() || left_type.is_duration_like() {
10385        let is_integer_literal = if let ExpressionKind::Literal(lit) = right.kind() {
10386            if let crate::planning::semantics::ValueKind::Number(n) = &lit.value {
10387                n.is_integer()
10388            } else {
10389                false
10390            }
10391        } else {
10392            false
10393        };
10394        if !is_integer_literal {
10395            return Err(vec![engine_error_at_graph(
10396                graph,
10397                source,
10398                "Cannot raise a measure value to a fractional or variable exponent. Use a positive integer literal.".to_string(),
10399            )]);
10400        }
10401    }
10402    Ok(())
10403}
10404
10405/// Discharges planning obligations for numeric arithmetic beyond type compatibility:
10406/// literal zero divisors and integer power exponents where required.
10407fn arithmetic_plan_time_exactness_planning_errors(
10408    graph: &Graph,
10409    right: ExprRef<'_, ExpressionKind>,
10410    left_type: &LemmaType,
10411    right_type: &LemmaType,
10412    operator: &ArithmeticComputation,
10413    source: &Source,
10414) -> Result<(), Vec<Error>> {
10415    if left_type.vetoed() || right_type.vetoed() {
10416        return Ok(());
10417    }
10418    if left_type.is_undetermined() || right_type.is_undetermined() {
10419        return Ok(());
10420    }
10421
10422    let mut errors = Vec::new();
10423    let collect = |result: Result<(), Vec<Error>>, errors: &mut Vec<Error>| {
10424        if let Err(mut errs) = result {
10425            errors.append(&mut errs);
10426        }
10427    };
10428
10429    collect(
10430        arithmetic_literal_zero_divisor_planning_errors(graph, right, operator, source),
10431        &mut errors,
10432    );
10433    collect(
10434        arithmetic_power_exponent_planning_errors(graph, right, left_type, operator, source),
10435        &mut errors,
10436    );
10437
10438    if errors.is_empty() {
10439        Ok(())
10440    } else {
10441        Err(errors)
10442    }
10443}
10444
10445fn operand_forbids_modulo(ty: &LemmaType) -> bool {
10446    ty.is_ratio() || ty.is_measure() || ty.is_duration_like() || ty.is_calendar_like()
10447}
10448
10449fn check_arithmetic_types(
10450    graph: &Graph,
10451    left_type: &LemmaType,
10452    right_type: &LemmaType,
10453    operator: &ArithmeticComputation,
10454    source: &Source,
10455) -> Result<(), Vec<Error>> {
10456    if left_type.vetoed() || right_type.vetoed() {
10457        return Ok(());
10458    }
10459    if let Some(error) = spec_value_not_operable(graph, left_type, right_type, source) {
10460        return Err(vec![error]);
10461    }
10462
10463    if left_type.is_range() || right_type.is_range() {
10464        let range_measure_allowed = matches!(
10465            operator,
10466            ArithmeticComputation::Add | ArithmeticComputation::Subtract
10467        ) && ((left_type.is_range()
10468            && right_type.is_range()
10469            && range_matches_range_measure(left_type, right_type))
10470            || (left_type.is_range()
10471                && !right_type.is_range()
10472                && range_matches_measure_type(left_type, right_type))
10473            || (right_type.is_range()
10474                && !left_type.is_range()
10475                && range_matches_measure_type(right_type, left_type)));
10476        if range_measure_allowed {
10477            return Ok(());
10478        }
10479
10480        return Err(vec![engine_error_at_graph(
10481            graph,
10482            source,
10483            format!(
10484                "Cannot apply '{}' to {} and {}.",
10485                operator,
10486                left_type.name(),
10487                right_type.name()
10488            ),
10489        )]);
10490    }
10491
10492    // Date/Time arithmetic is limited to supported temporal combinations.
10493    if left_type.is_date() || left_type.is_time() || right_type.is_date() || right_type.is_time() {
10494        if matches!(operator, ArithmeticComputation::Subtract) {
10495            if left_type.is_date() && right_type.is_date() {
10496                return Err(vec![engine_error_at_graph(
10497                    graph,
10498                    source,
10499                    "Cannot subtract dates. Use a date range instead: `start...end as day` or `start...end as year`.".to_string(),
10500                )]);
10501            }
10502            if left_type.is_time() && right_type.is_time() {
10503                return Err(vec![engine_error_at_graph(
10504                    graph,
10505                    source,
10506                    "Cannot subtract times. Use a datetime range instead: `start...end as hour` or `start...end as second`.".to_string(),
10507                )]);
10508            }
10509        }
10510
10511        let left_is_duration_like = left_type.is_duration_like();
10512        let right_is_duration_like = right_type.is_duration_like();
10513        let valid = matches!(
10514            (
10515                left_type.is_date(),
10516                left_type.is_time(),
10517                right_type.is_date(),
10518                right_type.is_time(),
10519                left_is_duration_like,
10520                right_is_duration_like,
10521                left_type.is_calendar_like(),
10522                right_type.is_calendar_like(),
10523                operator
10524            ),
10525            (
10526                true,
10527                _,
10528                _,
10529                true,
10530                _,
10531                _,
10532                _,
10533                _,
10534                ArithmeticComputation::Subtract
10535            ) | (
10536                true,
10537                _,
10538                _,
10539                _,
10540                _,
10541                true,
10542                _,
10543                _,
10544                ArithmeticComputation::Add | ArithmeticComputation::Subtract
10545            ) | (
10546                true,
10547                _,
10548                _,
10549                _,
10550                _,
10551                _,
10552                _,
10553                true,
10554                ArithmeticComputation::Add | ArithmeticComputation::Subtract
10555            ) | (_, _, true, _, true, _, _, _, ArithmeticComputation::Add)
10556                | (_, _, true, _, _, _, true, _, ArithmeticComputation::Add)
10557                | (
10558                    _,
10559                    true,
10560                    _,
10561                    _,
10562                    _,
10563                    true,
10564                    _,
10565                    _,
10566                    ArithmeticComputation::Add | ArithmeticComputation::Subtract
10567                )
10568                | (_, _, _, true, true, _, _, _, ArithmeticComputation::Add)
10569        );
10570        if !valid {
10571            return Err(vec![engine_error_at_graph(
10572                graph,
10573                source,
10574                format!(
10575                    "Cannot apply '{}' to {} and {}.",
10576                    operator,
10577                    left_type.name(),
10578                    right_type.name()
10579                ),
10580            )]);
10581        }
10582        return Ok(());
10583    }
10584
10585    // Measure/Measure rules:
10586    //   +/- requires same measure family (dimensionless addition is not meaningful otherwise).
10587    //   *   requires different measure families (same-family measure*measure is rejected; use `as number`).
10588    //   /   is allowed for all families (same-family → scalar Number; cross-family → anonymous measure).
10589    //   %   and ^ on two Quantities are always rejected.
10590    if left_type.is_measure() && right_type.is_measure() {
10591        if let (
10592            ArithmeticComputation::Multiply | ArithmeticComputation::Divide,
10593            Some(left_decomposition),
10594            Some(right_decomposition),
10595        ) = (
10596            operator,
10597            left_type.measure_type_decomposition(),
10598            right_type.measure_type_decomposition(),
10599        ) {
10600            if semantics::combine_decompositions(
10601                left_decomposition,
10602                right_decomposition,
10603                matches!(operator, ArithmeticComputation::Multiply),
10604            )
10605            .is_none()
10606            {
10607                return Err(vec![engine_error_at_graph(
10608                    graph,
10609                    source,
10610                    format!(
10611                        "Cannot apply '{}' to {} and {}: a resulting unit exponent exceeds the maximum of {}.",
10612                        operator,
10613                        left_type.name(),
10614                        right_type.name(),
10615                        MAX_UNIT_EXPONENT
10616                    ),
10617                )]);
10618            }
10619        }
10620        return match operator {
10621            ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
10622                if left_type.same_measure_family(right_type)
10623                    || left_type.compatible_with_anonymous_measure(right_type)
10624                {
10625                    Ok(())
10626                } else {
10627                    Err(vec![engine_error_at_graph(
10628                        graph,
10629                        source,
10630                        format!(
10631                            "Cannot {} unrelated measure types: {} and {}.",
10632                            if matches!(operator, ArithmeticComputation::Add) {
10633                                "add"
10634                            } else {
10635                                "subtract"
10636                            },
10637                            left_type.name(),
10638                            right_type.name()
10639                        ),
10640                    )])
10641                }
10642            }
10643            ArithmeticComputation::Multiply => {
10644                // Measure * Measure (same or cross family) → anonymous intermediate when
10645                // dimensions combine; promotion resolves a unique named type when possible.
10646                Ok(())
10647            }
10648            ArithmeticComputation::Divide => {
10649                // Measure / Measure (any family) → scalar Number or anonymous intermediate. Allowed.
10650                Ok(())
10651            }
10652            ArithmeticComputation::Modulo | ArithmeticComputation::Power => {
10653                Err(vec![engine_error_at_graph(
10654                    graph,
10655                    source,
10656                    format!(
10657                        "Cannot apply '{}' to two measure values ({} and {}).",
10658                        operator,
10659                        left_type.name(),
10660                        right_type.name()
10661                    ),
10662                )])
10663            }
10664        };
10665    }
10666
10667    // Duration * Duration (and power/modulo) rejected for same reason.
10668    if left_type.is_duration_like() && right_type.is_duration_like() {
10669        return match operator {
10670            ArithmeticComputation::Add | ArithmeticComputation::Subtract => Ok(()),
10671            ArithmeticComputation::Divide => Ok(()),
10672            _ => Err(vec![engine_error_at_graph(
10673                graph,
10674                source,
10675                "Cannot multiply two duration values. Convert operands first: 'value as number'."
10676                    .to_string(),
10677            )]),
10678        };
10679    }
10680
10681    if left_type.is_calendar_like() && right_type.is_calendar_like() {
10682        return match operator {
10683            ArithmeticComputation::Add | ArithmeticComputation::Subtract => Ok(()),
10684            ArithmeticComputation::Divide => Ok(()),
10685            _ => Err(vec![engine_error_at_graph(
10686                graph,
10687                source,
10688                "Cannot multiply two calendar values. Convert operands first: 'value as number'."
10689                    .to_string(),
10690            )]),
10691        };
10692    }
10693
10694    if (left_type.is_duration_like() && right_type.is_calendar_like())
10695        || (left_type.is_calendar_like() && right_type.is_duration_like())
10696    {
10697        return Err(vec![engine_error_at_graph(
10698            graph,
10699            source,
10700            format!(
10701                "Cannot apply '{}' to {} and {}. Duration and calendar are unrelated types.",
10702                operator,
10703                left_type.name(),
10704                right_type.name()
10705            ),
10706        )]);
10707    }
10708
10709    // Only Measure, Number, Ratio, Duration, and Calendar can participate in arithmetic
10710    let left_valid = left_type.is_measure()
10711        || left_type.is_number()
10712        || left_type.is_duration_like()
10713        || left_type.is_calendar_like()
10714        || left_type.is_ratio();
10715    let right_valid = right_type.is_measure()
10716        || right_type.is_number()
10717        || right_type.is_duration_like()
10718        || right_type.is_calendar_like()
10719        || right_type.is_ratio();
10720
10721    if !left_valid || !right_valid {
10722        return Err(vec![engine_error_at_graph(
10723            graph,
10724            source,
10725            format!(
10726                "Cannot apply '{}' to {} and {}.",
10727                operator,
10728                left_type.name(),
10729                right_type.name()
10730            ),
10731        )]);
10732    }
10733
10734    // `mod` is numbers only. This is before the same-base success return, because
10735    // two ratios share a base type and would otherwise never reach the Modulo arm.
10736    if matches!(operator, ArithmeticComputation::Modulo)
10737        && (operand_forbids_modulo(left_type) || operand_forbids_modulo(right_type))
10738    {
10739        return Err(vec![engine_error_at_graph(
10740            graph,
10741            source,
10742            format!(
10743                "Cannot apply modulo to {} and {}. `mod` is defined for numbers only.",
10744                left_type.name(),
10745                right_type.name()
10746            ),
10747        )]);
10748    }
10749
10750    // Operator-specific constraints (same base type is always allowed)
10751    if left_type.has_same_base_type(right_type) {
10752        return Ok(());
10753    }
10754
10755    struct OperandPair<'t> {
10756        left_type: &'t LemmaType,
10757        right_type: &'t LemmaType,
10758    }
10759    impl OperandPair<'_> {
10760        fn check(&self, a: impl Fn(&LemmaType) -> bool, b: impl Fn(&LemmaType) -> bool) -> bool {
10761            (a(self.left_type) && b(self.right_type)) || (b(self.left_type) && a(self.right_type))
10762        }
10763    }
10764    let pair = OperandPair {
10765        left_type,
10766        right_type,
10767    };
10768
10769    let allowed = match operator {
10770        ArithmeticComputation::Multiply => {
10771            pair.check(LemmaType::is_measure, LemmaType::is_number)
10772                || pair.check(LemmaType::is_measure, LemmaType::is_ratio)
10773                || pair.check(LemmaType::is_measure, LemmaType::is_duration_like_measure)
10774                || pair.check(LemmaType::is_measure, LemmaType::is_calendar_like)
10775                || pair.check(LemmaType::is_duration_like_measure, LemmaType::is_number)
10776                || pair.check(LemmaType::is_duration_like_measure, LemmaType::is_ratio)
10777                || pair.check(LemmaType::is_calendar_like, LemmaType::is_number)
10778                || pair.check(LemmaType::is_calendar_like, LemmaType::is_ratio)
10779                || pair.check(LemmaType::is_number, LemmaType::is_ratio)
10780        }
10781        ArithmeticComputation::Divide => {
10782            pair.check(LemmaType::is_measure, LemmaType::is_number)
10783                || (left_type.is_measure() && right_type.is_ratio())
10784                || pair.check(LemmaType::is_measure, LemmaType::is_duration_like_measure)
10785                || pair.check(LemmaType::is_measure, LemmaType::is_calendar_like)
10786                || (left_type.is_duration_like() && right_type.is_number())
10787                || (left_type.is_duration_like() && right_type.is_ratio())
10788                || (left_type.is_calendar_like() && right_type.is_number())
10789                || (left_type.is_calendar_like() && right_type.is_ratio())
10790                || (left_type.is_number() && right_type.is_duration_like())
10791                || (left_type.is_number() && right_type.is_calendar_like())
10792                || pair.check(LemmaType::is_number, LemmaType::is_ratio)
10793        }
10794        ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
10795            if pair.check(LemmaType::is_ratio, |t: &LemmaType| {
10796                t.is_number() || t.is_measure() || t.is_duration_like() || t.is_calendar_like()
10797            }) {
10798                return Err(vec![engine_error_at_graph(
10799                    graph,
10800                    source,
10801                    format!(
10802                        "Cannot apply '{}' to {} and {}. \
10803                         Adding or subtracting a ratio is ambiguous; \
10804                         scale explicitly, e.g. 'price - discount * price' \
10805                         or 'price * (100% - discount)'.",
10806                        operator,
10807                        left_type.name(),
10808                        right_type.name(),
10809                    ),
10810                )]);
10811            }
10812            false
10813        }
10814        ArithmeticComputation::Power => {
10815            // Exponent must be a dimensionless integer for measure left (enforced separately
10816            // in arithmetic_power_exponent_planning_errors). Measure ^ Ratio is rejected here.
10817            // number ^ (number | ratio) is allowed; exact rational result or runtime Veto.
10818            let left_ok = left_type.is_number()
10819                || left_type.is_measure()
10820                || left_type.is_ratio()
10821                || left_type.is_duration_like();
10822            let right_ok = if left_type.is_measure() || left_type.is_duration_like() {
10823                right_type.is_number()
10824            } else {
10825                right_type.is_number() || right_type.is_ratio()
10826            };
10827            left_ok && right_ok
10828        }
10829        ArithmeticComputation::Modulo => left_type.is_number() && right_type.is_number(),
10830    };
10831
10832    if !allowed {
10833        return Err(vec![engine_error_at_graph(
10834            graph,
10835            source,
10836            format!(
10837                "Cannot apply '{}' to {} and {}.",
10838                operator,
10839                left_type.name(),
10840                right_type.name(),
10841            ),
10842        )]);
10843    }
10844
10845    Ok(())
10846}
10847
10848fn literal_has_explicit_unit(lit: &semantics::TypedLiteral) -> bool {
10849    match &lit.value {
10850        ValueKind::Measure(_) => {
10851            let sig = lit.lemma_type.measure_runtime_signature();
10852            sig.len() == 1 && sig[0].1 == 1 && !sig[0].0.is_empty()
10853        }
10854        ValueKind::Ratio(_) => lit.lemma_type.ratio_primary_unit().is_some(),
10855        _ => false,
10856    }
10857}
10858
10859fn has_explicit_unit(source: ExprRef<'_, ExpressionKind>) -> bool {
10860    match source.kind() {
10861        ExpressionKind::Literal(lit) => literal_has_explicit_unit(lit),
10862        ExpressionKind::UnitConversion(_, SemanticConversionTarget::Unit { .. }) => true,
10863        ExpressionKind::StepRounding {
10864            step: Some(step), ..
10865        } => match source.child(*step).kind() {
10866            ExpressionKind::Literal(lit) => literal_has_explicit_unit(lit),
10867            _ => false,
10868        },
10869        _ => false,
10870    }
10871}
10872
10873/// `operand -> round` needs a number. `operand -> round step` needs a positive literal step
10874/// of the operand's kind.
10875fn check_step_rounding(
10876    graph: &Graph,
10877    mode: semantics::RoundingMode,
10878    operand: ExprRef<'_, ExpressionKind>,
10879    operand_type: &LemmaType,
10880    step: Option<ExprRef<'_, ExpressionKind>>,
10881    step_type: Option<&Arc<LemmaType>>,
10882    source: &Source,
10883) -> Result<(), Vec<Error>> {
10884    if operand_type.vetoed() || operand_type.is_undetermined() {
10885        return Ok(());
10886    }
10887    let Some(step) = step else {
10888        if operand_type.is_number() {
10889            return Ok(());
10890        }
10891        let label = expression_suggestion_label(operand);
10892        if operand_type.is_anonymous_measure() {
10893            let decomposition = operand_type
10894                .measure_type_decomposition()
10895                .expect("BUG: anonymous measure carries a decomposition");
10896            return Err(vec![engine_error_at_graph(
10897                graph,
10898                source,
10899                format!(
10900                    "Cannot use '{mode}' on '{label}'. Round to a step instead: write \
10901                     '{label} -> {mode}' followed by a step in a unit with dimensions {decomposition:?}"
10902                ),
10903            )]);
10904        }
10905        if operand_type.is_measure() || operand_type.is_ratio() {
10906            let kind = if operand_type.is_measure() {
10907                "measure"
10908            } else {
10909                "ratio"
10910            };
10911            let unit = first_unit_suggestion(operand_type);
10912            return Err(vec![engine_error_at_graph(
10913                graph,
10914                source,
10915                format!(
10916                    "Cannot use '{mode}' on {kind} '{}'. Round to a step instead, for example \
10917                     '{label} -> {mode} 1 {unit}'",
10918                    operand_type.name()
10919                ),
10920            )]);
10921        }
10922        return Err(vec![engine_error_at_graph(
10923            graph,
10924            source,
10925            format!(
10926                "Cannot use '{mode}' on {}; write '{label} -> {mode}' only for a number, \
10927                 or give a step of the same kind",
10928                operand_type.name()
10929            ),
10930        )]);
10931    };
10932    let step_type = step_type
10933        .expect("BUG: step carries a type when present")
10934        .as_ref();
10935    if step_type.vetoed() || step_type.is_undetermined() {
10936        return Ok(());
10937    }
10938    let ExpressionKind::Literal(literal) = step.kind() else {
10939        return Err(vec![engine_error_at_graph(
10940            graph,
10941            source,
10942            format!(
10943                "The step of '-> {mode}' must be a literal, not '{}'",
10944                expression_suggestion_label(step)
10945            ),
10946        )]);
10947    };
10948    if !step_rounding_compatible(operand_type, step_type) {
10949        let hint = match (
10950            operand_type.is_anonymous_measure(),
10951            operand_type.measure_unit_names(),
10952        ) {
10953            (false, Some(units)) if !units.is_empty() => format!(
10954                ". Write the step in a unit of {}, for example '-> {mode} 1 {}'",
10955                operand_type.name(),
10956                units[0]
10957            ),
10958            _ => String::new(),
10959        };
10960        return Err(vec![engine_error_at_graph(
10961            graph,
10962            source,
10963            format!(
10964                "Cannot round {} to a step of {}{hint}",
10965                operand_type.name(),
10966                step_type.name()
10967            ),
10968        )]);
10969    }
10970    let positive = match &literal.value {
10971        ValueKind::Number(step)
10972        | ValueKind::Ratio(step)
10973        | ValueKind::Measure(step)
10974        | ValueKind::UnscaledMeasure {
10975            magnitude: step, ..
10976        } => step.numer_is_positive(),
10977        other => unreachable!("BUG: step type is number, ratio or measure, got {other:?}"),
10978    };
10979    if !positive {
10980        return Err(vec![engine_error_at_graph(
10981            graph,
10982            source,
10983            format!("The step of '-> {mode}' must be greater than zero"),
10984        )]);
10985    }
10986    Ok(())
10987}
10988
10989enum SuggestionPiece<'a> {
10990    Node(ExprRef<'a, ExpressionKind>),
10991    Text(String),
10992}
10993
10994/// Pieces of `first op operand ...` over `steps`, in pop order.
10995fn arithmetic_chain_pieces<'a>(
10996    node: ExprRef<'a, ExpressionKind>,
10997    first: ExprId,
10998    steps: &[ast::ChainStep<ArithmeticComputation>],
10999    pending: &mut Vec<SuggestionPiece<'a>>,
11000) {
11001    for step in steps.iter().rev() {
11002        pending.push(SuggestionPiece::Node(node.child(step.operand)));
11003        pending.push(SuggestionPiece::Text(format!(" {} ", step.op)));
11004    }
11005    pending.push(SuggestionPiece::Node(node.child(first)));
11006}
11007
11008fn expression_suggestion_label(expression: ExprRef<'_, ExpressionKind>) -> String {
11009    suggestion_label(vec![SuggestionPiece::Node(expression)])
11010}
11011
11012fn chain_prefix_suggestion_label(
11013    expression: ExprRef<'_, ExpressionKind>,
11014    first: ExprId,
11015    prefix: &[ast::ChainStep<ArithmeticComputation>],
11016) -> String {
11017    let mut pending = Vec::new();
11018    arithmetic_chain_pieces(expression, first, prefix, &mut pending);
11019    suggestion_label(pending)
11020}
11021
11022fn suggestion_label(mut pending: Vec<SuggestionPiece<'_>>) -> String {
11023    use SuggestionPiece as Piece;
11024    let mut out = String::new();
11025    while let Some(piece) = pending.pop() {
11026        match piece {
11027            Piece::Text(text) => out.push_str(&text),
11028            Piece::Node(node) => match node.kind() {
11029                ExpressionKind::DataPath(p) => out.push_str(&p.data),
11030                ExpressionKind::RulePath(p) => out.push_str(&p.rule),
11031                ExpressionKind::SpecMember { base, path, name } => {
11032                    pending.push(Piece::Text(format!(".{name}")));
11033                    for segment in path.iter().rev() {
11034                        pending.push(Piece::Text(format!(".{segment}")));
11035                    }
11036                    pending.push(Piece::Node(node.child(*base)));
11037                }
11038                ExpressionKind::Arithmetic { first, steps } => {
11039                    arithmetic_chain_pieces(node, *first, steps, &mut pending);
11040                }
11041                ExpressionKind::UnitConversion(inner, target) => {
11042                    pending.push(Piece::Text(format!(" as {target}")));
11043                    pending.push(Piece::Node(node.child(*inner)));
11044                }
11045                ExpressionKind::Literal(lit) => match &lit.value {
11046                    ValueKind::Number(n) => out.push_str(&n.display_str()),
11047                    _ => out.push_str("<value>"),
11048                },
11049                _ => out.push_str("<expr>"),
11050            },
11051        }
11052    }
11053    out
11054}
11055
11056fn first_unit_suggestion(source_type: &LemmaType) -> String {
11057    source_type
11058        .measure_unit_names()
11059        .or_else(|| source_type.ratio_unit_names())
11060        .and_then(|names| names.first().map(|u| (*u).to_string()))
11061        .unwrap_or_else(|| "<unit>".to_string())
11062}
11063
11064fn lookup_unit_type(
11065    resolved_types: &ResolvedTypesMap,
11066    spec: &LemmaSpec,
11067    unit_name: &str,
11068) -> Option<Arc<LemmaType>> {
11069    find_types_by_spec(resolved_types, spec)
11070        .and_then(|dt| dt.unit_index.resolve(unit_name).ok())
11071        .map(|(_, owner)| owner)
11072}
11073
11074fn is_valid_range_span_unit(
11075    source_type: &LemmaType,
11076    unit_name: &str,
11077    unit_index: &UnitIndex,
11078) -> bool {
11079    let Ok((bare, target_type)) = unit_index.resolve(unit_name) else {
11080        return false;
11081    };
11082    let unit_name = bare.as_str();
11083    if source_type.is_date_range() {
11084        return target_type.is_duration_like() || target_type.is_calendar_like();
11085    }
11086    if source_type.is_time_range() {
11087        return target_type.is_duration_like();
11088    }
11089    if source_type.is_measure_range() {
11090        if source_type
11091            .measure_unit_names()
11092            .is_some_and(|names| names.contains(&unit_name))
11093        {
11094            return target_type.is_measure();
11095        }
11096        let TypeSpecification::MeasureRange { decomposition, .. } = &source_type.specifications
11097        else {
11098            unreachable!("BUG: is_measure_range without MeasureRange spec");
11099        };
11100        let Some(source_decomp) = decomposition else {
11101            return false;
11102        };
11103        return target_type.is_measure()
11104            && target_type
11105                .measure_type_decomposition()
11106                .is_some_and(|td| td == source_decomp);
11107    }
11108    if source_type.is_ratio_range() {
11109        return target_type.is_ratio();
11110    }
11111    false
11112}
11113
11114fn check_unit_conversion_types(
11115    graph: &Graph,
11116    source: ExprRef<'_, ExpressionKind>,
11117    source_type: &LemmaType,
11118    target: &SemanticConversionTarget,
11119    resolved_types: &ResolvedTypesMap,
11120    source_loc: &Source,
11121    spec: &LemmaSpec,
11122) -> Result<(), Vec<Error>> {
11123    if source_type.vetoed() {
11124        return Ok(());
11125    }
11126    if source_type.is_spec() {
11127        return Err(vec![engine_error_at_graph(
11128            graph,
11129            source_loc,
11130            format!(
11131                "spec instance is not a value here (got {}); read a field or use `is veto`",
11132                source_type.name()
11133            ),
11134        )]);
11135    }
11136    let unit_index = find_types_by_spec(resolved_types, spec)
11137        .map(|dt| &dt.unit_index)
11138        .expect("BUG: spec types missing during unit conversion check");
11139
11140    match target {
11141        SemanticConversionTarget::Type(ConversionTypeKind::Time) => {
11142            if source_type.is_date() || source_type.is_time() {
11143                Ok(())
11144            } else {
11145                Err(vec![engine_error_at_graph(
11146                    graph,
11147                    source_loc,
11148                    format!("Cannot convert {} to time.", source_type.name()),
11149                )])
11150            }
11151        }
11152        SemanticConversionTarget::Type(ConversionTypeKind::Boolean) => {
11153            if source_type.is_boolean() {
11154                Ok(())
11155            } else {
11156                Err(vec![engine_error_at_graph(
11157                    graph,
11158                    source_loc,
11159                    format!("Cannot convert {} to boolean.", source_type.name()),
11160                )])
11161            }
11162        }
11163        SemanticConversionTarget::Unit {
11164            unit_name,
11165            owning_type,
11166        } => {
11167            if source_type.is_number() {
11168                if owning_type_declares_unit_name(owning_type, unit_name) {
11169                    return Ok(());
11170                }
11171                return Err(vec![engine_error_at_graph(
11172                    graph,
11173                    source_loc,
11174                    format!("Unknown unit '{unit_name}' in spec '{}'.", spec.name),
11175                )]);
11176            }
11177            if source_type.is_measure() {
11178                let target_type = owning_type.as_ref();
11179                if source_type.same_measure_family(target_type)
11180                    || source_type.compatible_with_anonymous_measure(target_type)
11181                    || target_type.compatible_with_anonymous_measure(source_type)
11182                {
11183                    return Ok(());
11184                }
11185                if !has_explicit_unit(source) {
11186                    let expr_label = expression_suggestion_label(source);
11187                    let first_unit = first_unit_suggestion(source_type);
11188                    return Err(vec![engine_error_at_graph(
11189                        graph,
11190                        source_loc,
11191                        format!(
11192                            "Cannot convert '{}' to '{unit_name}' (different measure families). \
11193                             Express the source unit first, for example '{expr_label} as {first_unit} as {unit_name}'.",
11194                            source_type.name()
11195                        ),
11196                    )]);
11197                }
11198                return Ok(());
11199            }
11200            if source_type.is_date_range()
11201                || source_type.is_time_range()
11202                || source_type.is_measure_range()
11203                || source_type.is_ratio_range()
11204            {
11205                if is_valid_range_span_unit(source_type, unit_name, unit_index) {
11206                    return Ok(());
11207                }
11208                return Err(vec![engine_error_at_graph(
11209                    graph,
11210                    source_loc,
11211                    format!(
11212                        "Cannot convert {} span to unit '{unit_name}'.",
11213                        source_type.name()
11214                    ),
11215                )]);
11216            }
11217            if source_type.is_calendar_like() {
11218                if owning_type_declares_unit_name(owning_type, unit_name) {
11219                    return Ok(());
11220                }
11221                return Err(vec![engine_error_at_graph(
11222                    graph,
11223                    source_loc,
11224                    format!("Cannot convert calendar to unit '{unit_name}'."),
11225                )]);
11226            }
11227            if source_type.is_ratio() {
11228                // Target unit comes from the resolved owner (builtin percent/permille or
11229                // an in-scope custom). Source need not declare that unit; builtins bridge.
11230                if owning_type_declares_unit_name(owning_type, unit_name) {
11231                    return Ok(());
11232                }
11233                let valid = match &owning_type.specifications {
11234                    TypeSpecification::Ratio { units, .. } => units
11235                        .0
11236                        .iter()
11237                        .map(|u| u.name.as_str())
11238                        .collect::<Vec<_>>()
11239                        .join(", "),
11240                    _ => String::new(),
11241                };
11242                return Err(vec![engine_error_at_graph(
11243                    graph,
11244                    source_loc,
11245                    format!(
11246                        "Unknown unit '{unit_name}' for ratio type '{}'. Valid units: {}",
11247                        owning_type.name(),
11248                        valid
11249                    ),
11250                )]);
11251            }
11252            Err(vec![engine_error_at_graph(
11253                graph,
11254                source_loc,
11255                format!(
11256                    "Cannot convert {} to unit '{unit_name}'.",
11257                    source_type.name()
11258                ),
11259            )])
11260        }
11261        SemanticConversionTarget::Type(ConversionTypeKind::Number) => {
11262            if source_type.is_date_range()
11263                || source_type.is_time_range()
11264                || source_type.is_measure_range()
11265            {
11266                return Err(vec![engine_error_at_graph(
11267                    graph,
11268                    source_loc,
11269                    format!(
11270                        "Cannot use 'as number' on a {}. \
11271                         Express the span in a unit first, for example '<range> as <unit> as number'.",
11272                        source_type.name()
11273                    ),
11274                )]);
11275            }
11276            if source_type.is_number_range() {
11277                return Ok(());
11278            }
11279            if source_type.is_ratio_range() || source_type.is_calendar_like_range() {
11280                return Err(vec![engine_error_at_graph(
11281                    graph,
11282                    source_loc,
11283                    format!("Cannot convert {} to number.", source_type.name()),
11284                )]);
11285            }
11286            if source_type.is_anonymous_measure() {
11287                if let Some(decomp) = source_type.measure_type_decomposition() {
11288                    if !decomp.is_empty() {
11289                        return Err(vec![engine_error_at_graph(
11290                            graph,
11291                            source_loc,
11292                            format!(
11293                                "Cannot use 'as number' to strip an anonymous intermediate with unresolved \
11294                                 dimensions {:?}. Ensure all dimensions cancel before converting to number.",
11295                                decomp
11296                            ),
11297                        )]);
11298                    }
11299                }
11300            }
11301            if source_type.is_measure() && !source_type.is_anonymous_measure() {
11302                if !has_explicit_unit(source) {
11303                    let expr_label = expression_suggestion_label(source);
11304                    let first_unit = first_unit_suggestion(source_type);
11305                    return Err(vec![engine_error_at_graph(
11306                        graph,
11307                        source_loc,
11308                        format!(
11309                            "Cannot use 'as number' on measure '{}' without a unit. \
11310                             Express it in a unit first, for example '{expr_label} as {first_unit} as number'.",
11311                            source_type.name()
11312                        ),
11313                    )]);
11314                }
11315                return Ok(());
11316            }
11317            if source_type.is_measure()
11318                || source_type.is_number()
11319                || source_type.is_duration_like()
11320                || source_type.is_calendar_like()
11321                || source_type.is_ratio()
11322            {
11323                Ok(())
11324            } else {
11325                Err(vec![engine_error_at_graph(
11326                    graph,
11327                    source_loc,
11328                    format!("Cannot convert {} to number.", source_type.name()),
11329                )])
11330            }
11331        }
11332        SemanticConversionTarget::Type(target_kind)
11333            if source_type.matches_primitive_kind(target_kind.to_primitive_kind()) =>
11334        {
11335            Ok(())
11336        }
11337        SemanticConversionTarget::Type(target_kind) => Err(vec![engine_error_at_graph(
11338            graph,
11339            source_loc,
11340            format!(
11341                "Cannot convert {} to {:?}.",
11342                source_type.name(),
11343                target_kind
11344            ),
11345        )]),
11346    }
11347}
11348fn check_mathematical_operand(
11349    graph: &Graph,
11350    op: &semantics::MathematicalComputation,
11351    operand_type: &LemmaType,
11352    source: &Source,
11353) -> Result<(), Vec<Error>> {
11354    if operand_type.vetoed() {
11355        return Ok(());
11356    }
11357    if operand_type.is_spec() {
11358        return Err(vec![engine_error_at_graph(
11359            graph,
11360            source,
11361            format!(
11362                "spec instance is not a value here (got {}); read a field or use `is veto`",
11363                operand_type.name()
11364            ),
11365        )]);
11366    }
11367    if operand_type.is_number() {
11368        return Ok(());
11369    }
11370    if (operand_type.is_measure() || operand_type.is_ratio())
11371        && mathematical_computation_applies_to_magnitude(op)
11372    {
11373        return Ok(());
11374    }
11375    let message = if operand_type.is_measure() {
11376        format!(
11377            "Mathematical function '{op}' cannot be applied to {operand_type}; use 'as number' first"
11378        )
11379    } else {
11380        format!("Mathematical function '{op}' requires number operand, got {operand_type}")
11381    };
11382    Err(vec![engine_error_at_graph(graph, source, message)])
11383}
11384
11385/// Check that all rule references in the graph point to existing rules.
11386fn check_all_rule_references_exist(graph: &Graph) -> Result<(), Vec<Error>> {
11387    let mut errors = Vec::new();
11388    for (rule_path, rule_node) in graph.rules() {
11389        for dependency in &rule_node.depends_on_rules {
11390            match graph.rule_presence(dependency) {
11391                RulePresence::Present | RulePresence::Dropped => {}
11392                RulePresence::Absent => {
11393                    errors.push(engine_error_at_graph(
11394                        graph,
11395                        &rule_node.source,
11396                        format!(
11397                            "Rule '{}' references non-existent rule '{}'",
11398                            rule_path.rule, dependency.rule
11399                        ),
11400                    ));
11401                }
11402            }
11403        }
11404    }
11405    if errors.is_empty() {
11406        Ok(())
11407    } else {
11408        Err(errors)
11409    }
11410}
11411
11412/// Check that no data and rule share the same name in the same spec.
11413fn check_data_and_rule_name_collisions(graph: &Graph) -> Result<(), Vec<Error>> {
11414    let mut errors = Vec::new();
11415    for rule_path in graph.rules().keys() {
11416        let data_path = DataPath::new(rule_path.segments.clone(), rule_path.rule.clone());
11417        if graph.data().contains_key(&data_path) {
11418            let rule_node = graph.rules().get(rule_path).unwrap_or_else(|| {
11419                unreachable!(
11420                    "BUG: rule '{}' missing from graph while validating name collisions",
11421                    rule_path.rule
11422                )
11423            });
11424            errors.push(engine_error_at_graph(
11425                graph,
11426                &rule_node.source,
11427                format!(
11428                    "Name collision: '{}' is defined as both a data and a rule",
11429                    data_path
11430                ),
11431            ));
11432        }
11433    }
11434    if errors.is_empty() {
11435        Ok(())
11436    } else {
11437        Err(errors)
11438    }
11439}
11440
11441/// Check that a data reference names data of this spec. A `uses` slot is the instance.
11442fn check_data_reference(
11443    data_path: &DataPath,
11444    graph: &Graph,
11445    data_source: &Source,
11446) -> Result<(), Vec<Error>> {
11447    if graph.data().contains_key(data_path) {
11448        return Ok(());
11449    }
11450    Err(vec![engine_error_at_graph(
11451        graph,
11452        data_source,
11453        format!("Unknown data reference '{}'", data_path),
11454    )])
11455}
11456
11457/// One step of a type check over an expression tree.
11458enum CheckStep {
11459    /// Errors found at the node, reported in order.
11460    Report(Vec<Error>),
11461    /// Check the child at this id, with its steps in order.
11462    Visit(ExprId),
11463}
11464
11465/// Traversal stacks of [`check_expression`], reused across calls. Empty between calls.
11466#[derive(Default)]
11467struct CheckScratch {
11468    pending: Vec<CheckStep>,
11469    steps: Vec<CheckStep>,
11470}
11471
11472/// Ordered steps of one node's check: local errors interleaved with child visits.
11473struct CheckSteps<'s> {
11474    steps: &'s mut Vec<CheckStep>,
11475    errors: Vec<Error>,
11476}
11477
11478impl<'s> CheckSteps<'s> {
11479    /// Steps are appended to `steps`.
11480    fn new(steps: &'s mut Vec<CheckStep>) -> Self {
11481        Self {
11482            steps,
11483            errors: Vec::new(),
11484        }
11485    }
11486
11487    fn push(&mut self, error: Error) {
11488        self.errors.push(error);
11489    }
11490
11491    fn collect(&mut self, result: Result<(), Vec<Error>>) {
11492        if let Err(errs) = result {
11493            self.errors.extend(errs);
11494        }
11495    }
11496
11497    fn visit(&mut self, child: &ExprId) {
11498        self.flush();
11499        self.steps.push(CheckStep::Visit(*child));
11500    }
11501
11502    fn splice(&mut self, steps: Vec<CheckStep>) {
11503        self.flush();
11504        self.steps.extend(steps);
11505    }
11506
11507    fn flush(&mut self) {
11508        if !self.errors.is_empty() {
11509            self.steps
11510                .push(CheckStep::Report(std::mem::take(&mut self.errors)));
11511        }
11512    }
11513
11514    fn finish(mut self) {
11515        self.flush();
11516    }
11517}
11518
11519/// Type errors of the expression at `expression` and its subtree.
11520///
11521/// Children are visited depth-first in source order with an explicit stack; each node's
11522/// steps come from [`check_steps`].
11523fn check_expression(
11524    expression: ExprRef<'_, ExpressionKind>,
11525    graph: &Graph,
11526    inferred_types: &HashMap<RulePath, Arc<LemmaType>>,
11527    resolved_types: &ResolvedTypesMap,
11528    spec: &LemmaSpec,
11529    cache: &mut InferExpressionTypeCache,
11530    scratch: &mut CheckScratch,
11531) -> Result<(), Vec<Error>> {
11532    let mut errors = Vec::new();
11533    let CheckScratch { pending, steps } = scratch;
11534    pending.push(CheckStep::Visit(expression.id));
11535    while let Some(step) = pending.pop() {
11536        match step {
11537            CheckStep::Report(found) => errors.extend(found),
11538            CheckStep::Visit(id) => {
11539                check_steps(
11540                    expression.child(id),
11541                    graph,
11542                    inferred_types,
11543                    resolved_types,
11544                    spec,
11545                    cache,
11546                    steps,
11547                );
11548                pending.extend(steps.drain(..).rev());
11549            }
11550        }
11551    }
11552    if errors.is_empty() {
11553        Ok(())
11554    } else {
11555        Err(errors)
11556    }
11557}
11558
11559/// Steps of the check of one node, not including its children's subtrees.
11560fn check_steps(
11561    expression: ExprRef<'_, ExpressionKind>,
11562    graph: &Graph,
11563    inferred_types: &HashMap<RulePath, Arc<LemmaType>>,
11564    resolved_types: &ResolvedTypesMap,
11565    spec: &LemmaSpec,
11566    cache: &mut InferExpressionTypeCache,
11567    steps: &mut Vec<CheckStep>,
11568) {
11569    let mut checks = CheckSteps::new(steps);
11570    match expression.kind() {
11571        ExpressionKind::Literal(_) => {}
11572
11573        ExpressionKind::DataPath(data_path) => {
11574            let data_source = expression
11575                .source()
11576                .expect("BUG: expression missing source in check_expression");
11577            checks.collect(check_data_reference(data_path, graph, data_source));
11578        }
11579
11580        ExpressionKind::RulePath(_) => {}
11581
11582        ExpressionKind::SpecMember { base, name, path } => {
11583            let expr_source = expression
11584                .source()
11585                .expect("BUG: expression missing source in check_expression");
11586            checks.visit(base);
11587            let base_type = infer_expression_type(
11588                expression.child(*base),
11589                graph,
11590                inferred_types,
11591                resolved_types,
11592                cache,
11593            );
11594            if !base_type.is_spec() && !base_type.is_undetermined() && !base_type.vetoed() {
11595                let message = match expression.child(*base).kind() {
11596                    ExpressionKind::RulePath(rule) => {
11597                        format!("rule '{rule}' does not return a spec")
11598                    }
11599                    _ => format!(
11600                        "`.{name}` reads a field of a spec value, but the operand is {}",
11601                        base_type.name()
11602                    ),
11603                };
11604                checks.push(engine_error_at_graph(graph, expr_source, message));
11605            } else if base_type.is_spec() && row_member_end(graph, &base_type, path, name).is_none()
11606            {
11607                checks.push(engine_error_at_graph(
11608                    graph,
11609                    expr_source,
11610                    format!(
11611                        "no data, rule or uses '{name}' on spec {}",
11612                        base_type.name()
11613                    ),
11614                ));
11615            }
11616        }
11617
11618        ExpressionKind::RowField { .. } | ExpressionKind::RowValue { .. } => {}
11619
11620        ExpressionKind::Logical {
11621            connective,
11622            first,
11623            steps,
11624        } => {
11625            checks.visit(first);
11626            let mut left_type = infer_expression_type(
11627                expression.child(*first),
11628                graph,
11629                inferred_types,
11630                resolved_types,
11631                cache,
11632            );
11633            for step in steps {
11634                checks.visit(&step.operand);
11635                let right_type = infer_expression_type(
11636                    expression.child(step.operand),
11637                    graph,
11638                    inferred_types,
11639                    resolved_types,
11640                    cache,
11641                );
11642                checks.collect(check_logical_operands(
11643                    graph,
11644                    *connective,
11645                    &left_type,
11646                    &right_type,
11647                    &step.source_location,
11648                ));
11649                left_type = logical_connective_type(&left_type, &right_type);
11650            }
11651        }
11652
11653        ExpressionKind::LogicalNegation(operand, _) => {
11654            checks.visit(operand);
11655
11656            let operand_type = infer_expression_type(
11657                expression.child(*operand),
11658                graph,
11659                inferred_types,
11660                resolved_types,
11661                cache,
11662            );
11663            let expr_source = expression
11664                .source()
11665                .expect("BUG: expression missing source in check_expression");
11666            checks.collect(check_logical_operand(graph, &operand_type, expr_source));
11667        }
11668
11669        ExpressionKind::Comparison(left, op, right) => {
11670            checks.visit(left);
11671            checks.visit(right);
11672
11673            let left_type = infer_expression_type(
11674                expression.child(*left),
11675                graph,
11676                inferred_types,
11677                resolved_types,
11678                cache,
11679            );
11680            let right_type = infer_expression_type(
11681                expression.child(*right),
11682                graph,
11683                inferred_types,
11684                resolved_types,
11685                cache,
11686            );
11687            let expr_source = expression
11688                .source()
11689                .expect("BUG: expression missing source in check_expression");
11690            checks.collect(check_comparison_types(
11691                graph,
11692                &left_type,
11693                op,
11694                &right_type,
11695                expr_source,
11696            ));
11697        }
11698
11699        ExpressionKind::Arithmetic { first, steps } => {
11700            checks.visit(first);
11701            let scope = arithmetic_scope(resolved_types, graph);
11702            let mut left_type = infer_expression_type(
11703                expression.child(*first),
11704                graph,
11705                inferred_types,
11706                resolved_types,
11707                cache,
11708            );
11709            for (index, step) in steps.iter().enumerate() {
11710                let operator = &step.op;
11711                let right = &step.operand;
11712
11713                // Detect `left OP (inner as Unit{..})` where OP is *, /, or % and check whether
11714                // the `as Unit` was meant to convert the operand or the result of the arithmetic.
11715                // When the operand conversion is invalid but the result conversion would be valid,
11716                // emit a targeted error with a parenthesized suggestion instead of the generic
11717                // "different measure families" message that `check_unit_conversion_types` would emit.
11718                //
11719                // Three outcomes:
11720                //   CheckedInlineConversionValid   — conversion on operand is valid; right was
11721                //                                    checked inline; arithmetic checks must run.
11722                //   CheckedConversionErrorEmitted  — conversion error (targeted or standard)
11723                //                                    already emitted; arithmetic checks must be
11724                //                                    skipped to avoid confusing secondary errors.
11725                //   NotHandledInline               — pattern did not match; fall through to the
11726                //                                    normal `check_expression(right)` call.
11727                enum InlineConversionOutcome {
11728                    CheckedInlineConversionValid,
11729                    CheckedConversionErrorEmitted,
11730                    NotHandledInline,
11731                }
11732
11733                let is_multiplicative = matches!(
11734                    operator,
11735                    ArithmeticComputation::Multiply
11736                        | ArithmeticComputation::Divide
11737                        | ArithmeticComputation::Modulo
11738                );
11739
11740                let right_outcome = if is_multiplicative {
11741                    if let ExpressionKind::UnitConversion(
11742                        inner_source,
11743                        conversion_target @ SemanticConversionTarget::Unit { unit_name, .. },
11744                    ) = expression.child(*right).kind()
11745                    {
11746                        // Always descend into the inner source to catch errors in sub-expressions.
11747                        checks.visit(inner_source);
11748
11749                        let inner_type = infer_expression_type(
11750                            expression.child(*inner_source),
11751                            graph,
11752                            inferred_types,
11753                            resolved_types,
11754                            cache,
11755                        );
11756                        let expr_source = &step.source_location;
11757
11758                        let target_type_opt = lookup_unit_type(resolved_types, spec, unit_name);
11759
11760                        if let Some(target_type) = &target_type_opt {
11761                            let inner_is_valid_conversion_source = inner_type.is_measure()
11762                                && (inner_type.same_measure_family(target_type)
11763                                    || inner_type.compatible_with_anonymous_measure(target_type)
11764                                    || target_type.compatible_with_anonymous_measure(&inner_type));
11765
11766                            if inner_is_valid_conversion_source {
11767                                // The parse tree is correct: `left OP (inner as unit)`.
11768                                // Run check_unit_conversion_types to catch explicit-unit requirements,
11769                                // then let arithmetic checks run on the full `left OP right` below.
11770                                checks.collect(check_unit_conversion_types(
11771                                    graph,
11772                                    expression.child(*inner_source),
11773                                    &inner_type,
11774                                    conversion_target,
11775                                    resolved_types,
11776                                    expr_source,
11777                                    spec,
11778                                ));
11779                                InlineConversionOutcome::CheckedInlineConversionValid
11780                            } else if inner_type.is_measure() || inner_type.is_number() {
11781                                // Conversion on the operand fails. Check whether converting the
11782                                // result of the whole arithmetic expression would be valid instead.
11783                                let combined_type = compute_arithmetic_result_type(
11784                                    Arc::clone(&left_type),
11785                                    operator,
11786                                    Arc::clone(&inner_type),
11787                                    &arithmetic_scope(resolved_types, graph),
11788                                );
11789                                let combined_is_valid_conversion_source = combined_type
11790                                    .is_measure()
11791                                    && (combined_type.same_measure_family(target_type)
11792                                        || combined_type
11793                                            .compatible_with_anonymous_measure(target_type)
11794                                        || target_type
11795                                            .compatible_with_anonymous_measure(&combined_type));
11796
11797                                if combined_is_valid_conversion_source {
11798                                    let inner_label = expression_suggestion_label(
11799                                        expression.child(*inner_source),
11800                                    );
11801                                    let left_label = chain_prefix_suggestion_label(
11802                                        expression,
11803                                        *first,
11804                                        &steps[..index],
11805                                    );
11806                                    checks.push(engine_error_at_graph(
11807                                        graph,
11808                                        expr_source,
11809                                        format!(
11810                                            "'as {unit_name}' converts '{inner_label}' here, not \
11811                                             the result of the expression. Write \
11812                                             '({left_label} {operator} {inner_label}) as {unit_name}' \
11813                                             to convert the result.",
11814                                        ),
11815                                    ));
11816                                } else {
11817                                    // Neither interpretation is valid. Emit the standard conversion
11818                                    // error so the user knows the unit is incompatible.
11819                                    checks.collect(check_unit_conversion_types(
11820                                        graph,
11821                                        expression.child(*inner_source),
11822                                        &inner_type,
11823                                        conversion_target,
11824                                        resolved_types,
11825                                        expr_source,
11826                                        spec,
11827                                    ));
11828                                }
11829                                InlineConversionOutcome::CheckedConversionErrorEmitted
11830                            } else {
11831                                // Inner is not measure/number (e.g. text, boolean, date). Fall through
11832                                // to standard path so check_expression(right) runs normally.
11833                                InlineConversionOutcome::NotHandledInline
11834                            }
11835                        } else {
11836                            // Unknown unit — fall through so check_expression(right) emits the standard
11837                            // "Unknown unit" error from check_unit_conversion_types.
11838                            InlineConversionOutcome::NotHandledInline
11839                        }
11840                    } else {
11841                        InlineConversionOutcome::NotHandledInline
11842                    }
11843                } else {
11844                    InlineConversionOutcome::NotHandledInline
11845                };
11846
11847                match right_outcome {
11848                    InlineConversionOutcome::NotHandledInline => {
11849                        checks.visit(right);
11850                    }
11851                    InlineConversionOutcome::CheckedInlineConversionValid
11852                    | InlineConversionOutcome::CheckedConversionErrorEmitted => {
11853                        // Right child was already checked inline; do not descend again.
11854                    }
11855                }
11856
11857                let run_arithmetic_checks = matches!(
11858                    right_outcome,
11859                    InlineConversionOutcome::NotHandledInline
11860                        | InlineConversionOutcome::CheckedInlineConversionValid
11861                );
11862
11863                if run_arithmetic_checks {
11864                    let right_type = infer_expression_type(
11865                        expression.child(*right),
11866                        graph,
11867                        inferred_types,
11868                        resolved_types,
11869                        cache,
11870                    );
11871                    let expr_source = &step.source_location;
11872                    checks.collect(check_arithmetic_types(
11873                        graph,
11874                        &left_type,
11875                        &right_type,
11876                        operator,
11877                        expr_source,
11878                    ));
11879                    checks.collect(arithmetic_plan_time_exactness_planning_errors(
11880                        graph,
11881                        expression.child(*right),
11882                        &left_type,
11883                        &right_type,
11884                        operator,
11885                        expr_source,
11886                    ));
11887                    if let Some(error) = measure_power_cap_error(
11888                        graph,
11889                        &left_type,
11890                        operator,
11891                        expression.child(*right),
11892                        expr_source,
11893                    ) {
11894                        checks.push(error);
11895                    }
11896                }
11897                let right_expr = expression.child(step.operand);
11898                let right_type =
11899                    infer_expression_type(right_expr, graph, inferred_types, resolved_types, cache);
11900                left_type =
11901                    arithmetic_step_type(left_type, operator, right_expr, right_type, &scope);
11902            }
11903        }
11904
11905        ExpressionKind::UnitConversion(source_expression, target) => {
11906            checks.visit(source_expression);
11907
11908            let source_type = infer_expression_type(
11909                expression.child(*source_expression),
11910                graph,
11911                inferred_types,
11912                resolved_types,
11913                cache,
11914            );
11915            let expr_source = expression
11916                .source()
11917                .expect("BUG: expression missing source in check_expression");
11918            checks.collect(check_unit_conversion_types(
11919                graph,
11920                expression.child(*source_expression),
11921                &source_type,
11922                target,
11923                resolved_types,
11924                expr_source,
11925                spec,
11926            ));
11927        }
11928
11929        ExpressionKind::MathematicalComputation(op, operand) => {
11930            checks.visit(operand);
11931
11932            let operand_type = infer_expression_type(
11933                expression.child(*operand),
11934                graph,
11935                inferred_types,
11936                resolved_types,
11937                cache,
11938            );
11939            let expr_source = expression
11940                .source()
11941                .expect("BUG: expression missing source in check_expression");
11942            checks.collect(check_mathematical_operand(
11943                graph,
11944                op,
11945                &operand_type,
11946                expr_source,
11947            ));
11948        }
11949
11950        ExpressionKind::StepRounding {
11951            operand,
11952            mode,
11953            step,
11954        } => {
11955            checks.visit(operand);
11956            if let Some(step) = step {
11957                checks.visit(step);
11958            }
11959            let operand_type = infer_expression_type(
11960                expression.child(*operand),
11961                graph,
11962                inferred_types,
11963                resolved_types,
11964                cache,
11965            );
11966            let step_type = step.map(|step| {
11967                infer_expression_type(
11968                    expression.child(step),
11969                    graph,
11970                    inferred_types,
11971                    resolved_types,
11972                    cache,
11973                )
11974            });
11975            let expr_source = expression
11976                .source()
11977                .expect("BUG: expression missing source in check_expression");
11978            checks.collect(check_step_rounding(
11979                graph,
11980                *mode,
11981                expression.child(*operand),
11982                &operand_type,
11983                step.map(|step| expression.child(step)),
11984                step_type.as_ref(),
11985                expr_source,
11986            ));
11987        }
11988
11989        ExpressionKind::RangeBound(bound, operand) => {
11990            checks.visit(operand);
11991
11992            let operand_type = infer_expression_type(
11993                expression.child(*operand),
11994                graph,
11995                inferred_types,
11996                resolved_types,
11997                cache,
11998            );
11999            if !operand_type.vetoed() && !operand_type.is_undetermined() && !operand_type.is_range()
12000            {
12001                let expr_source = expression
12002                    .source()
12003                    .expect("BUG: expression missing source in check_expression");
12004                checks.push(engine_error_at_graph(
12005                    graph,
12006                    expr_source,
12007                    format!(
12008                        "'{bound}' requires a range, got type '{}'",
12009                        operand_type.name()
12010                    ),
12011                ));
12012            }
12013        }
12014
12015        ExpressionKind::Veto(_) => {}
12016
12017        ExpressionKind::ResultIsVeto(operand) => {
12018            checks.visit(operand);
12019        }
12020
12021        ExpressionKind::Now => {}
12022
12023        ExpressionKind::DateRelative(_, date_expr) => {
12024            checks.visit(date_expr);
12025
12026            let date_type = infer_expression_type(
12027                expression.child(*date_expr),
12028                graph,
12029                inferred_types,
12030                resolved_types,
12031                cache,
12032            );
12033            if !date_type.vetoed() && !date_type.is_date() {
12034                let expr_source = expression
12035                    .source()
12036                    .expect("BUG: expression missing source in check_expression");
12037                checks.push(engine_error_at_graph(
12038                    graph,
12039                    expr_source,
12040                    format!(
12041                        "Date sugar 'in past/future' requires a date expression, got type '{}'",
12042                        date_type
12043                    ),
12044                ));
12045            }
12046        }
12047
12048        ExpressionKind::DateCalendar(_, _, date_expr) => {
12049            checks.visit(date_expr);
12050
12051            let date_type = infer_expression_type(
12052                expression.child(*date_expr),
12053                graph,
12054                inferred_types,
12055                resolved_types,
12056                cache,
12057            );
12058            if !date_type.vetoed() && !date_type.is_date() {
12059                let expr_source = expression
12060                    .source()
12061                    .expect("BUG: expression missing source in check_expression");
12062                checks.push(engine_error_at_graph(
12063                    graph,
12064                    expr_source,
12065                    format!(
12066                        "Calendar sugar requires a date expression, got type '{}'",
12067                        date_type
12068                    ),
12069                ));
12070            }
12071        }
12072
12073        ExpressionKind::RangeLiteral(left, right) => {
12074            checks.visit(left);
12075            checks.visit(right);
12076
12077            let left_type = infer_expression_type(
12078                expression.child(*left),
12079                graph,
12080                inferred_types,
12081                resolved_types,
12082                cache,
12083            );
12084            let right_type = infer_expression_type(
12085                expression.child(*right),
12086                graph,
12087                inferred_types,
12088                resolved_types,
12089                cache,
12090            );
12091            let expr_source = expression
12092                .source()
12093                .expect("BUG: expression missing source in check_expression");
12094
12095            if !(left_type.vetoed() || right_type.vetoed()) {
12096                let inferred_range_type =
12097                    infer_range_type_from_endpoint_types(&left_type, &right_type);
12098                if inferred_range_type.is_undetermined() {
12099                    checks.push(engine_error_at_graph(
12100                        graph,
12101                        expr_source,
12102                        format!(
12103                            "Cannot create a range from {} and {}.",
12104                            left_type.name(),
12105                            right_type.name()
12106                        ),
12107                    ));
12108                } else if inferred_range_type.is_time_range() {
12109                    if let (ExpressionKind::Literal(left_lit), ExpressionKind::Literal(right_lit)) = (
12110                        expression.child(*left).kind(),
12111                        expression.child(*right).kind(),
12112                    ) {
12113                        if let (ValueKind::Time(left_time), ValueKind::Time(right_time)) =
12114                            (&left_lit.value, &right_lit.value)
12115                        {
12116                            if !time_range_endpoints_share_timezone(left_time, right_time) {
12117                                checks.push(engine_error_at_graph(
12118                                    graph,
12119                                    expr_source,
12120                                    "Time range endpoints must use the same timezone".to_string(),
12121                                ));
12122                            }
12123                        }
12124                    }
12125                }
12126            }
12127        }
12128
12129        ExpressionKind::PastFutureRange(_, offset_expr) => {
12130            checks.visit(offset_expr);
12131
12132            let offset_type = infer_expression_type(
12133                expression.child(*offset_expr),
12134                graph,
12135                inferred_types,
12136                resolved_types,
12137                cache,
12138            );
12139            if !offset_type.vetoed()
12140                && !offset_type.is_duration_like()
12141                && !offset_type.is_calendar_like()
12142            {
12143                let expr_source = expression
12144                    .source()
12145                    .expect("BUG: expression missing source in check_expression");
12146                checks.push(engine_error_at_graph(
12147                    graph,
12148                    expr_source,
12149                    format!(
12150                        "Past/future range requires a duration or calendar expression, got type '{}'",
12151                        offset_type.name()
12152                    ),
12153                ));
12154            }
12155        }
12156
12157        ExpressionKind::RangeContainment(value, range) => {
12158            checks.visit(value);
12159            checks.visit(range);
12160
12161            let value_type = infer_expression_type(
12162                expression.child(*value),
12163                graph,
12164                inferred_types,
12165                resolved_types,
12166                cache,
12167            );
12168            let range_type = infer_expression_type(
12169                expression.child(*range),
12170                graph,
12171                inferred_types,
12172                resolved_types,
12173                cache,
12174            );
12175            let expr_source = expression
12176                .source()
12177                .expect("BUG: expression missing source in check_expression");
12178
12179            checks.collect(check_range_containment_types(
12180                graph,
12181                &value_type,
12182                &range_type,
12183                expr_source,
12184            ));
12185        }
12186
12187        ExpressionKind::Piecewise(arms) => {
12188            for (condition, result) in arms {
12189                checks.visit(condition);
12190                checks.visit(result);
12191                let condition_type = infer_expression_type(
12192                    expression.child(*condition),
12193                    graph,
12194                    inferred_types,
12195                    resolved_types,
12196                    cache,
12197                );
12198                if !condition_type.vetoed() && !condition_type.is_boolean() {
12199                    let expr_source = expression
12200                        .child(*condition)
12201                        .source()
12202                        .expect("BUG: expression missing source in check_expression");
12203                    checks.push(engine_error_at_graph(
12204                        graph,
12205                        expr_source,
12206                        format!(
12207                            "Piecewise condition must be boolean, got type '{}'",
12208                            condition_type.name()
12209                        ),
12210                    ));
12211                }
12212            }
12213        }
12214
12215        ExpressionKind::ListCommand { list, op, .. } => {
12216            checks.visit(list);
12217            let list_type = infer_expression_type(
12218                expression.child(*list),
12219                graph,
12220                inferred_types,
12221                resolved_types,
12222                cache,
12223            );
12224            let expr_source = expression
12225                .source()
12226                .expect("BUG: expression missing source in check_expression");
12227            if !list_type.is_list() && !list_type.is_undetermined() && !list_type.vetoed() {
12228                checks.push(engine_error_at_graph(
12229                    graph,
12230                    expr_source,
12231                    format!("`->` commands require a list, got {}", list_type.name()),
12232                ));
12233            }
12234            let mut typing = TypingScope {
12235                graph,
12236                inferred_types,
12237                resolved_types,
12238                cache,
12239            };
12240            checks.splice(check_list_op(
12241                op,
12242                expression,
12243                expression.child(*list),
12244                &list_type,
12245                &mut typing,
12246                expr_source,
12247            ));
12248        }
12249    }
12250
12251    checks.finish();
12252}
12253
12254fn check_list_op(
12255    op: &ListOp,
12256    node: ExprRef<'_, ExpressionKind>,
12257    list: ExprRef<'_, ExpressionKind>,
12258    list_type: &LemmaType,
12259    typing: &mut TypingScope<'_>,
12260    source: &Source,
12261) -> Vec<CheckStep> {
12262    let TypingScope {
12263        graph,
12264        inferred_types,
12265        resolved_types,
12266        ..
12267    } = *typing;
12268    if let TypeSpecification::List { element, key, .. } = &list_type.specifications {
12269        if let TypeSpecification::Spec { spec: identity } = &element.specifications {
12270            return check_instance_list_op(op, node, list, identity, key, typing, source);
12271        }
12272    }
12273    let mut steps = Vec::new();
12274    let mut checks = CheckSteps::new(&mut steps);
12275    match op {
12276        ListOp::First | ListOp::Last | ListOp::Count => {}
12277        ListOp::Find { keys } => {
12278            for key in keys {
12279                checks.visit(key);
12280            }
12281            if keys.len() != 1 {
12282                checks.push(engine_error_at_graph(
12283                    graph,
12284                    source,
12285                    "numbered `-> find` takes one row number".to_string(),
12286                ));
12287            } else {
12288                checks.collect(check_numbered_find_key(node.child(keys[0]), graph, source));
12289            }
12290        }
12291        ListOp::Reduce { argument, .. } => {
12292            if let Some(argument) = argument {
12293                checks.visit(argument);
12294                checks.push(engine_error_at_graph(
12295                    graph,
12296                    source,
12297                    "value lists do not take a field argument; write `fares -> sum`".to_string(),
12298                ));
12299            }
12300        }
12301        ListOp::Predicate { form, expr, .. } => {
12302            checks.visit(expr);
12303            if *form == ast::ListPredicateForm::Instance {
12304                checks.push(engine_error_at_graph(
12305                    graph,
12306                    source,
12307                    "value-list `any`/`all`/`where` take a comparison (`>= 100 eur`), not a field expression"
12308                        .to_string(),
12309                ));
12310            }
12311        }
12312        ListOp::Sort { argument, .. } => {
12313            if let Some(argument) = argument {
12314                checks.visit(argument);
12315                checks.push(engine_error_at_graph(
12316                    graph,
12317                    source,
12318                    "value-list `sort` takes `asc` or `desc`, not a field".to_string(),
12319                ));
12320            }
12321        }
12322        ListOp::Top { n } => {
12323            checks.visit(n);
12324            checks.collect(check_top_count(
12325                node.child(*n),
12326                graph,
12327                inferred_types,
12328                resolved_types,
12329                source,
12330                typing.cache,
12331            ));
12332        }
12333        ListOp::With { rows } => {
12334            for row in rows {
12335                checks.visit(row);
12336            }
12337        }
12338        ListOp::WithRow { .. } => {
12339            unreachable!("BUG: `-> with field: value` converted on a value list")
12340        }
12341        ListOp::Key { .. } => {
12342            checks.push(engine_error_at_graph(
12343                graph,
12344                source,
12345                "`-> key` is for instance lists".to_string(),
12346            ));
12347        }
12348    }
12349    if list_type.is_list() {
12350        let element_type = list_element_type(list_type, graph, resolved_types);
12351        checks.collect(check_list_element_op(
12352            op,
12353            &element_type,
12354            node,
12355            typing,
12356            source,
12357        ));
12358    }
12359    checks.finish();
12360    steps
12361}
12362
12363fn check_top_count(
12364    n: ExprRef<'_, ExpressionKind>,
12365    graph: &Graph,
12366    inferred_types: &HashMap<RulePath, Arc<LemmaType>>,
12367    resolved_types: &ResolvedTypesMap,
12368    source: &Source,
12369    cache: &mut InferExpressionTypeCache,
12370) -> Result<(), Vec<Error>> {
12371    let error = |message: String| Err(vec![engine_error_at_graph(graph, source, message)]);
12372    match n.kind() {
12373        ExpressionKind::Literal(literal) => match &literal.value {
12374            ValueKind::Number(count) => {
12375                let reduced = RationalInteger::try_reduce_ref(count)
12376                    .map_err(|failure: NumericFailure| {
12377                        vec![engine_error_at_graph(graph, source, failure.to_string())]
12378                    })?;
12379                if reduced.is_integer() && !reduced.numer_is_negative() {
12380                    Ok(())
12381                } else {
12382                    error(format!(
12383                        "`top` takes a whole number >= 0, got {}",
12384                        count.display_str()
12385                    ))
12386                }
12387            }
12388            _ => error(format!("`top` takes a number, got {literal}")),
12389        },
12390        ExpressionKind::DataPath(_) | ExpressionKind::RulePath(_) => {
12391            let n_type = infer_expression_type(n, graph, inferred_types, resolved_types, cache);
12392            if n_type.is_number() || n_type.is_undetermined() || n_type.vetoed() {
12393                Ok(())
12394            } else {
12395                error(format!("`top` takes a number, got {}", n_type.name()))
12396            }
12397        }
12398        _ => error(
12399            "`top` takes a number literal or a data or rule reference; compute other counts in a rule"
12400                .to_string(),
12401        ),
12402    }
12403}
12404
12405/// Row-type restrictions of value-list commands.
12406fn check_list_element_op(
12407    op: &ListOp,
12408    element_type: &LemmaType,
12409    node: ExprRef<'_, ExpressionKind>,
12410    typing: &mut TypingScope<'_>,
12411    source: &Source,
12412) -> Result<(), Vec<Error>> {
12413    let graph = typing.graph;
12414    if element_type.is_undetermined() || element_type.vetoed() {
12415        return Ok(());
12416    }
12417    let error = |message: String| Err(vec![engine_error_at_graph(graph, source, message)]);
12418    let summable = element_type.is_numeric() || element_type.is_ratio();
12419    let ordered = summable || element_type.is_date() || element_type.is_time();
12420    match op {
12421        ListOp::Reduce {
12422            kind,
12423            argument: None,
12424        } => match kind {
12425            ListReduce::Sum | ListReduce::Avg if !summable => error(format!(
12426                "`{}` takes number, measure or ratio rows, got {}",
12427                kind.keyword(),
12428                element_type.name()
12429            )),
12430            ListReduce::Product if element_type.is_measure() => error(format!(
12431                "`product` of {} rows would have a unit that depends on the row count; multiply in a rule instead",
12432                element_type.name()
12433            )),
12434            ListReduce::Product if !(element_type.is_number() || element_type.is_ratio()) => {
12435                error(format!(
12436                    "`product` takes number or ratio rows, got {}",
12437                    element_type.name()
12438                ))
12439            }
12440            ListReduce::Min | ListReduce::Max if !ordered => error(format!(
12441                "`{}` needs ordered rows (number, measure, ratio, date, time), got {}",
12442                kind.keyword(),
12443                element_type.name()
12444            )),
12445            _ => Ok(()),
12446        },
12447        ListOp::Sort { argument: None, .. } if !ordered => error(format!(
12448            "`sort` needs ordered rows (number, measure, ratio, date, time), got {}",
12449            element_type.name()
12450        )),
12451        ListOp::Predicate { expr, .. } => {
12452            let predicate_type = typing.infer(node.child(*expr));
12453            if predicate_type.is_boolean()
12454                || predicate_type.is_undetermined()
12455                || predicate_type.vetoed()
12456            {
12457                Ok(())
12458            } else {
12459                error(format!(
12460                    "list predicate needs a boolean expression, got {}",
12461                    predicate_type.name()
12462                ))
12463            }
12464        }
12465        ListOp::Find { keys } if keys.len() == 1 => {
12466            let key_type = typing.infer(node.child(keys[0]));
12467            if key_type.is_number() || key_type.is_undetermined() || key_type.vetoed() {
12468                Ok(())
12469            } else {
12470                error(format!(
12471                    "numbered `find` takes a row number, got {}",
12472                    key_type.name()
12473                ))
12474            }
12475        }
12476        _ => Ok(()),
12477    }
12478}
12479/// Inputs every typing check reads, plus the inference memo they share.
12480struct TypingScope<'t> {
12481    graph: &'t Graph<'t>,
12482    inferred_types: &'t HashMap<RulePath, Arc<LemmaType>>,
12483    resolved_types: &'t ResolvedTypesMap<'t>,
12484    cache: &'t mut InferExpressionTypeCache,
12485}
12486
12487impl TypingScope<'_> {
12488    fn infer(&mut self, expression: ExprRef<'_, ExpressionKind>) -> Arc<LemmaType> {
12489        infer_expression_type(
12490            expression,
12491            self.graph,
12492            self.inferred_types,
12493            self.resolved_types,
12494            self.cache,
12495        )
12496    }
12497
12498    fn error(&self, source: &Source, message: impl Into<String>) -> Error {
12499        engine_error_at_graph(self.graph, source, message)
12500    }
12501}
12502
12503/// Commands on a list whose rows are instances of `identity`.
12504fn check_instance_list_op(
12505    op: &ListOp,
12506    node: ExprRef<'_, ExpressionKind>,
12507    list: ExprRef<'_, ExpressionKind>,
12508    identity: &SpecIdentity,
12509    key: &ListKey,
12510    typing: &mut TypingScope<'_>,
12511    source: &Source,
12512) -> Vec<CheckStep> {
12513    let mut steps = Vec::new();
12514    let mut checks = CheckSteps::new(&mut steps);
12515    let Some(template) = row_fields(typing.graph, identity) else {
12516        checks.collect(Err(vec![typing.error(
12517            source,
12518            format!(
12519                "instance list of `{}` needs `uses {}` before `-> key` or `-> with`",
12520                identity.spec, identity.spec
12521            ),
12522        )]));
12523        checks.finish();
12524        return steps;
12525    };
12526    let row_spec = &identity.spec;
12527    match op {
12528        ListOp::First | ListOp::Last | ListOp::Count => {}
12529        ListOp::Find { keys } => {
12530            for key_expression in keys {
12531                checks.visit(key_expression);
12532            }
12533            checks.collect(check_instance_find(
12534                keys, node, key, template, typing, source,
12535            ));
12536        }
12537        ListOp::Reduce { kind, argument } => match argument {
12538            None => checks.collect(Err(vec![typing.error(
12539                source,
12540                format!(
12541                    "`{}` on `{row_spec}` rows needs a field, for example `-> {} <field>`",
12542                    kind.keyword(),
12543                    kind.keyword()
12544                ),
12545            )])),
12546            Some(argument) => {
12547                checks.visit(argument);
12548                let argument_type = typing.infer(node.child(*argument));
12549                checks.collect(check_list_element_op(
12550                    &ListOp::Reduce {
12551                        kind: *kind,
12552                        argument: None,
12553                    },
12554                    &argument_type,
12555                    node,
12556                    typing,
12557                    source,
12558                ));
12559            }
12560        },
12561        ListOp::Sort {
12562            descending,
12563            argument,
12564        } => match argument {
12565            None => checks.collect(Err(vec![typing.error(
12566                source,
12567                format!("`sort` on `{row_spec}` rows needs a field, for example `-> sort <field>`"),
12568            )])),
12569            Some(argument) => {
12570                checks.visit(argument);
12571                let argument_type = typing.infer(node.child(*argument));
12572                checks.collect(check_list_element_op(
12573                    &ListOp::Sort {
12574                        descending: *descending,
12575                        argument: None,
12576                    },
12577                    &argument_type,
12578                    node,
12579                    typing,
12580                    source,
12581                ));
12582            }
12583        },
12584        ListOp::Predicate { kind, form, expr } => {
12585            checks.visit(expr);
12586            if *form == ast::ListPredicateForm::Value {
12587                checks.collect(Err(vec![typing.error(
12588                    source,
12589                    format!(
12590                        "`{}` on `{row_spec}` rows takes a row expression such as `price > 3 eur`, not a bare comparison",
12591                        kind.keyword()
12592                    ),
12593                )]));
12594            }
12595            let condition_type = typing.infer(node.child(*expr));
12596            if !condition_type.is_boolean()
12597                && !condition_type.is_undetermined()
12598                && !condition_type.vetoed()
12599            {
12600                checks.collect(Err(vec![typing.error(
12601                    source,
12602                    format!(
12603                        "`{}` on `{row_spec}` rows needs a boolean row expression, got {}",
12604                        kind.keyword(),
12605                        condition_type.name()
12606                    ),
12607                )]));
12608            }
12609        }
12610        ListOp::Top { n } => {
12611            checks.visit(n);
12612            checks.collect(check_top_count(
12613                node.child(*n),
12614                typing.graph,
12615                typing.inferred_types,
12616                typing.resolved_types,
12617                source,
12618                typing.cache,
12619            ));
12620        }
12621        ListOp::With { rows } => {
12622            for row in rows {
12623                checks.visit(row);
12624                let value_type = typing.infer(node.child(*row));
12625                if value_type.is_undetermined() || value_type.vetoed() {
12626                    continue;
12627                }
12628                let ok = match &value_type.specifications {
12629                    TypeSpecification::Spec { spec } if spec == identity => true,
12630                    TypeSpecification::List { element, .. } => {
12631                        matches!(
12632                            &element.specifications,
12633                            TypeSpecification::Spec { spec } if spec == identity
12634                        )
12635                    }
12636                    _ => false,
12637                };
12638                if !ok {
12639                    checks.collect(Err(vec![typing.error(
12640                        source,
12641                        format!(
12642                            "`-> with` on `{row_spec}` rows takes `field: value` pairs, a `{row_spec}` instance, or a `{row_spec}` list"
12643                        ),
12644                    )]));
12645                }
12646            }
12647        }
12648        ListOp::WithRow { fields, .. } => {
12649            let mut fields_typed = true;
12650            for (path, value) in fields {
12651                checks.visit(value);
12652                let field_type =
12653                    template.end_type(&RowEnd::Data(path.clone()), typing.inferred_types);
12654                let value_type = typing.infer(node.child(*value));
12655                if !value_type.is_undetermined()
12656                    && !value_type.vetoed()
12657                    && !field_type.same_value_type(value_type.as_ref())
12658                {
12659                    fields_typed = false;
12660                    checks.collect(Err(vec![typing.error(
12661                        source,
12662                        format!(
12663                            "row field `{}` of `{row_spec}` is {}, got {}",
12664                            path.input_key(),
12665                            field_type.name(),
12666                            value_type.name()
12667                        ),
12668                    )]));
12669                }
12670            }
12671            if let ListKey::Fields(key_fields) = key {
12672                for key_field in key_fields {
12673                    let path = &key_field.path;
12674                    let written = fields.iter().any(|(written, _)| *written == *path);
12675                    if !written && template.default(path).is_none() {
12676                        checks.collect(Err(vec![typing.error(
12677                            source,
12678                            format!("`-> with` row must set key field `{}`", key_field.name),
12679                        )]));
12680                    }
12681                }
12682            }
12683            if fields_typed {
12684                checks.collect(check_appended_row_key(
12685                    list, node, fields, key, typing, source,
12686                ));
12687            }
12688        }
12689        ListOp::Key { fields } => {
12690            checks.collect(check_key_list_spine(list, typing, source));
12691            for path in fields {
12692                assert!(
12693                    path.segments.is_empty(),
12694                    "BUG: `-> key` fields are names of the row spec"
12695                );
12696                let chain = template.chain(typing.graph, identity, &[path.data.as_str()]);
12697                let end = chain.as_deref().map(|chain| match chain {
12698                    [hop] => &hop.end,
12699                    _ => unreachable!("BUG: a one-part chain has one hop"),
12700                });
12701                let row_data = match &template {
12702                    RowFields::Plan(plan) => &plan.data,
12703                    RowFields::Root(graph) => graph.data(),
12704                };
12705                match end {
12706                    Some(RowEnd::Data(data))
12707                        if matches!(row_data.get(data), Some(DataDefinition::Import { .. })) =>
12708                    {
12709                        checks.collect(Err(vec![typing.error(
12710                            source,
12711                            format!(
12712                                "`-> key` field `{}` must be data on `{row_spec}`, not a uses instance",
12713                                path.input_key()
12714                            ),
12715                        )]))
12716                    }
12717                    Some(RowEnd::Data(_)) => {}
12718                    Some(RowEnd::Rule(_)) => checks.collect(Err(vec![typing.error(
12719                        source,
12720                        format!(
12721                            "`-> key` field `{}` must be data on `{row_spec}`, not a rule",
12722                            path.input_key()
12723                        ),
12724                    )])),
12725                    None => checks.collect(Err(vec![typing.error(
12726                        source,
12727                        format!(
12728                            "`-> key` field `{}` is not a member of `{row_spec}`",
12729                            path.input_key()
12730                        ),
12731                    )])),
12732                }
12733            }
12734        }
12735    }
12736    checks.finish();
12737    steps
12738}
12739
12740/// `-> key` may only refine a bare list / prior `key`-free construction spine.
12741/// It must not follow `with` or a query command, and must not appear twice.
12742fn check_key_list_spine(
12743    list: ExprRef<'_, ExpressionKind>,
12744    typing: &TypingScope<'_>,
12745    source: &Source,
12746) -> Result<(), Vec<Error>> {
12747    match list.kind() {
12748        ExpressionKind::ListCommand { op, .. } => match op {
12749            ListOp::Key { .. } => Err(vec![typing.error(
12750                source,
12751                "`-> key` appears twice; list every key field in one `-> key`",
12752            )]),
12753            ListOp::With { .. } | ListOp::WithRow { .. } => Err(vec![typing.error(
12754                source,
12755                "`-> key` must come before the first `-> with`",
12756            )]),
12757            ListOp::Predicate { .. }
12758            | ListOp::Sort { .. }
12759            | ListOp::Top { .. }
12760            | ListOp::Reduce { .. }
12761            | ListOp::Find { .. }
12762            | ListOp::First
12763            | ListOp::Last
12764            | ListOp::Count => Err(vec![typing.error(
12765                source,
12766                "`-> key` declares list addressing; write it on the list before query commands such as `where`",
12767            )]),
12768        },
12769        _ => Ok(()),
12770    }
12771}
12772
12773/// `-> find` on instance rows: one row number on a numbered list, one value per key field otherwise.
12774fn check_instance_find(
12775    keys: &[ExprId],
12776    node: ExprRef<'_, ExpressionKind>,
12777    key: &ListKey,
12778    template: RowFields<'_>,
12779    typing: &mut TypingScope<'_>,
12780    source: &Source,
12781) -> Result<(), Vec<Error>> {
12782    let fields = match key {
12783        ListKey::Numbered => {
12784            if keys.len() != 1 {
12785                return Err(vec![
12786                    typing.error(source, "numbered `-> find` takes one row number")
12787                ]);
12788            }
12789            check_numbered_find_key(node.child(keys[0]), typing.graph, source)?;
12790            let key_type = typing.infer(node.child(keys[0]));
12791            if key_type.is_number() || key_type.is_undetermined() || key_type.vetoed() {
12792                return Ok(());
12793            }
12794            return Err(vec![typing.error(
12795                source,
12796                format!(
12797                    "numbered `find` takes a row number, got {}",
12798                    key_type.name()
12799                ),
12800            )]);
12801        }
12802        ListKey::Fields(fields) => fields.as_slice(),
12803    };
12804    if keys.len() != fields.len() {
12805        return Err(vec![typing.error(
12806            source,
12807            format!(
12808                "`-> find` takes {} key fields ({}), got {}",
12809                fields.len(),
12810                fields
12811                    .iter()
12812                    .map(|field| field.name.as_str())
12813                    .collect::<Vec<_>>()
12814                    .join(", "),
12815                keys.len()
12816            ),
12817        )]);
12818    }
12819    let mut errors = Vec::new();
12820    for (key_expression, key_field) in keys.iter().zip(fields) {
12821        let key_expression = node.child(*key_expression);
12822        let end = RowEnd::Data(key_field.path.clone());
12823        let field_type = template.end_type(&end, typing.inferred_types);
12824        let key_type = typing.infer(key_expression);
12825        let result = if field_type.is_range() {
12826            check_range_containment_types(typing.graph, &key_type, &field_type, source)
12827        } else {
12828            check_comparison_types(
12829                typing.graph,
12830                &field_type,
12831                &ComparisonComputation::Is,
12832                &key_type,
12833                source,
12834            )
12835        };
12836        if let Err(found) = result {
12837            errors.push(typing.error(
12838                source,
12839                format!(
12840                    "key field `{}` is {}, got {}: {}",
12841                    key_field.name,
12842                    field_type.name(),
12843                    key_type.name(),
12844                    found
12845                        .iter()
12846                        .map(|error| error.message().to_string())
12847                        .collect::<Vec<_>>()
12848                        .join("; ")
12849                ),
12850            ));
12851        }
12852    }
12853    if errors.is_empty() {
12854        Ok(())
12855    } else {
12856        Err(errors)
12857    }
12858}
12859
12860/// An appended row with literal key values must not collide with the declared source rows.
12861/// Type name in a branch mismatch; lists also name their key, which must match too.
12862fn branch_type_label(lemma_type: &LemmaType) -> String {
12863    match &lemma_type.specifications {
12864        TypeSpecification::List {
12865            key: ListKey::Fields(fields),
12866            ..
12867        } => format!(
12868            "{} (key {})",
12869            lemma_type.name(),
12870            fields
12871                .iter()
12872                .map(|field| field.name.as_str())
12873                .collect::<Vec<_>>()
12874                .join(", ")
12875        ),
12876        TypeSpecification::List {
12877            key: ListKey::Numbered,
12878            ..
12879        } => format!("{} (numbered rows)", lemma_type.name()),
12880        _ => lemma_type.name().to_string(),
12881    }
12882}
12883
12884fn check_appended_row_key(
12885    list: ExprRef<'_, ExpressionKind>,
12886    node: ExprRef<'_, ExpressionKind>,
12887    fields: &[(DataPath, ExprId)],
12888    key: &ListKey,
12889    typing: &TypingScope<'_>,
12890    source: &Source,
12891) -> Result<(), Vec<Error>> {
12892    let ListKey::Fields(key_fields) = key else {
12893        return Ok(());
12894    };
12895    let ExpressionKind::DataPath(list_path) = list.kind() else {
12896        return Ok(());
12897    };
12898    let Some(list_source) = typing.graph.instance_lists().sources.get(list_path) else {
12899        return Ok(());
12900    };
12901    let Some(template) = row_fields(typing.graph, &list_source.identity) else {
12902        return Ok(());
12903    };
12904    let key_paths: Vec<DataPath> = key_fields
12905        .iter()
12906        .map(|key_field| key_field.path.clone())
12907        .collect();
12908    let mut appended = Vec::with_capacity(key_paths.len());
12909    for path in &key_paths {
12910        let written = fields
12911            .iter()
12912            .find(|(field, _)| field == path)
12913            .map(|(_, value)| value);
12914        let value = match written.map(|value| node.child(*value).kind()) {
12915            Some(ExpressionKind::Literal(literal)) => literal.as_ref().clone(),
12916            Some(_) => return Ok(()),
12917            None => match template.default(path) {
12918                Some(default) => default,
12919                None => return Ok(()),
12920            },
12921        };
12922        appended.push(value);
12923    }
12924    let store = &list_source.store;
12925    if store.key.is_empty() {
12926        // The store's keys failed to check, which is an error already.
12927        return Ok(());
12928    }
12929    let stored = store.rows.len();
12930    let key_of = |row: usize| {
12931        if row == stored {
12932            appended.clone()
12933        } else {
12934            store.key_values(row)
12935        }
12936    };
12937    let built = KeyIndex::build(store.key_names(), stored + 1, |row, field| {
12938        if row == stored {
12939            &appended[field].value
12940        } else {
12941            store.key_value(row, field)
12942        }
12943    });
12944    match built {
12945        Ok(_) => Ok(()),
12946        Err((fields, conflict)) => {
12947            let message = conflict.message(&fields, |row| {
12948                let label = if row == stored {
12949                    "the appended row".to_string()
12950                } else {
12951                    format!("row {}", row + 1)
12952                };
12953                (label, key_of(row))
12954            });
12955            Err(vec![typing.error(source, message)])
12956        }
12957    }
12958}
12959
12960fn check_numbered_find_key(
12961    key: ExprRef<'_, ExpressionKind>,
12962    graph: &Graph,
12963    source: &Source,
12964) -> Result<(), Vec<Error>> {
12965    let ExpressionKind::Literal(literal) = key.kind() else {
12966        return Ok(());
12967    };
12968    let ValueKind::Number(n) = &literal.value else {
12969        return Ok(());
12970    };
12971    let decimal = n
12972        .try_to_decimal()
12973        .map_err(|failure| vec![engine_error_at_graph(graph, source, failure.to_string())])?;
12974    if decimal.fract() != Decimal::ZERO {
12975        return Err(vec![engine_error_at_graph(
12976            graph,
12977            source,
12978            format!("row number must be a whole number, got {decimal}"),
12979        )]);
12980    }
12981    if decimal <= Decimal::ZERO {
12982        return Err(vec![engine_error_at_graph(
12983            graph,
12984            source,
12985            format!("row number must be >= 1, got {decimal}"),
12986        )]);
12987    }
12988    Ok(())
12989}
12990
12991/// Check all rule types in topological order, given precomputed inferred types.
12992/// Validates:
12993/// - Branch type consistency (all non-Veto branches must return the same value type)
12994/// - Condition types (unless clause conditions must be boolean)
12995/// - All sub-expressions via `check_expression`
12996fn check_rule_types(
12997    graph: &Graph,
12998    rule_order: &[RulePath],
12999    inferred_types: &HashMap<RulePath, Arc<LemmaType>>,
13000    resolved_types: &ResolvedTypesMap,
13001    cache: &mut InferExpressionTypeCache,
13002) -> Result<(), Vec<Error>> {
13003    let mut errors = Vec::new();
13004    let mut scratch = CheckScratch::default();
13005
13006    let collect = |result: Result<(), Vec<Error>>, errors: &mut Vec<Error>| {
13007        if let Err(errs) = result {
13008            errors.extend(errs);
13009        }
13010    };
13011
13012    for rule_path in rule_order {
13013        let rule_node = graph
13014            .rules()
13015            .get(rule_path)
13016            .expect("BUG: rule from topological sort not in graph");
13017        let branches = &rule_node.branches;
13018        let spec = &rule_node.spec;
13019
13020        if branches.is_empty() {
13021            continue;
13022        }
13023
13024        let default_result = rule_node.expression.at(branches[0].1);
13025        collect(
13026            check_expression(
13027                default_result,
13028                graph,
13029                inferred_types,
13030                resolved_types,
13031                spec,
13032                cache,
13033                &mut scratch,
13034            ),
13035            &mut errors,
13036        );
13037        let default_type =
13038            infer_expression_type(default_result, graph, inferred_types, resolved_types, cache);
13039
13040        if default_type.is_anonymous_measure() {
13041            if let Some(decomp) = default_type.measure_type_decomposition() {
13042                if !decomp.is_empty() && anonymous_rule_boundary_requires_rejection() {
13043                    let default_source = default_result
13044                        .source()
13045                        .expect("BUG: default branch result expression has no source location");
13046                    errors.push(engine_error_at_graph(
13047                        graph,
13048                        default_source,
13049                        anonymous_rule_boundary_error(
13050                            rule_path,
13051                            spec,
13052                            graph,
13053                            resolved_types,
13054                            decomp,
13055                            None,
13056                        ),
13057                    ));
13058                }
13059            }
13060        }
13061
13062        let mut non_veto_type: Option<LemmaType> = None;
13063        if !default_type.vetoed() && !default_type.is_undetermined() {
13064            non_veto_type = Some(default_type.as_ref().clone());
13065        }
13066
13067        for (branch_index, (condition, result)) in branches.iter().enumerate().skip(1) {
13068            let result = rule_node.expression.at(*result);
13069            if let Some(condition) = condition {
13070                let condition_expression = rule_node.expression.at(*condition);
13071                collect(
13072                    check_expression(
13073                        condition_expression,
13074                        graph,
13075                        inferred_types,
13076                        resolved_types,
13077                        spec,
13078                        cache,
13079                        &mut scratch,
13080                    ),
13081                    &mut errors,
13082                );
13083                let condition_type = infer_expression_type(
13084                    condition_expression,
13085                    graph,
13086                    inferred_types,
13087                    resolved_types,
13088                    cache,
13089                );
13090                if !condition_type.vetoed()
13091                    && !condition_type.is_boolean()
13092                    && !condition_type.is_undetermined()
13093                {
13094                    let condition_source = condition_expression
13095                        .source()
13096                        .expect("BUG: condition expression missing source in check_rule_types");
13097                    errors.push(engine_error_at_graph(
13098                        graph,
13099                        condition_source,
13100                        format!(
13101                            "Unless clause condition in rule '{}' must be boolean, got {}",
13102                            rule_path.rule, condition_type
13103                        ),
13104                    ));
13105                }
13106            }
13107
13108            collect(
13109                check_expression(
13110                    result,
13111                    graph,
13112                    inferred_types,
13113                    resolved_types,
13114                    spec,
13115                    cache,
13116                    &mut scratch,
13117                ),
13118                &mut errors,
13119            );
13120            let result_type =
13121                infer_expression_type(result, graph, inferred_types, resolved_types, cache);
13122
13123            if result_type.is_anonymous_measure() {
13124                if let Some(decomp) = result_type.measure_type_decomposition() {
13125                    if !decomp.is_empty() && anonymous_rule_boundary_requires_rejection() {
13126                        let branch_source = result
13127                            .source()
13128                            .expect("BUG: unless branch result expression has no source location");
13129                        errors.push(engine_error_at_graph(
13130                            graph,
13131                            branch_source,
13132                            anonymous_rule_boundary_error(
13133                                rule_path,
13134                                spec,
13135                                graph,
13136                                resolved_types,
13137                                decomp,
13138                                Some(branch_index),
13139                            ),
13140                        ));
13141                    }
13142                }
13143            }
13144
13145            if !result_type.vetoed() && !result_type.is_undetermined() {
13146                if non_veto_type.is_none() {
13147                    non_veto_type = Some(result_type.as_ref().clone());
13148                } else if let Some(ref existing_type) = non_veto_type {
13149                    if !existing_type.same_value_type(result_type.as_ref()) {
13150                        let Some(rule_node) = graph.rules().get(rule_path) else {
13151                            unreachable!(
13152                                "BUG: rule type validation referenced missing rule '{}'",
13153                                rule_path.rule
13154                            );
13155                        };
13156                        let rule_source = &rule_node.source;
13157
13158                        let mut location_parts = vec![format!(
13159                            "{}:{}:{}",
13160                            rule_source.source_type, rule_source.span.line, rule_source.span.col
13161                        )];
13162
13163                        if let Some(loc) = default_result.source() {
13164                            location_parts.push(format!(
13165                                "default branch at {}:{}:{}",
13166                                loc.source_type, loc.span.line, loc.span.col
13167                            ));
13168                        }
13169                        if let Some(loc) = result.source() {
13170                            location_parts.push(format!(
13171                                "unless clause {} at {}:{}:{}",
13172                                branch_index, loc.source_type, loc.span.line, loc.span.col
13173                            ));
13174                        }
13175
13176                        errors.push(Error::validation_with_context(
13177                            format!("Type mismatch in rule '{}' in spec '{}' ({}): default branch returns {}, but unless clause {} returns {}. All branches must return the same value type (matching base kind and, for measures, dimensions).",
13178                            rule_path.rule,
13179                            spec.name,
13180                            location_parts.join(", "),
13181                            branch_type_label(existing_type),
13182                            branch_index,
13183                            branch_type_label(&result_type)),
13184                            Some(rule_source.clone()),
13185                            None::<String>,
13186                            Some(graph.main_spec),
13187                            None,
13188                        ));
13189                    }
13190                }
13191            }
13192        }
13193    }
13194
13195    if errors.is_empty() {
13196        Ok(())
13197    } else {
13198        Err(errors)
13199    }
13200}
13201
13202// =============================================================================
13203// Phase 3: Apply inferred types to the graph (the only mutation point)
13204// =============================================================================
13205
13206/// Write inferred types into the graph's rule nodes.
13207/// This is the only function that mutates the graph during the validation pipeline.
13208/// It must only be called after all checks pass (no errors).
13209fn apply_inferred_types(graph: &mut Graph, inferred_types: HashMap<RulePath, Arc<LemmaType>>) {
13210    for (rule_path, rule_type) in inferred_types {
13211        if let Some(rule_node) = graph.rules_mut().get_mut(&rule_path) {
13212            rule_node.rule_type = rule_type;
13213        }
13214    }
13215}
13216
13217/// Infer the types of all rules in topological order without validating expressions.
13218/// Returns a map from rule path to its inferred type, and the Errors of recursive
13219/// rules whose type cannot be seeded or does not match its seed.
13220fn infer_rule_types(
13221    graph: &Graph,
13222    rule_order: &[RulePath],
13223    resolved_types: &ResolvedTypesMap,
13224    cache: &mut InferExpressionTypeCache,
13225) -> (HashMap<RulePath, Arc<LemmaType>>, Vec<Error>) {
13226    let mut computed_types: HashMap<RulePath, Arc<LemmaType>> = HashMap::new();
13227    let mut errors = Vec::new();
13228    let mut seeds: HashMap<RulePath, Arc<LemmaType>> = HashMap::new();
13229
13230    // 1. On-demand seed every recursive rule (and any locals their seeds name).
13231    for rule_path in rule_order {
13232        let rule_node = graph
13233            .rules()
13234            .get(rule_path)
13235            .expect("BUG: rule from topological sort not in graph");
13236        if !rule_node.recursive || rule_node.branches.is_empty() {
13237            continue;
13238        }
13239        ensure_seed(rule_path, graph, &mut computed_types, resolved_types, cache);
13240        match computed_types.get(rule_path) {
13241            Some(seed) if !seed.vetoed() && !seed.is_undetermined() => {
13242                seeds.insert(rule_path.clone(), Arc::clone(seed));
13243            }
13244            _ => {
13245                errors.push(reference_error(
13246                    graph.main_spec,
13247                    &rule_node.source,
13248                    format!(
13249                        "recursive rule `{}` needs a non-recursive part to determine its type",
13250                        rule_path.rule
13251                    ),
13252                ));
13253                computed_types.insert(rule_path.clone(), Arc::new(LemmaType::undetermined_type()));
13254            }
13255        }
13256        cache.clear();
13257    }
13258
13259    // 2. Infer every rule in same-instance topo (recursive full bodies against seeds).
13260    // `written_at` is the write count after a rule's type was last written, `inferred_at`
13261    // the write count when a rule was inferred; pass-1 entries were written at 0.
13262    let mut writes = 0usize;
13263    let mut written_at: HashMap<&RulePath, usize> = HashMap::new();
13264    let mut inferred_at: HashMap<&RulePath, usize> = HashMap::new();
13265    let mut overwrote_pass_one_entry = false;
13266    for rule_path in rule_order {
13267        let rule_node = graph
13268            .rules()
13269            .get(rule_path)
13270            .expect("BUG: rule from topological sort not in graph");
13271        if rule_node.branches.is_empty() {
13272            continue;
13273        }
13274        if rule_node.recursive && !seeds.contains_key(rule_path) {
13275            continue;
13276        }
13277        inferred_at.insert(rule_path, writes);
13278        let rule_type =
13279            infer_rule_body_type(rule_node, graph, &computed_types, resolved_types, cache);
13280        if let Some(seed) = seeds.get(rule_path) {
13281            let determined = !rule_type.vetoed() && !rule_type.is_undetermined();
13282            if determined && !rule_type.same_value_type(seed) {
13283                errors.push(reference_error(
13284                    graph.main_spec,
13285                    &rule_node.source,
13286                    format!(
13287                        "recursive rule `{}` is {} from its non-recursive part, but {} as a whole",
13288                        rule_path.rule,
13289                        seed.name(),
13290                        rule_type.name()
13291                    ),
13292                ));
13293            }
13294        }
13295        writes += 1;
13296        written_at.insert(rule_path, writes);
13297        if computed_types
13298            .insert(rule_path.clone(), rule_type)
13299            .is_some()
13300        {
13301            overwrote_pass_one_entry = true;
13302        }
13303    }
13304
13305    // 3. Re-infer non-recursive rules against full recursive types (not seeds). A rule's
13306    // type is a function of the rule types it reads; it changes only if one of them was
13307    // written after the rule was inferred. In pass 2 a rule reads only types already
13308    // written, so that needs a pass-1 entry that pass 2 overwrote later.
13309    if overwrote_pass_one_entry {
13310        for rule_path in rule_order {
13311            let rule_node = graph
13312                .rules()
13313                .get(rule_path)
13314                .expect("BUG: rule from topological sort not in graph");
13315            if rule_node.recursive || rule_node.branches.is_empty() {
13316                continue;
13317            }
13318            let inferred = *inferred_at
13319                .get(rule_path)
13320                .expect("BUG: pass 2 inferred every non-recursive rule with branches");
13321            let read_a_later_write = rule_node
13322                .branches
13323                .iter()
13324                .flat_map(|(_, result)| {
13325                    local_rule_names_in(rule_node.expression.at(*result), graph)
13326                })
13327                .any(|read| written_at.get(&read).is_some_and(|at| *at > inferred));
13328            if !read_a_later_write {
13329                continue;
13330            }
13331            cache.clear();
13332            let rule_type =
13333                infer_rule_body_type(rule_node, graph, &computed_types, resolved_types, cache);
13334            if computed_types.get(rule_path) != Some(&rule_type) {
13335                writes += 1;
13336                written_at.insert(rule_path, writes);
13337                computed_types.insert(rule_path.clone(), rule_type);
13338            }
13339        }
13340        cache.clear();
13341    }
13342
13343    (computed_types, errors)
13344}
13345
13346fn infer_rule_body_type(
13347    rule_node: &RuleNode<'_>,
13348    graph: &Graph,
13349    computed_types: &HashMap<RulePath, Arc<LemmaType>>,
13350    resolved_types: &ResolvedTypesMap,
13351    cache: &mut InferExpressionTypeCache,
13352) -> Arc<LemmaType> {
13353    let body_types = rule_node.branches.iter().map(|(_, result)| {
13354        infer_expression_type(
13355            rule_node.expression.at(*result),
13356            graph,
13357            computed_types,
13358            resolved_types,
13359            cache,
13360        )
13361    });
13362    piecewise_type(body_types, &arithmetic_scope(resolved_types, graph))
13363}
13364
13365/// Seed of `rule` from its non-recursive parts. Inserts a placeholder while visiting so
13366/// mutual row recursion does not loop; replaces it when a concrete seed is found.
13367fn ensure_seed(
13368    rule: &RulePath,
13369    graph: &Graph,
13370    computed_types: &mut HashMap<RulePath, Arc<LemmaType>>,
13371    resolved_types: &ResolvedTypesMap,
13372    cache: &mut InferExpressionTypeCache,
13373) {
13374    if computed_types.contains_key(rule) {
13375        return;
13376    }
13377    if !graph.rules().contains_key(rule) {
13378        return;
13379    }
13380    let mut reachable = Vec::new();
13381    let mut queued = HashSet::new();
13382    let mut pending = VecDeque::from([rule.clone()]);
13383    queued.insert(rule.clone());
13384    while let Some(path) = pending.pop_front() {
13385        let node = graph
13386            .rules()
13387            .get(&path)
13388            .expect("BUG: local rule missing from graph in ensure_seed");
13389        reachable.push(path);
13390        for (_, result) in &node.branches {
13391            for dependency in local_rule_names_in(node.expression.at(*result), graph) {
13392                if queued.insert(dependency.clone()) {
13393                    pending.push_back(dependency);
13394                }
13395            }
13396        }
13397    }
13398    for path in &reachable {
13399        computed_types
13400            .entry(path.clone())
13401            .or_insert_with(|| Arc::new(LemmaType::undetermined_type()));
13402    }
13403    loop {
13404        let mut changed = false;
13405        for path in &reachable {
13406            let current = computed_types
13407                .get(path)
13408                .expect("BUG: reachable seed rule missing placeholder");
13409            if !current.vetoed() && !current.is_undetermined() {
13410                continue;
13411            }
13412            let node = graph
13413                .rules()
13414                .get(path)
13415                .expect("BUG: reachable seed rule missing from graph");
13416            cache.clear();
13417            let seed = node.branches.iter().find_map(|(_, result)| {
13418                seed_type(
13419                    node.expression.at(*result),
13420                    graph,
13421                    computed_types,
13422                    resolved_types,
13423                    cache,
13424                )
13425            });
13426            if let Some(seed) = seed {
13427                computed_types.insert(path.clone(), seed);
13428                changed = true;
13429            }
13430        }
13431        if !changed {
13432            break;
13433        }
13434    }
13435}
13436
13437/// Type `expression` has whatever the recursive rule being seeded (typed undetermined
13438/// in `computed_types`) turns out to be, when its non-recursive parts fix it.
13439fn seed_type(
13440    expression: ExprRef<'_, ExpressionKind>,
13441    graph: &Graph,
13442    computed_types: &HashMap<RulePath, Arc<LemmaType>>,
13443    resolved_types: &ResolvedTypesMap,
13444    cache: &mut InferExpressionTypeCache,
13445) -> Option<Arc<LemmaType>> {
13446    let mut pending = vec![expression];
13447    while let Some(node) = pending.pop() {
13448        let (fixed, children) = seed_candidates(node);
13449        if let Some(fixed) = fixed {
13450            return Some(fixed);
13451        }
13452        if !children.is_empty() {
13453            // Expand before full infer: helpers in `own + children_cost` may be untyped.
13454            pending.extend(children.into_iter().rev().map(|id| node.child(id)));
13455            continue;
13456        }
13457        match node.kind() {
13458            ExpressionKind::RulePath(rule) => {
13459                if let Some(lemma_type) = computed_types.get(rule) {
13460                    if !lemma_type.vetoed() && !lemma_type.is_undetermined() {
13461                        return Some(Arc::clone(lemma_type));
13462                    }
13463                }
13464            }
13465            _ => {
13466                let inferred =
13467                    infer_expression_type(node, graph, computed_types, resolved_types, cache);
13468                if !inferred.vetoed() && !inferred.is_undetermined() {
13469                    return Some(inferred);
13470                }
13471            }
13472        }
13473    }
13474    None
13475}
13476
13477/// Local rules that `node` and its table prefix name (operands precede parents).
13478fn local_rule_names_in(node: ExprRef<'_, ExpressionKind>, graph: &Graph<'_>) -> Vec<RulePath> {
13479    let mut out = Vec::new();
13480    for index in 0..=node.id.index() {
13481        let child = node.child(ExprId::from_index(index));
13482        match child.kind() {
13483            ExpressionKind::RulePath(rule) => {
13484                if graph.rules().contains_key(rule) {
13485                    out.push(rule.clone());
13486                }
13487            }
13488            ExpressionKind::RowField { end, .. } => {
13489                push_local_rule_from_row_end(graph, end, &mut out);
13490            }
13491            ExpressionKind::SpecMember { path, name, .. } => {
13492                let identity = &graph.instance_lists().self_identity;
13493                if let Some(fields) = row_fields(graph, identity) {
13494                    let mut parts: Vec<&str> = path.iter().map(String::as_str).collect();
13495                    parts.push(name);
13496                    if let Some([hop]) = fields.chain(graph, identity, &parts).as_deref() {
13497                        push_local_rule_from_row_end(graph, &hop.end, &mut out);
13498                    }
13499                }
13500            }
13501            ExpressionKind::DataPath(path) => {
13502                if let Some(ReferenceEnd::Rule(rule)) = graph.reference_ends().get(path) {
13503                    if graph.rules().contains_key(rule) {
13504                        out.push(rule.clone());
13505                    }
13506                } else if let Some(DataDefinition::Reference {
13507                    target: ReferenceTarget::Rule(rule),
13508                    ..
13509                }) = graph.data().get(path)
13510                {
13511                    if graph.rules().contains_key(rule) {
13512                        out.push(rule.clone());
13513                    }
13514                }
13515            }
13516            _ => {}
13517        }
13518    }
13519    out
13520}
13521
13522fn push_local_rule_from_row_end(graph: &Graph<'_>, end: &RowEnd, out: &mut Vec<RulePath>) {
13523    match end {
13524        RowEnd::Rule(rule) => {
13525            if graph.rules().contains_key(rule) {
13526                out.push(rule.clone());
13527            }
13528        }
13529        RowEnd::Data(path) => {
13530            if let Some(DataDefinition::Reference {
13531                target: ReferenceTarget::Rule(rule),
13532                ..
13533            }) = graph.data().get(path)
13534            {
13535                if graph.rules().contains_key(rule) {
13536                    out.push(rule.clone());
13537                }
13538            }
13539        }
13540    }
13541}
13542
13543/// The type a node's kind fixes without looking inside, and otherwise the children whose
13544/// seeds are tried in order.
13545fn seed_candidates(node: ExprRef<'_, ExpressionKind>) -> (Option<Arc<LemmaType>>, Vec<ExprId>) {
13546    match node.kind() {
13547        ExpressionKind::Arithmetic { first, steps }
13548            if steps.iter().all(|step| {
13549                matches!(
13550                    step.op,
13551                    ArithmeticComputation::Add | ArithmeticComputation::Subtract
13552                )
13553            }) =>
13554        {
13555            (
13556                None,
13557                std::iter::once(*first)
13558                    .chain(steps.iter().map(|step| step.operand))
13559                    .collect(),
13560            )
13561        }
13562        ExpressionKind::Comparison(..)
13563        | ExpressionKind::Logical { .. }
13564        | ExpressionKind::LogicalNegation(..) => {
13565            (Some(primitive_boolean_arc().clone()), Vec::new())
13566        }
13567        ExpressionKind::ListCommand { op, .. } => match op {
13568            ListOp::Count => (Some(primitive_number_arc().clone()), Vec::new()),
13569            ListOp::Predicate { kind, .. } => match kind {
13570                ListPredKind::Any | ListPredKind::All => {
13571                    (Some(primitive_boolean_arc().clone()), Vec::new())
13572                }
13573                ListPredKind::Where => (None, Vec::new()),
13574            },
13575            ListOp::Reduce {
13576                kind: ListReduce::Sum | ListReduce::Min | ListReduce::Max | ListReduce::Avg,
13577                argument: Some(argument),
13578            } => (None, vec![*argument]),
13579            _ => (None, Vec::new()),
13580        },
13581        ExpressionKind::Piecewise(arms) => (None, arms.iter().map(|(_, result)| *result).collect()),
13582        _ => (None, Vec::new()),
13583    }
13584}
13585
13586/// Unit name -> (owning type name when local to the spec, decomposition, factor).
13587type UnitDecompLookup = HashMap<String, (Option<String>, BaseMeasureVector, RationalInteger)>;
13588
13589fn local_owner_name(owner: &PlanningOwner) -> Option<String> {
13590    match owner {
13591        PlanningOwner::Local { type_name } => Some(type_name.clone()),
13592        PlanningOwner::PrimitiveRatio | PlanningOwner::Imported { .. } => None,
13593    }
13594}
13595
13596fn unwrap_unique_resolved_type(arc: Arc<LemmaType>) -> LemmaType {
13597    Arc::try_unwrap(arc).expect("BUG: resolved LemmaType Arc unique during type resolution")
13598}
13599
13600fn declared_measure_decomposition(lemma_type: &LemmaType) -> BaseMeasureVector {
13601    match &lemma_type.specifications {
13602        TypeSpecification::Measure { traits, .. }
13603            if traits.contains(&semantics::MeasureTrait::Duration) =>
13604        {
13605            duration_decomposition()
13606        }
13607        TypeSpecification::Measure { traits, .. }
13608            if traits.contains(&semantics::MeasureTrait::Calendar) =>
13609        {
13610            calendar_decomposition()
13611        }
13612        _ => {
13613            let family = lemma_type
13614                .measure_family()
13615                .expect("BUG: named measure type must carry a family");
13616            [(semantics::Dimension::Family(family.clone()), 1i32)]
13617                .into_iter()
13618                .collect()
13619        }
13620    }
13621}
13622
13623/// Local type owning `unit_name`; `None` when unknown or imported.
13624fn local_owning_measure_type_for_unit(
13625    unit_name: &str,
13626    lookup: &UnitDecompLookup,
13627    unit_index: &PlanningUnitIndex,
13628    resolved: &IndexMap<String, Arc<LemmaType>>,
13629) -> Option<String> {
13630    if let Some((owning_measure_name, _, _)) = lookup.get(unit_name) {
13631        return owning_measure_name.clone();
13632    }
13633    unit_index
13634        .resolve_owner(unit_name, resolved)
13635        .ok()
13636        .and_then(|(_, owner)| local_owner_name(owner))
13637}
13638
13639/// Order compound measure types so every referenced unit from another compound type is resolved first.
13640fn sort_derived_measure_types_for_resolution(
13641    spec_name: &str,
13642    derived_measure_type_names: Vec<String>,
13643    resolved: &IndexMap<String, Arc<LemmaType>>,
13644    lookup: &UnitDecompLookup,
13645    unit_index: &PlanningUnitIndex,
13646    type_sources: &HashMap<String, Source>,
13647) -> Result<Vec<String>, Error> {
13648    let derived_measure_type_count = derived_measure_type_names.len();
13649    if derived_measure_type_count == 0 {
13650        return Ok(derived_measure_type_names);
13651    }
13652
13653    let type_index: HashMap<&str, usize> = derived_measure_type_names
13654        .iter()
13655        .enumerate()
13656        .map(|(index, name)| (name.as_str(), index))
13657        .collect();
13658
13659    let mut dependency_sets: Vec<BTreeSet<usize>> =
13660        vec![BTreeSet::new(); derived_measure_type_count];
13661
13662    for (dependent_index, type_name) in derived_measure_type_names.iter().enumerate() {
13663        let TypeSpecification::Measure { units, .. } = &resolved[type_name].specifications else {
13664            continue;
13665        };
13666        for unit in units.iter() {
13667            for (factor_unit_name, _) in &unit.derived_measure_factors {
13668                let Some(owning_measure_name) = local_owning_measure_type_for_unit(
13669                    factor_unit_name,
13670                    lookup,
13671                    unit_index,
13672                    resolved,
13673                ) else {
13674                    continue;
13675                };
13676                let Some(dependency_index) = type_index.get(owning_measure_name.as_str()).copied()
13677                else {
13678                    continue;
13679                };
13680                if dependency_index == dependent_index {
13681                    continue;
13682                }
13683                dependency_sets[dependent_index].insert(dependency_index);
13684            }
13685        }
13686    }
13687
13688    let mut in_degree = vec![0usize; derived_measure_type_count];
13689    let mut dependents: Vec<Vec<usize>> = vec![Vec::new(); derived_measure_type_count];
13690
13691    for (dependent_index, dependencies) in dependency_sets.iter().enumerate() {
13692        for &dependency_index in dependencies {
13693            in_degree[dependent_index] += 1;
13694            dependents[dependency_index].push(dependent_index);
13695        }
13696    }
13697
13698    let mut queue: VecDeque<usize> = (0..derived_measure_type_count)
13699        .filter(|&index| in_degree[index] == 0)
13700        .collect();
13701    let mut sorted_indices: Vec<usize> = Vec::with_capacity(derived_measure_type_count);
13702
13703    while let Some(index) = queue.pop_front() {
13704        sorted_indices.push(index);
13705        for &dependent_index in &dependents[index] {
13706            in_degree[dependent_index] -= 1;
13707            if in_degree[dependent_index] == 0 {
13708                queue.push_back(dependent_index);
13709            }
13710        }
13711    }
13712
13713    if sorted_indices.len() != derived_measure_type_count {
13714        let mut cycle_type_names: Vec<String> = (0..derived_measure_type_count)
13715            .filter(|&index| in_degree[index] > 0)
13716            .map(|index| derived_measure_type_names[index].clone())
13717            .collect();
13718        cycle_type_names.sort();
13719        return Err(Error::validation(
13720            format!(
13721                "In spec '{}': circular compound measure type dependency among: {}",
13722                spec_name,
13723                cycle_type_names.join(", ")
13724            ),
13725            type_sources.get(&cycle_type_names[0]).cloned(),
13726            None::<String>,
13727        ));
13728    }
13729
13730    Ok(sorted_indices
13731        .into_iter()
13732        .map(|index| derived_measure_type_names[index].clone())
13733        .collect())
13734}
13735
13736/// Build the reverse signature index over every named-measure-type unit in the spec.
13737/// Each unit's canonical-form `derived_measure_factors` becomes a key; the value is the
13738/// owning unit name and type. An ambiguity (the same canonical key matched by two units in
13739/// distinct types) is a planning error: the spec must rename one of the units or change
13740/// its factor decomposition so it is unique.
13741pub(crate) struct SignatureIndexBuild {
13742    pub index: SignatureIndex,
13743    pub deferred_cross_type: Vec<(String, BaseMeasureVector)>,
13744}
13745
13746pub(crate) fn build_signature_index(
13747    spec_name: &str,
13748    unit_index: &UnitIndex,
13749    already_resolved: &ResolvedTypesMap<'_>,
13750) -> Result<SignatureIndexBuild, Error> {
13751    use crate::computation::arithmetic::SignatureIndex;
13752    let mut signature_index: SignatureIndex = SignatureIndex::new();
13753    let mut deferred_cross_type = Vec::new();
13754    let mut sorted_units: Vec<_> = unit_index.iter_owners().collect();
13755    sorted_units.sort_by_key(|(name, _)| *name);
13756    for (unit_name, owner) in sorted_units {
13757        let unit_name = unit_name.to_string();
13758        let lemma_type = owner.lemma_type();
13759        let TypeSpecification::Measure { units, .. } = &lemma_type.specifications else {
13760            continue;
13761        };
13762        let unit = units.get(unit_name.as_str()).map_err(|_| {
13763            Error::validation(
13764                format!(
13765                    "In spec '{}': unit_index entry '{}' is not declared on measure type '{}'",
13766                    spec_name,
13767                    unit_name,
13768                    lemma_type.name()
13769                ),
13770                None::<Source>,
13771                None::<String>,
13772            )
13773        })?;
13774        if lemma_type.measure_type_decomposition().is_none() {
13775            continue;
13776        }
13777        // Identity signatures `[(name, 1)]` collide when the same bare name has
13778        // multiple owners. Skip those; keep unique identities so expand can
13779        // promote compounds that reduce to a single unique unit (e.g. eur).
13780        if measure_unit_is_simple(unit) && unit_index.owners_for(&unit_name).len() != 1 {
13781            continue;
13782        }
13783        let owning_type_name = lemma_type.name.clone().unwrap_or_default();
13784        let signature = if owner.import_path().is_empty() {
13785            resolved_signature_key(
13786                &unit.derived_measure_factors,
13787                &unit_name,
13788                lemma_type,
13789                unit_index,
13790            )
13791        } else if let Some(key) = imported_signature_key(already_resolved, &unit_name, lemma_type) {
13792            key
13793        } else if measure_unit_is_simple(unit) {
13794            // Declaring spec skipped this identity because the name had several
13795            // owners there. Here it is unique, and the key is the unit's own family.
13796            resolved_signature_key(
13797                &unit.derived_measure_factors,
13798                &unit_name,
13799                lemma_type,
13800                unit_index,
13801            )
13802        } else {
13803            // Same type, same signature: the index keeps one slot, under the
13804            // sibling unit that was inserted last (`square_meter` for `acre`).
13805            continue;
13806        };
13807        match signature_index.get(&signature) {
13808            Some((existing_unit_name, existing_owning_type))
13809                if existing_owning_type.name() != lemma_type.name() =>
13810            {
13811                let existing_type_name = existing_owning_type.name.clone().unwrap_or_default();
13812                let decomposition = lemma_type
13813                    .measure_type_decomposition()
13814                    .expect("BUG: indexed measure unit has no decomposition")
13815                    .clone();
13816                deferred_cross_type.push((
13817                    format!(
13818                        "In spec '{spec_name}': ambiguous unit signature {signature:?}: matched by '{existing_unit_name}' in type '{existing_type_name}' and '{unit_name}' in type '{owning_type_name}'. \
13819                         Rename one or differentiate factors."
13820                    ),
13821                    decomposition,
13822                ));
13823            }
13824            _ => {
13825                signature_index.insert(signature, (unit_name.clone(), Arc::clone(lemma_type)));
13826            }
13827        }
13828    }
13829    Ok(SignatureIndexBuild {
13830        index: signature_index,
13831        deferred_cross_type,
13832    })
13833}
13834
13835fn resolved_signature_key(
13836    factors: &[(String, i32)],
13837    unit_name: &str,
13838    lemma_type: &LemmaType,
13839    unit_index: &UnitIndex,
13840) -> Vec<SignatureFactor> {
13841    use crate::computation::arithmetic::SignatureFactor;
13842    let mut key = Vec::with_capacity(factors.len());
13843    for (factor_name, exponent) in factors {
13844        let (name, family) = if factor_name == unit_name {
13845            (
13846                factor_name.clone(),
13847                lemma_type
13848                    .measure_family()
13849                    .expect("BUG: indexed measure unit has no family")
13850                    .clone(),
13851            )
13852        } else {
13853            let (bare, owner) = unit_index.resolve_owner(factor_name).unwrap_or_else(|err| {
13854                panic!(
13855                    "BUG: compound factor '{factor_name}' on unit '{unit_name}' must resolve: {err}"
13856                )
13857            });
13858            (
13859                bare,
13860                owner
13861                    .lemma_type()
13862                    .measure_family()
13863                    .expect("BUG: resolved factor owner is not a named measure")
13864                    .clone(),
13865            )
13866        };
13867        key.push(SignatureFactor {
13868            name,
13869            family,
13870            exponent: *exponent,
13871        });
13872    }
13873    key.sort_by(|left, right| {
13874        (&left.family, left.name.as_str(), left.exponent).cmp(&(
13875            &right.family,
13876            right.name.as_str(),
13877            right.exponent,
13878        ))
13879    });
13880    key
13881}
13882
13883/// Key already built for this unit by the spec that declared it.
13884/// The imported owner holds the same `LemmaType` allocation as that index entry.
13885fn imported_signature_key(
13886    already_resolved: &ResolvedTypesMap<'_>,
13887    unit_name: &str,
13888    lemma_type: &Arc<LemmaType>,
13889) -> Option<Vec<SignatureFactor>> {
13890    for (_, _, types) in already_resolved {
13891        for (key, (name, owner)) in &types.signature_index {
13892            if name == unit_name && Arc::ptr_eq(owner, lemma_type) {
13893                return Some(key.clone());
13894            }
13895        }
13896    }
13897    None
13898}
13899
13900fn deferred_signature_errors(
13901    graph: &Graph<'_>,
13902    resolved_types: &ResolvedTypesMap,
13903    inferred: &HashMap<RulePath, Arc<LemmaType>>,
13904) -> Vec<Error> {
13905    let Some(types) = resolved_types.iter().find_map(|(_, spec, types)| {
13906        discovery::same_loaded_spec(spec, graph.main_spec()).then_some(types)
13907    }) else {
13908        return Vec::new();
13909    };
13910    let mut errors = Vec::new();
13911    for (message, decomposition) in &types.deferred_signature_collisions {
13912        let produced = inferred.values().any(|lemma_type| {
13913            lemma_type.is_anonymous_measure()
13914                && lemma_type.measure_type_decomposition() == Some(decomposition)
13915        });
13916        if produced {
13917            continue;
13918        }
13919        errors.push(Error::validation(
13920            message.clone(),
13921            None::<Source>,
13922            None::<String>,
13923        ));
13924    }
13925    errors
13926}
13927
13928/// Rebuild [`MeasureRange`] (and other range) specs for `ParentType::Ranged` declarations
13929/// after the decomposition pass has populated parent measure types.
13930fn refresh_named_range_specs(
13931    resolver: &TypeResolver<'_>,
13932    spec: &LemmaSpec,
13933    data_defs: &HashMap<String, DataTypeDef>,
13934    resolved_types: &mut ResolvedSpecTypes,
13935    already_resolved: &ResolvedTypesMap<'_>,
13936    at: &EffectiveDate,
13937) -> Vec<Error> {
13938    let local = local_type_entries(resolved_types);
13939    let resolved = &mut resolved_types.resolved;
13940    let declared_suggestions = &mut resolved_types.declared_suggestions;
13941    let mut errors = Vec::new();
13942    for (type_name, def) in data_defs {
13943        let ParentType::Ranged { .. } = &def.parent else {
13944            continue;
13945        };
13946        let element = element_parent_type(&def.parent);
13947        if matches!(element, ParentType::Primitive { .. }) {
13948            continue;
13949        }
13950        let scope = ParentLookup {
13951            resolver,
13952            spec,
13953            source: &def.source,
13954            at,
13955            already_resolved,
13956            resolved: &local,
13957            site: TypeUseSite::DataDefinition,
13958        };
13959        let endpoint_type = match lookup_parent_type(&scope, element) {
13960            Ok((.., endpoint_type)) => endpoint_type,
13961            Err(lookup_errors) => {
13962                errors.extend(lookup_errors);
13963                continue;
13964            }
13965        };
13966        let range_spec = endpoint_type
13967            .specifications
13968            .range_from_element()
13969            .expect("BUG: ranged element was checked rangeable when the type was resolved");
13970        let lemma_type = resolved
13971            .get_mut(type_name.as_str())
13972            .expect("BUG: ranged type resolved before its range spec is refreshed");
13973        let mut updated = lemma_type.as_ref().clone();
13974        updated.specifications = range_spec;
13975        *lemma_type = Arc::new(updated);
13976
13977        if let Some(BoundValueKind {
13978            value: ValueKind::Range(left, right),
13979            ..
13980        }) = declared_suggestions.get_mut(type_name.as_str())
13981        {
13982            let left_typed = left.as_ref().clone();
13983            let right_typed = right.as_ref().clone();
13984            let coerced_left =
13985                match Graph::coerce_literal_to_schema_type(&left_typed, &endpoint_type) {
13986                    Ok(coerced) => coerced,
13987                    Err(message) => {
13988                        errors.push(Error::validation_with_context(
13989                            format!(
13990                            "In spec '{}': named range default left endpoint for '{}': {message}",
13991                            spec.name, type_name
13992                        ),
13993                            Some(def.source.clone()),
13994                            None::<String>,
13995                            Some(spec),
13996                            None,
13997                        ));
13998                        continue;
13999                    }
14000                };
14001            let coerced_right =
14002                match Graph::coerce_literal_to_schema_type(&right_typed, &endpoint_type) {
14003                    Ok(coerced) => coerced,
14004                    Err(message) => {
14005                        errors.push(Error::validation_with_context(
14006                            format!(
14007                            "In spec '{}': named range default right endpoint for '{}': {message}",
14008                            spec.name, type_name
14009                        ),
14010                            Some(def.source.clone()),
14011                            None::<String>,
14012                            Some(spec),
14013                            None,
14014                        ));
14015                        continue;
14016                    }
14017                };
14018            *declared_suggestions
14019                .get_mut(type_name.as_str())
14020                .expect("BUG: named range default removed while refreshing endpoints") =
14021                BoundValueKind {
14022                    value: ValueKind::Range(Box::new(coerced_left), Box::new(coerced_right)),
14023                    unit: None,
14024                };
14025        }
14026    }
14027    errors
14028}
14029
14030/// Apply range-local constraints after [`refresh_named_range_specs`] for named ranged types.
14031fn apply_deferred_named_range_constraints(
14032    spec: &LemmaSpec,
14033    data_defs: &HashMap<String, DataTypeDef>,
14034    resolved: &mut TypeMap,
14035    declared_suggestions: &mut IndexMap<String, BoundValueKind>,
14036    declared_values: &mut IndexMap<String, BoundValueKind>,
14037    type_sources: &HashMap<String, Source>,
14038    unit_index: &PlanningUnitIndex,
14039) -> Vec<Error> {
14040    let mut errors = Vec::new();
14041    for (type_name, def) in data_defs {
14042        if !should_defer_ranged_constraints(&def.parent) {
14043            continue;
14044        }
14045        let Some(constraints) = &def.constraints else {
14046            continue;
14047        };
14048        let Some(lemma_type) = resolved.get(type_name.as_str()).cloned() else {
14049            continue;
14050        };
14051        let mut declared_suggestion: Option<RawSuggestion> = None;
14052        let mut declared_value: Option<RawSuggestion> = None;
14053        let resolve = UnitTokenResolver::Planning {
14054            unit_index,
14055            named_types: resolved,
14056        };
14057        let mut updated = lemma_type.as_ref().clone();
14058        match apply_constraints_to_spec(
14059            spec,
14060            &constraint_application_type_name(&def.parent, type_name),
14061            type_name,
14062            &mut updated,
14063            constraints,
14064            &def.source,
14065            ConstraintSuggestionSink {
14066                suggestion: &mut declared_suggestion,
14067                value: &mut declared_value,
14068                resolve_unit: Some(&resolve),
14069            },
14070        ) {
14071            Ok(()) => {
14072                let updated_arc = Arc::new(updated);
14073                if let Some(raw) = declared_suggestion {
14074                    match bound_value_kind_from_raw_suggestion(
14075                        raw,
14076                        &updated_arc.specifications,
14077                        type_name.as_str(),
14078                    ) {
14079                        Ok(value_kind) => {
14080                            declared_suggestions.insert(type_name.clone(), value_kind);
14081                        }
14082                        Err(message) => {
14083                            let source = type_sources
14084                                .get(type_name.as_str())
14085                                .cloned()
14086                                .unwrap_or_else(|| def.source.clone());
14087                            errors.push(Error::validation_with_context(
14088                                message,
14089                                Some(source),
14090                                None::<String>,
14091                                Some(spec),
14092                                None,
14093                            ));
14094                        }
14095                    }
14096                }
14097                if let Some(raw) = declared_value {
14098                    match bound_value_kind_from_raw_suggestion(
14099                        raw,
14100                        &updated_arc.specifications,
14101                        type_name.as_str(),
14102                    ) {
14103                        Ok(value_kind) => {
14104                            declared_values.insert(type_name.clone(), value_kind);
14105                        }
14106                        Err(message) => {
14107                            let source = type_sources
14108                                .get(type_name.as_str())
14109                                .cloned()
14110                                .unwrap_or_else(|| def.source.clone());
14111                            errors.push(Error::validation_with_context(
14112                                message,
14113                                Some(source),
14114                                None::<String>,
14115                                Some(spec),
14116                                None,
14117                            ));
14118                        }
14119                    }
14120                }
14121                resolved.insert(type_name.clone(), updated_arc);
14122            }
14123            Err(constraint_errors) => errors.extend(constraint_errors),
14124        }
14125    }
14126    errors
14127}
14128
14129/// Where a type is written, for planning error wording.
14130#[derive(Clone, Copy)]
14131enum TypeUseSite {
14132    DataDefinition,
14133    RuleListType,
14134}
14135
14136impl fmt::Display for TypeUseSite {
14137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
14138        match self {
14139            TypeUseSite::DataDefinition => write!(f, "data definition"),
14140            TypeUseSite::RuleListType => write!(f, "list type in a rule"),
14141        }
14142    }
14143}
14144
14145/// Scope a parent type name resolves in: the spec's local types so far, the specs it
14146/// imports, and where the type is written.
14147#[derive(Clone, Copy)]
14148struct ParentLookup<'s, 'a> {
14149    resolver: &'s TypeResolver<'a>,
14150    spec: &'s LemmaSpec,
14151    source: &'s Source,
14152    at: &'s EffectiveDate,
14153    already_resolved: &'s ResolvedTypesMap<'a>,
14154    resolved: &'s HashMap<String, ResolvedTypeEntry>,
14155    site: TypeUseSite,
14156}
14157
14158fn lookup_parent_type(
14159    scope: &ParentLookup<'_, '_>,
14160    parent: &ParentType,
14161) -> Result<ParentLookupOk, Vec<Error>> {
14162    let mut wrappers = Vec::new();
14163    let mut leaf = parent;
14164    while let ParentType::List { inner } | ParentType::Ranged { inner } = leaf {
14165        wrappers.push(leaf);
14166        leaf = inner.as_ref();
14167    }
14168    let mut lookup = lookup_leaf_parent_type(scope, leaf)?;
14169    while let Some(wrapper) = wrappers.pop() {
14170        lookup = lookup_wrapper_parent_type(scope, wrapper, lookup)?;
14171    }
14172    Ok(lookup)
14173}
14174
14175/// Lookup of a `list` or `range` wrapper, given the lookup of its `inner`.
14176fn lookup_wrapper_parent_type(
14177    scope: &ParentLookup<'_, '_>,
14178    wrapper: &ParentType,
14179    inner_lookup: ParentLookupOk,
14180) -> Result<ParentLookupOk, Vec<Error>> {
14181    let ParentLookup { spec, source, .. } = *scope;
14182    let (element_specs, .., element_type) = inner_lookup;
14183    match wrapper {
14184        ParentType::List { inner } => {
14185            let element = if matches!(inner.as_ref(), ParentType::Ranged { .. }) {
14186                Arc::new(LemmaType::primitive(element_specs))
14187            } else {
14188                element_type
14189            };
14190            let lemma_type = Arc::new(LemmaType::list_of(element));
14191            Ok((lemma_type.specifications.clone(), None, None, lemma_type))
14192        }
14193        ParentType::Ranged { inner } => {
14194            let range_spec = element_specs.range_from_element().ok_or_else(|| {
14195                vec![Error::validation_with_context(
14196                    format!(
14197                        "'{inner}' is not rangeable: only measure, number, ratio, date, and time types support ranges"
14198                    ),
14199                    Some(source.clone()),
14200                    None::<String>,
14201                    Some(spec),
14202                    None,
14203                )]
14204            })?;
14205            Ok((range_spec, None, None, element_type))
14206        }
14207        ParentType::Primitive { .. } | ParentType::Custom { .. } | ParentType::Qualified { .. } => {
14208            unreachable!("BUG: lookup_wrapper_parent_type called on a leaf parent")
14209        }
14210    }
14211}
14212
14213/// Lookup of a parent that is not a `list` or `range` wrapper.
14214fn lookup_leaf_parent_type(
14215    scope: &ParentLookup<'_, '_>,
14216    parent: &ParentType,
14217) -> Result<ParentLookupOk, Vec<Error>> {
14218    let ParentLookup {
14219        resolver,
14220        spec,
14221        source,
14222        at,
14223        already_resolved,
14224        resolved,
14225        site,
14226    } = *scope;
14227    match parent {
14228        ParentType::List { .. } | ParentType::Ranged { .. } => {
14229            unreachable!("BUG: list and range wrappers are unwound before the leaf lookup")
14230        }
14231        ParentType::Primitive { primitive: kind } => {
14232            let lemma_type = Arc::clone(semantics::lemma_type_for_primitive(*kind));
14233            Ok((
14234                lemma_type.as_ref().specifications.clone(),
14235                None,
14236                None,
14237                lemma_type,
14238            ))
14239        }
14240        ParentType::Custom { name, .. } => {
14241            if let Some((parent_type, parent_suggestion, parent_value)) =
14242                resolved.get(name.as_str())
14243            {
14244                return Ok((
14245                    parent_type.as_ref().specifications.clone(),
14246                    parent_suggestion.clone(),
14247                    parent_value.clone(),
14248                    Arc::clone(parent_type),
14249                ));
14250            }
14251            let type_exists = resolver
14252                .data_types
14253                .iter()
14254                .find(|(s, _)| discovery::same_loaded_spec(s, spec))
14255                .map(|(_, type_map)| type_map.contains_key(name.as_str()))
14256                .unwrap_or(false);
14257            if !type_exists {
14258                if spec.data.iter().any(|data| {
14259                    data.reference.is_local()
14260                        && data.reference.name == name.as_str()
14261                        && data.value.uses_spec_ref().is_some()
14262                }) {
14263                    return Err(vec![Error::validation_with_context(
14264                        format!(
14265                            "'{name}' names a spec import alias, not a type. Use `data x: {name}.TypeName` after `uses`"
14266                        ),
14267                        Some(source.clone()),
14268                        None::<String>,
14269                        Some(spec),
14270                        None,
14271                    )]);
14272                }
14273                if spec.rules.iter().any(|rule| rule.name == name.as_str()) {
14274                    return Err(vec![Error::validation_with_context(
14275                        format!(
14276                            "Unknown parent '{parent}' for {site}: '{name}' is a local rule, not a type. Use a type name, or a reference expression if you meant the rule's value."
14277                        ),
14278                        Some(source.clone()),
14279                        None::<String>,
14280                        Some(spec),
14281                        None,
14282                    )]);
14283                }
14284                let mut imported_aliases: Vec<&str> = Vec::new();
14285                for data_row in &spec.data {
14286                    let Some(spec_ref) = data_row.value.uses_spec_ref() else {
14287                        continue;
14288                    };
14289                    let import_alias = &data_row.reference.name;
14290                    let (_, imported_spec) =
14291                        match resolver.resolve_spec_for_import(spec, spec_ref, source, at) {
14292                            Ok(pair) => pair,
14293                            Err(_) => continue,
14294                        };
14295                    let Some(imported_resolved) = already_resolved
14296                        .iter()
14297                        .find(|(_, s, _)| discovery::same_loaded_spec(s, imported_spec))
14298                        .map(|(_, _, r)| r)
14299                    else {
14300                        continue;
14301                    };
14302                    if imported_resolved.resolved.contains_key(name.as_str()) {
14303                        imported_aliases.push(import_alias.as_str());
14304                    }
14305                }
14306                if !imported_aliases.is_empty() {
14307                    let forms = imported_aliases
14308                        .iter()
14309                        .map(|alias| format!("{alias}.{name}"))
14310                        .collect::<Vec<_>>()
14311                        .join(", ");
14312                    return Err(vec![Error::validation_with_context(
14313                        format!(
14314                            "Unknown parent '{parent}' for {site}: '{name}' is declared by import{}; write {forms}",
14315                            if imported_aliases.len() == 1 {
14316                                format!(" '{}'", imported_aliases[0])
14317                            } else {
14318                                format!("s {}", imported_aliases.join(", "))
14319                            }
14320                        ),
14321                        Some(source.clone()),
14322                        None::<String>,
14323                        Some(spec),
14324                        None,
14325                    )]);
14326                }
14327                if resolver
14328                    .context
14329                    .spec_set(resolver.registered_repository(spec), name)
14330                    .is_some()
14331                {
14332                    return Err(vec![Error::validation_with_context(
14333                        format!(
14334                            "Unknown parent '{parent}' for {site}: '{name}' is a spec, not a name in spec '{}'. Only the local names of `uses` lines exist inside a spec",
14335                            spec.name
14336                        ),
14337                        Some(source.clone()),
14338                        Some(format!(
14339                            "Add `uses {name}`, or write the local name of the `uses` line that imports '{name}'."
14340                        )),
14341                        Some(spec),
14342                        None,
14343                    )]);
14344                }
14345                return Err(vec![Error::validation_with_context(
14346                    format!(
14347                        "Unknown parent '{parent}' for {site}. Parent must be defined before use. Valid primitive types are: boolean, measure, number, ratio, text, date, time"
14348                    ),
14349                    Some(source.clone()),
14350                    None::<String>,
14351                    Some(spec),
14352                    None,
14353                )]);
14354            }
14355            Err(vec![Error::validation_with_context(
14356                format!(
14357                    "Circular dependency detected in type resolution: {}::{}",
14358                    spec.name, name
14359                ),
14360                Some(source.clone()),
14361                None::<String>,
14362                Some(spec),
14363                None,
14364            )])
14365        }
14366        ParentType::Qualified { spec_alias, inner } => {
14367            let import_row = spec
14368                .data
14369                .iter()
14370                .find(|row| row.reference.segments.is_empty() && row.reference.name == *spec_alias);
14371            let spec_ref = import_row.and_then(|row| {
14372                row.value
14373                    .uses_spec_ref()
14374                    .cloned()
14375                    .or_else(|| discovery::data_instance_list_spec_ref(spec, row))
14376            });
14377            let Some(spec_ref) = spec_ref else {
14378                if spec.rules.iter().any(|rule| rule.name == *spec_alias) {
14379                    return Err(vec![Error::validation_with_context(
14380                        format!(
14381                            "'{spec_alias}' is a local rule, not a type path. Row fields in type position name a `uses` import or a `data` instance list, not a rule."
14382                        ),
14383                        Some(source.clone()),
14384                        None::<String>,
14385                        Some(spec),
14386                        None,
14387                    )]);
14388                }
14389                return Err(vec![Error::validation_with_context(
14390                    format!("'{spec_alias}' is not a `uses` import alias in this spec"),
14391                    Some(source.clone()),
14392                    Some(format!(
14393                        "Add `uses {spec_alias}: <target_spec>`, then write `{spec_alias}.<type>`."
14394                    )),
14395                    Some(spec),
14396                    None,
14397                )]);
14398            };
14399            let dynamic_effective = matches!(
14400                import_row.map(|row| &row.value),
14401                Some(ParsedDataValue::Import {
14402                    effective_ref: Some(_),
14403                    ..
14404                })
14405            );
14406            match inner.as_ref() {
14407                ParentType::Primitive { primitive: kind } => {
14408                    let lemma_type = Arc::clone(semantics::lemma_type_for_primitive(*kind));
14409                    Ok((
14410                        lemma_type.as_ref().specifications.clone(),
14411                        None,
14412                        None,
14413                        lemma_type,
14414                    ))
14415                }
14416                ParentType::Custom { name, .. } if dynamic_effective => {
14417                    union_dynamic_import_parent_type(
14418                        resolver,
14419                        spec,
14420                        source,
14421                        already_resolved,
14422                        &spec_ref,
14423                        spec_alias,
14424                        name.as_str(),
14425                    )
14426                }
14427                ParentType::Custom { name, .. } => {
14428                    let (_, target_spec) =
14429                        match resolver.resolve_spec_for_import(spec, &spec_ref, source, at) {
14430                            Ok(import_pair) => import_pair,
14431                            Err(error) => return Err(vec![error]),
14432                        };
14433                    let Some(resolved_spec_types) = already_resolved
14434                        .iter()
14435                        .find(|(_, imported, _)| discovery::same_loaded_spec(imported, target_spec))
14436                        .map(|(_, _, resolved_spec_types)| resolved_spec_types)
14437                    else {
14438                        return Err(vec![Error::validation_with_context(
14439                            format!(
14440                                "Cannot resolve type '{name}' from spec '{}' (via import '{spec_alias}'): spec '{}' failed type resolution",
14441                                target_spec.name, target_spec.name
14442                            ),
14443                            Some(source.clone()),
14444                            None::<String>,
14445                            Some(spec),
14446                            None,
14447                        )]);
14448                    };
14449                    let Some(parent_type) = resolved_spec_types.resolved.get(name.as_str()) else {
14450                        let type_exists = resolver
14451                            .data_types
14452                            .iter()
14453                            .find(|(s, _)| discovery::same_loaded_spec(s, target_spec))
14454                            .map(|(_, type_map)| type_map.contains_key(name.as_str()))
14455                            .unwrap_or(false);
14456                        if !type_exists {
14457                            return Err(vec![Error::validation_with_context(
14458                                format!(
14459                                    "Type '{name}' is not defined in spec '{}' (via import '{spec_alias}')",
14460                                    target_spec.name
14461                                ),
14462                                Some(source.clone()),
14463                                None::<String>,
14464                                Some(spec),
14465                                None,
14466                            )]);
14467                        }
14468                        return Err(vec![Error::validation_with_context(
14469                            format!(
14470                                "Circular dependency detected in type resolution: {}::{}",
14471                                target_spec.name, name
14472                            ),
14473                            Some(source.clone()),
14474                            None::<String>,
14475                            Some(spec),
14476                            None,
14477                        )]);
14478                    };
14479                    Ok((
14480                        parent_type.as_ref().specifications.clone(),
14481                        resolved_spec_types
14482                            .source_defaults
14483                            .get(name.as_str())
14484                            .cloned(),
14485                        resolved_spec_types
14486                            .source_value_defaults
14487                            .get(name.as_str())
14488                            .cloned(),
14489                        Arc::clone(parent_type),
14490                    ))
14491                }
14492                ParentType::Qualified { .. } => Err(vec![Error::validation_with_context(
14493                    "Nested qualified parent types are invalid",
14494                    Some(source.clone()),
14495                    None::<String>,
14496                    Some(spec),
14497                    None,
14498                )]),
14499                ParentType::Ranged { .. } => Err(vec![Error::validation_with_context(
14500                    "Nested ranged parent types are invalid",
14501                    Some(source.clone()),
14502                    None::<String>,
14503                    Some(spec),
14504                    None,
14505                )]),
14506                ParentType::List { .. } => Err(vec![Error::validation_with_context(
14507                    "Nested list parent types are invalid",
14508                    Some(source.clone()),
14509                    None::<String>,
14510                    Some(spec),
14511                    None,
14512                )]),
14513            }
14514        }
14515    }
14516}
14517
14518/// Shared interface type for `data x: alias.Type` when `alias` is a `uses … -> effective`
14519/// import: options values are the sorted union across every candidate body; `options.rule`
14520/// must be absent on every body or identical.
14521fn union_dynamic_import_parent_type(
14522    resolver: &TypeResolver<'_>,
14523    spec: &LemmaSpec,
14524    source: &Source,
14525    already_resolved: &ResolvedTypesMap<'_>,
14526    spec_ref: &ast::SpecRef,
14527    spec_alias: &str,
14528    type_name: &str,
14529) -> Result<ParentLookupOk, Vec<Error>> {
14530    let resolved_repo = match discovery::resolve_spec_ref_repository(
14531        resolver.context,
14532        spec_ref,
14533        Some(source),
14534        spec.name.as_str(),
14535        Some(spec),
14536    ) {
14537        Ok(resolved) => resolved,
14538        Err(error) => return Err(vec![error]),
14539    };
14540    let repository = match &resolved_repo.repository {
14541        Some(explicit) => Arc::clone(explicit),
14542        None => Arc::clone(resolver.registered_repository(spec)),
14543    };
14544    let dep_name = crate::parsing::ast::ascii_lowercase_logical_name(spec_ref.name.clone());
14545    let mut candidates: Vec<&Arc<LemmaType>> = Vec::new();
14546    for (repo, imported, resolved_types) in already_resolved {
14547        if !Arc::ptr_eq(repo, &repository) {
14548            continue;
14549        }
14550        if crate::parsing::ast::ascii_lowercase_logical_name(imported.name.clone()) != dep_name {
14551            continue;
14552        }
14553        let Some(parent_type) = resolved_types.resolved.get(type_name) else {
14554            let type_exists = resolver
14555                .data_types
14556                .iter()
14557                .find(|(s, _)| discovery::same_loaded_spec(s, imported))
14558                .map(|(_, type_map)| type_map.contains_key(type_name))
14559                .unwrap_or(false);
14560            if !type_exists {
14561                return Err(vec![Error::validation_with_context(
14562                    format!(
14563                        "Type '{type_name}' is not defined in spec '{}' (via import '{spec_alias}')",
14564                        imported.name
14565                    ),
14566                    Some(source.clone()),
14567                    None::<String>,
14568                    Some(spec),
14569                    None,
14570                )]);
14571            }
14572            return Err(vec![Error::validation_with_context(
14573                format!(
14574                    "Circular dependency detected in type resolution: {}::{type_name}",
14575                    imported.name
14576                ),
14577                Some(source.clone()),
14578                None::<String>,
14579                Some(spec),
14580                None,
14581            )]);
14582        };
14583        candidates.push(parent_type);
14584    }
14585    if candidates.is_empty() {
14586        return Err(vec![Error::validation_with_context(
14587            format!(
14588                "Cannot resolve type '{type_name}' from dependency '{}' (via import '{spec_alias}'): no candidate body has completed type resolution",
14589                spec_ref.name
14590            ),
14591            Some(source.clone()),
14592            None::<String>,
14593            Some(spec),
14594            None,
14595        )]);
14596    }
14597    let first = candidates[0];
14598    for later in &candidates[1..] {
14599        if !first.same_interface_ignoring_options(later) {
14600            return Err(vec![Error::validation_with_context(
14601                format!(
14602                    "'{spec_alias}.{type_name}' changes across versions of '{}' under `-> effective`",
14603                    spec_ref.name
14604                ),
14605                Some(source.clone()),
14606                Some(format!(
14607                    "Keep '{type_name}' interface-compatible across every temporal version of '{}', or pin the import to a literal date.",
14608                    spec_ref.name
14609                )),
14610                Some(spec),
14611                None,
14612            )]);
14613        }
14614        if first.options.rule != later.options.rule {
14615            return Err(vec![Error::validation_with_context(
14616                format!(
14617                    "'{spec_alias}.{type_name}' has different `-> options` rules across versions of '{}' under `-> effective`",
14618                    spec_ref.name
14619                ),
14620                Some(source.clone()),
14621                Some(
14622                    "Use the same options rule name on every version, or only inline `-> option` values."
14623                        .to_string(),
14624                ),
14625                Some(spec),
14626                None,
14627            )]);
14628        }
14629    }
14630    let mut unioned = first.as_ref().clone();
14631    let mut values: Vec<semantics::OptionValue> = Vec::new();
14632    for candidate in &candidates {
14633        for option in &candidate.options.values {
14634            if !values.iter().any(|seen| seen == option) {
14635                values.push(option.clone());
14636            }
14637        }
14638    }
14639    values.sort_by(|left, right| {
14640        left.value
14641            .value
14642            .to_string()
14643            .cmp(&right.value.value.to_string())
14644            .then_with(|| left.value.unit.cmp(&right.value.unit))
14645    });
14646    unioned.options.values = values;
14647    Ok((
14648        unioned.specifications.clone(),
14649        None,
14650        None,
14651        Arc::new(unioned),
14652    ))
14653}
14654
14655/// Order `type_names` so each type follows its local element parent. A cycle is a planning Error.
14656fn topo_sort_type_names(
14657    spec: &LemmaSpec,
14658    data_defs: &HashMap<String, DataTypeDef>,
14659    mut type_names: Vec<String>,
14660) -> Result<Vec<String>, Vec<Error>> {
14661    type_names.sort();
14662    if type_names.is_empty() {
14663        return Ok(type_names);
14664    }
14665    let type_index: HashMap<&str, usize> = type_names
14666        .iter()
14667        .enumerate()
14668        .map(|(index, name)| (name.as_str(), index))
14669        .collect();
14670    let type_count = type_names.len();
14671    let mut in_degree = vec![0usize; type_count];
14672    let mut dependents: Vec<Vec<usize>> = vec![Vec::new(); type_count];
14673    for (child_index, type_name) in type_names.iter().enumerate() {
14674        let custom_parent = match element_parent_type(&data_defs[type_name].parent) {
14675            ParentType::Custom { name, .. } => Some(name.as_str()),
14676            _ => None,
14677        };
14678        let Some(parent_name) = custom_parent else {
14679            continue;
14680        };
14681        let Some(&parent_index) = type_index.get(parent_name) else {
14682            continue;
14683        };
14684        if parent_index == child_index {
14685            continue;
14686        }
14687        in_degree[child_index] += 1;
14688        dependents[parent_index].push(child_index);
14689    }
14690    let mut queue: std::collections::VecDeque<usize> = (0..type_count)
14691        .filter(|&index| in_degree[index] == 0)
14692        .collect();
14693    let mut sorted = Vec::with_capacity(type_count);
14694    while let Some(index) = queue.pop_front() {
14695        sorted.push(type_names[index].clone());
14696        for &dependent_index in &dependents[index] {
14697            in_degree[dependent_index] -= 1;
14698            if in_degree[dependent_index] == 0 {
14699                queue.push_back(dependent_index);
14700            }
14701        }
14702    }
14703    if sorted.len() != type_count {
14704        let cycle_type_name = type_names
14705            .iter()
14706            .find(|name| in_degree[type_index[name.as_str()]] > 0)
14707            .expect("BUG: incomplete topo sort without cycle participant");
14708        return Err(vec![Error::validation_with_context(
14709            format!(
14710                "Circular dependency detected in type resolution: {}::{}",
14711                spec.name, cycle_type_name
14712            ),
14713            Some(data_defs[cycle_type_name].source.clone()),
14714            None::<String>,
14715            Some(spec),
14716            None,
14717        )]);
14718    }
14719    Ok(sorted)
14720}
14721
14722/// Split local type declarations into scalar types and list types.
14723///
14724/// A list type (`money list`, or a type extending one) is resolved after every scalar
14725/// pass has finished, so its element is the final element type.
14726fn partition_list_type_names(
14727    data_defs: &HashMap<String, DataTypeDef>,
14728) -> (Vec<String>, Vec<String>) {
14729    fn has_list(parent: &ParentType) -> bool {
14730        let mut current = parent;
14731        loop {
14732            match current {
14733                ParentType::List { .. } => return true,
14734                ParentType::Ranged { inner } | ParentType::Qualified { inner, .. } => {
14735                    current = inner.as_ref();
14736                }
14737                ParentType::Primitive { .. } | ParentType::Custom { .. } => return false,
14738            }
14739        }
14740    }
14741    let mut list_names: HashSet<String> = data_defs
14742        .iter()
14743        .filter(|(_, def)| has_list(&def.parent))
14744        .map(|(name, _)| name.clone())
14745        .collect();
14746    loop {
14747        let before = list_names.len();
14748        for (name, def) in data_defs {
14749            if let ParentType::Custom { name: parent, .. } = element_parent_type(&def.parent) {
14750                if list_names.contains(parent) {
14751                    list_names.insert(name.clone());
14752                }
14753            }
14754        }
14755        if list_names.len() == before {
14756            break;
14757        }
14758    }
14759    let scalar_names = data_defs
14760        .keys()
14761        .filter(|name| !list_names.contains(*name))
14762        .cloned()
14763        .collect();
14764    (scalar_names, list_names.into_iter().collect())
14765}
14766
14767type TypeMap = IndexMap<String, Arc<LemmaType>>;
14768
14769/// Whether `name` is a local data field or rule in `spec`.
14770fn local_data_or_rule(spec: &LemmaSpec, name: &str) -> bool {
14771    let is_data = spec
14772        .data
14773        .iter()
14774        .any(|d| d.reference.is_local() && d.reference.name == name);
14775    let is_rule = spec.rules.iter().any(|r| r.name == name);
14776    is_data || is_rule
14777}
14778
14779/// Whether `alias` is a `uses` import in `spec`.
14780fn is_uses_alias(spec: &LemmaSpec, alias: &str) -> bool {
14781    spec.data.iter().any(|d| {
14782        d.reference.is_local()
14783            && d.reference.name == alias
14784            && matches!(d.value, ParsedDataValue::Import { .. })
14785    })
14786}
14787
14788/// Classify a `-> unit <name>: <reference>` payload: scale (data/rule) vs compound
14789/// (unit path). Lookup is against local data/rules, the unit index, and uses aliases —
14790/// never against hop count alone. A name that is both a local data/rule and a
14791/// resolvable unit path is ambiguous and must Error.
14792fn classify_unit_payload_reference(
14793    reference: &ast::Reference,
14794    spec: &LemmaSpec,
14795    unit_index: &PlanningUnitIndex,
14796    resolved: &TypeMap,
14797) -> Result<UnitPayloadClass, String> {
14798    let local = reference.segments.is_empty() && local_data_or_rule(spec, &reference.name);
14799    let unit_ref = reference.to_string();
14800    let resolves_as_unit = unit_index
14801        .resolve_with_named_types(&unit_ref, resolved)
14802        .is_ok();
14803    if local && resolves_as_unit {
14804        return Err(format!(
14805            "unit scale reference '{reference}' is ambiguous: it names both a local data or rule and a unit path"
14806        ));
14807    }
14808    if local {
14809        return Ok(UnitPayloadClass::Scale);
14810    }
14811    if resolves_as_unit {
14812        return Ok(UnitPayloadClass::Compound);
14813    }
14814    if matches!(reference.segments.as_slice(), [alias] if is_uses_alias(spec, alias)) {
14815        return Ok(UnitPayloadClass::Scale);
14816    }
14817    Ok(UnitPayloadClass::Compound)
14818}
14819
14820enum UnitPayloadClass {
14821    Scale,
14822    Compound,
14823}
14824
14825/// Turn unit-path `UnitScale::Reference` payloads into compound factors before
14826/// decomposition. Scale references (data/rule, including `uses.member`) stay.
14827/// Returns planning errors for ambiguous bare names.
14828fn rewrite_unit_path_scale_payloads(
14829    spec: &LemmaSpec,
14830    resolved: &mut TypeMap,
14831    unit_index: &PlanningUnitIndex,
14832    type_sources: &HashMap<String, Source>,
14833) -> Vec<Error> {
14834    let mut errors = Vec::new();
14835    let type_names: Vec<String> = resolved.keys().cloned().collect();
14836    for type_name in type_names {
14837        let lemma_type = resolved
14838            .get(&type_name)
14839            .expect("BUG: type_name comes from resolved keys");
14840        let TypeSpecification::Measure { units, .. } = &lemma_type.specifications else {
14841            continue;
14842        };
14843        let mut compounds: Vec<(usize, String)> = Vec::new();
14844        for (idx, unit) in units.iter().enumerate() {
14845            let Some((segments, name)) = unit.scale_reference_parts() else {
14846                continue;
14847            };
14848            if unit.resolved_scale_target().is_some() {
14849                continue;
14850            }
14851            let reference = ast::Reference {
14852                segments: segments.to_vec(),
14853                name: name.to_string(),
14854            };
14855            match classify_unit_payload_reference(&reference, spec, unit_index, resolved) {
14856                Ok(UnitPayloadClass::Scale) => {}
14857                Ok(UnitPayloadClass::Compound) => {
14858                    compounds.push((idx, reference.to_string()));
14859                }
14860                Err(message) => {
14861                    let source = type_sources.get(&type_name).cloned().unwrap_or_else(|| {
14862                        panic!(
14863                            "BUG: measure type '{type_name}' with a unit payload has no type source"
14864                        )
14865                    });
14866                    errors.push(Error::validation(message, Some(source), None::<String>));
14867                }
14868            }
14869        }
14870        if compounds.is_empty() {
14871            continue;
14872        }
14873        let mut owned = lemma_type.as_ref().clone();
14874        let TypeSpecification::Measure { units, .. } = &mut owned.specifications else {
14875            unreachable!("BUG: measure type still a measure after clone");
14876        };
14877        for (idx, referenced) in compounds {
14878            let name = units.0[idx].name.clone();
14879            units.0[idx] =
14880                MeasureUnit::from_decimal_factor(name, Decimal::ONE, vec![(referenced, 1)])
14881                    .expect("BUG: factor 1 always converts to a rational");
14882        }
14883        resolved.insert(type_name, Arc::new(owned));
14884    }
14885    errors
14886}
14887
14888fn resolve_measure_decompositions(
14889    spec_name: &str,
14890    this_spec: &SpecVersionKey,
14891    mut resolved: TypeMap,
14892    unit_index: &PlanningUnitIndex,
14893    type_sources: &HashMap<String, Source>,
14894) -> (TypeMap, Vec<Error>) {
14895    let mut errors: Vec<Error> = Vec::new();
14896
14897    let source_for = |type_name: &str| -> Option<Source> { type_sources.get(type_name).cloned() };
14898
14899    let base_type_names: Vec<String> = resolved
14900        .iter()
14901        .filter_map(|(name, lt)| {
14902            if let TypeSpecification::Measure { units, .. } = &lt.specifications {
14903                if units.iter().all(measure_unit_is_simple) {
14904                    return Some(name.clone());
14905                }
14906            }
14907            None
14908        })
14909        .collect();
14910
14911    for type_name in &base_type_names {
14912        let owned = unwrap_unique_resolved_type(
14913            resolved
14914                .shift_remove(type_name)
14915                .expect("BUG: type_name comes from resolved's own keys"),
14916        );
14917        let base_decomp = declared_measure_decomposition(&owned);
14918
14919        let reserved_collision = reserved_calendar_units_in_measure(&owned);
14920        if !reserved_collision.is_empty() && !owned.has_trait_calendar() {
14921            errors.push(Error::validation(
14922                format!(
14923                    "In spec '{}': measure type '{}' declares unit(s) {:?} which are reserved calendar unit names.",
14924                    spec_name, type_name, reserved_collision
14925                ),
14926                source_for(type_name),
14927                None::<String>,
14928            ));
14929            resolved.insert(type_name.clone(), Arc::new(owned));
14930            continue;
14931        }
14932
14933        let updated = owned.map_measure(|units, _| {
14934            let units = units.map(|u| u.with_decomposition(base_decomp.clone()));
14935            (units, Some(base_decomp.clone()))
14936        });
14937        resolved.insert(type_name.clone(), Arc::new(updated));
14938    }
14939
14940    for lemma_type in resolved.values_mut() {
14941        let TypeSpecification::MeasureRange { units, .. } = &lemma_type.specifications else {
14942            continue;
14943        };
14944        if units.is_empty() {
14945            continue;
14946        }
14947        let family = lemma_type
14948            .family()
14949            .expect("BUG: measure range declaring units must be a named custom type");
14950        let range_decomp: BaseMeasureVector =
14951            [(semantics::Dimension::Family(family.clone()), 1i32)]
14952                .into_iter()
14953                .collect();
14954        let TypeSpecification::MeasureRange {
14955            units,
14956            decomposition,
14957            ..
14958        } = &mut Arc::get_mut(lemma_type)
14959            .expect("BUG: resolved LemmaType Arc unique during type resolution")
14960            .specifications
14961        else {
14962            unreachable!("BUG: matched MeasureRange above");
14963        };
14964        *units = std::mem::take(units).map(|u| u.with_decomposition(range_decomp.clone()));
14965        *decomposition = Some(range_decomp);
14966    }
14967
14968    let mut lookup = UnitDecompLookup::new();
14969
14970    for (unit_name, owner) in unit_index.iter_owners() {
14971        let unit_name = unit_name.to_string();
14972        let lemma_type = owner.lemma_type(&resolved);
14973        if let TypeSpecification::Measure {
14974            decomposition: Some(decomposition),
14975            units,
14976            ..
14977        } = &lemma_type.specifications
14978        {
14979            let measure_name = local_owner_name(owner);
14980            let unit = units
14981                .iter()
14982                .find(|u| u.name == unit_name)
14983                .unwrap_or_else(|| {
14984                    panic!(
14985                        "BUG: unit_name '{}' from unit_index must be in its owning type's units",
14986                        unit_name
14987                    )
14988                });
14989            let Some(factor) = unit.literal_factor().cloned() else {
14990                continue;
14991            };
14992            // Unique bare names only in decomp lookup; ambiguous factors resolve via UnitIndex.
14993            if unit_index.has_unique_owner(unit_name.as_str()) {
14994                lookup.insert(
14995                    unit_name.clone(),
14996                    (measure_name, decomposition.clone(), factor),
14997                );
14998            }
14999        }
15000    }
15001
15002    for (type_name, lemma_type) in resolved.iter() {
15003        if let TypeSpecification::Measure {
15004            units,
15005            decomposition: Some(decomposition),
15006            ..
15007        } = &lemma_type.specifications
15008        {
15009            let is_defining_type = lemma_type.measure_family().is_some_and(|family| {
15010                family.is_declared_in(this_spec) && family.name == *type_name
15011            });
15012            if !is_defining_type {
15013                continue;
15014            }
15015            for unit in units.iter() {
15016                let declared_by_other_family =
15017                    unit_index.owners_for(&unit.name).iter().any(|owner| {
15018                        !owner
15019                            .lemma_type(&resolved)
15020                            .same_measure_family(lemma_type.as_ref())
15021                    });
15022                if declared_by_other_family {
15023                    continue;
15024                }
15025                let Some(factor) = unit.literal_factor().cloned() else {
15026                    continue;
15027                };
15028                lookup.insert(
15029                    unit.name.clone(),
15030                    (Some(type_name.clone()), decomposition.clone(), factor),
15031                );
15032            }
15033        }
15034    }
15035
15036    let derived_measure_type_names_unsorted: Vec<String> = resolved
15037        .iter()
15038        .filter_map(|(name, lemma_type)| {
15039            if let TypeSpecification::Measure { units, .. } = &lemma_type.specifications {
15040                if units.iter().any(|unit| !measure_unit_is_simple(unit)) {
15041                    return Some(name.clone());
15042                }
15043            }
15044            None
15045        })
15046        .collect();
15047
15048    let derived_measure_type_names = match sort_derived_measure_types_for_resolution(
15049        spec_name,
15050        derived_measure_type_names_unsorted,
15051        &resolved,
15052        &lookup,
15053        unit_index,
15054        type_sources,
15055    ) {
15056        Ok(sorted) => sorted,
15057        Err(error) => {
15058            errors.push(error);
15059            return (resolved, errors);
15060        }
15061    };
15062
15063    for type_name in &derived_measure_type_names {
15064        let type_source = source_for(type_name);
15065
15066        let (units_snapshot, already_resolved_body) = match &resolved[type_name].specifications {
15067            TypeSpecification::Measure {
15068                units,
15069                decomposition,
15070                ..
15071            } => (units.clone(), decomposition.clone()),
15072            _ => continue,
15073        };
15074
15075        // Post-resolve import clones already carry `decomposition: Some`. Re-running
15076        // `resolve_compound_unit` on inherited units squares non-1 scales (eur/hour → 1/3600²).
15077        // Pure aliases (no locally owned units) leave the clone intact before the unit walk.
15078        let body_already_resolved = already_resolved_body.is_some();
15079        let unit_owned_locally = |unit_name: &str| {
15080            unit_index.owners_for(unit_name).iter().any(|owner| {
15081                matches!(
15082                    owner,
15083                    PlanningOwner::Local {
15084                        type_name: local_name
15085                    } if local_name == type_name
15086                )
15087            })
15088        };
15089        if body_already_resolved && !units_snapshot.iter().any(|u| unit_owned_locally(&u.name)) {
15090            continue;
15091        }
15092
15093        let mut resolved_type_decomp: Option<BaseMeasureVector> = already_resolved_body;
15094        let mut unit_errors: Vec<Error> = Vec::new();
15095        let mut resolved_unit_factors: Vec<Option<RationalInteger>> =
15096            vec![None; units_snapshot.len()];
15097        let mut resolve_to_simple: Vec<bool> = vec![false; units_snapshot.len()];
15098
15099        for (unit_idx, unit) in units_snapshot.iter().enumerate() {
15100            if calendar_unit_factor(&unit.name).is_some()
15101                && !resolved[type_name].has_trait_calendar()
15102            {
15103                unit_errors.push(Error::validation(
15104                    format!(
15105                        "In spec '{}': measure type '{}' declares unit '{}' which is a reserved calendar unit name.",
15106                        spec_name, type_name, unit.name
15107                    ),
15108                    type_source.clone(),
15109                    None::<String>,
15110                ));
15111                continue;
15112            }
15113            if measure_unit_is_simple(unit) {
15114                let simple_decomp = declared_measure_decomposition(&resolved[type_name]);
15115
15116                if let Some(existing) = &resolved_type_decomp {
15117                    if existing != &simple_decomp {
15118                        unit_errors.push(Error::validation(
15119                            format!(
15120                                "In spec '{}': measure type '{}' has inconsistent unit decompositions. \
15121                                 Unit '{}' is a simple unit (decomposition {}) but other units \
15122                                 have decomposition {}.",
15123                                spec_name,
15124                                type_name,
15125                                unit.name,
15126                                semantics::format_signature_operator_style(
15127                                    &semantics::base_measure_vector_as_signature(&simple_decomp)
15128                                ),
15129                                semantics::format_signature_operator_style(
15130                                    &semantics::base_measure_vector_as_signature(existing)
15131                                )
15132                            ),
15133                            type_source.clone(),
15134                            None::<String>,
15135                        ));
15136                    }
15137                } else {
15138                    resolved_type_decomp = Some(simple_decomp);
15139                }
15140
15141                if let Some(factor) = unit.literal_factor() {
15142                    resolved_unit_factors[unit_idx] = Some(factor.clone());
15143                }
15144                continue;
15145            }
15146
15147            if body_already_resolved && !unit_owned_locally(&unit.name) {
15148                if let Some(factor) = unit.literal_factor() {
15149                    resolved_unit_factors[unit_idx] = Some(factor.clone());
15150                }
15151                continue;
15152            }
15153
15154            match resolve_compound_unit(
15155                spec_name,
15156                type_name,
15157                unit,
15158                &lookup,
15159                unit_index,
15160                &resolved,
15161                type_source.as_ref(),
15162            ) {
15163                Ok((unit_decomp, derived_factor)) => {
15164                    if let [(referenced, 1)] = unit.derived_measure_factors.as_slice() {
15165                        let (bare_name, owner) = unit_index
15166                            .resolve_owner(referenced, &resolved)
15167                            .unwrap_or_else(|err| {
15168                                panic!(
15169                                    "BUG: factor '{referenced}' resolved for its conversion but not for its owner: {err}"
15170                                )
15171                            });
15172                        let owning_type = owner.lemma_type(&resolved);
15173                        let owner_family = owning_type
15174                            .measure_family()
15175                            .expect("BUG: resolved unit owner is not a named measure");
15176                        let declaring_family = resolved[type_name]
15177                            .measure_family()
15178                            .expect("BUG: declaring measure has no family");
15179                        if owner_family != declaring_family {
15180                            unit_errors.push(Error::validation(
15181                                format!(
15182                                    "In spec '{spec_name}': unit '{}' in measure type '{type_name}' cannot use a single unit from another measure. '{bare_name}' belongs to '{}'.",
15183                                    unit.name, owner_family.name
15184                                ),
15185                                type_source.clone(),
15186                                None::<String>,
15187                            ));
15188                        }
15189                    }
15190                    // Same-family scale (`workday: 8 hour` on duration): resolved decomp matches
15191                    // the declaring type's declared decomp → simple identity + expanded factor.
15192                    if unit_decomp == declared_measure_decomposition(resolved[type_name].as_ref()) {
15193                        resolve_to_simple[unit_idx] = true;
15194                    }
15195                    if let Some(existing) = &resolved_type_decomp {
15196                        if existing != &unit_decomp {
15197                            unit_errors.push(Error::validation(
15198                                format!(
15199                                    "In spec '{}': measure type '{}' has inconsistent unit \
15200                                     decompositions. Unit '{}' resolved to {:?} but other units \
15201                                     resolved to {:?}. All units of a measure must measure the same \
15202                                     physical measure.",
15203                                    spec_name, type_name, unit.name, unit_decomp, existing
15204                                ),
15205                                type_source.clone(),
15206                                None::<String>,
15207                            ));
15208                        }
15209                    } else {
15210                        resolved_type_decomp = Some(unit_decomp);
15211                    }
15212
15213                    resolved_unit_factors[unit_idx] = Some(derived_factor);
15214                }
15215                Err(err) => unit_errors.push(err),
15216            }
15217        }
15218
15219        if !unit_errors.is_empty() {
15220            errors.extend(unit_errors);
15221            continue;
15222        }
15223
15224        let type_decomp = match resolved_type_decomp {
15225            Some(d) => d,
15226            None => continue,
15227        };
15228
15229        let owned = unwrap_unique_resolved_type(
15230            resolved
15231                .shift_remove(type_name)
15232                .expect("BUG: type_name comes from resolved's own keys"),
15233        );
15234
15235        let updated = owned.map_measure(|units, _| {
15236            let units = MeasureUnits(
15237                units
15238                    .0
15239                    .into_iter()
15240                    .enumerate()
15241                    .map(|(idx, u)| {
15242                        let u = u.with_decomposition(type_decomp.clone());
15243                        let u = match resolved_unit_factors[idx].clone() {
15244                            Some(factor) => u.with_factor(factor),
15245                            None => u,
15246                        };
15247                        if resolve_to_simple[idx] {
15248                            u.with_derived_measure_factors(Vec::new())
15249                        } else {
15250                            u
15251                        }
15252                    })
15253                    .collect(),
15254            );
15255            (units, Some(type_decomp.clone()))
15256        });
15257
15258        if let TypeSpecification::Measure { units, .. } = &updated.specifications {
15259            for unit in units.iter() {
15260                // Import clones: only additive (locally owned) units may claim bare names in
15261                // lookup. Inherited compounds stay keyed to the defining import owner.
15262                if body_already_resolved && !unit_owned_locally(&unit.name) {
15263                    continue;
15264                }
15265                let Some(factor) = unit.literal_factor().cloned() else {
15266                    continue;
15267                };
15268                lookup.insert(
15269                    unit.name.clone(),
15270                    (Some(type_name.clone()), type_decomp.clone(), factor),
15271                );
15272            }
15273        }
15274        resolved.insert(type_name.clone(), Arc::new(updated));
15275    }
15276
15277    let resolved = canonicalize_unit_signatures(resolved);
15278
15279    (resolved, errors)
15280}
15281
15282/// Functional wrapper around the still-`&mut`-based
15283/// [`semantics::finalize_measure_unit_constraint_magnitudes`]. Consumes the
15284/// `LemmaType`, performs the validation, and returns either the updated value
15285/// or an error message (the type is dropped on failure).
15286fn finalize_lemma_measure_magnitudes(
15287    lemma_type: LemmaType,
15288    declared_suggestion: Option<&BoundValueKind>,
15289    type_name: &str,
15290) -> Result<LemmaType, String> {
15291    let LemmaType {
15292        name,
15293        mut specifications,
15294        extends,
15295        options,
15296    } = lemma_type;
15297    semantics::finalize_measure_unit_constraint_magnitudes(
15298        &mut specifications,
15299        declared_suggestion.map(|bound| &bound.value),
15300        type_name,
15301    )?;
15302    Ok(LemmaType {
15303        name,
15304        specifications,
15305        extends,
15306        options,
15307    })
15308}
15309
15310fn finalize_measure_magnitudes_in_resolved(
15311    resolved: IndexMap<String, Arc<LemmaType>>,
15312    declared_suggestions: &IndexMap<String, BoundValueKind>,
15313    type_sources: &HashMap<String, Source>,
15314    spec_name: &str,
15315    spec: &LemmaSpec,
15316) -> (IndexMap<String, Arc<LemmaType>>, Vec<Error>) {
15317    resolved.into_iter().fold(
15318        (IndexMap::new(), Vec::new()),
15319        |(mut acc, mut errors), (type_name, arc)| {
15320            let source = type_sources.get(&type_name).cloned().unwrap_or_else(|| {
15321                unreachable!(
15322                    "BUG: resolved type '{}' has no corresponding DataTypeDef in spec '{}'",
15323                    type_name, spec_name
15324                )
15325            });
15326            let owned = unwrap_unique_resolved_type(arc);
15327            let lemma_type = match finalize_lemma_measure_magnitudes(
15328                owned.clone(),
15329                declared_suggestions.get(&type_name),
15330                &type_name,
15331            ) {
15332                Ok(lt) => lt,
15333                Err(message) => {
15334                    errors.push(Error::validation_with_context(
15335                        format!(
15336                            "Type '{}' has invalid measure unit constraints: {}",
15337                            type_name, message
15338                        ),
15339                        Some(source),
15340                        None::<String>,
15341                        Some(spec),
15342                        None,
15343                    ));
15344                    owned
15345                }
15346            };
15347            acc.insert(type_name, Arc::new(lemma_type));
15348            (acc, errors)
15349        },
15350    )
15351}
15352
15353/// Populate every unit's `derived_measure_factors` to its canonical symbolic signature.
15354///
15355/// - Compound declarations keep their parsed factors, canonicalized.
15356/// - Simple units (empty `derived_measure_factors`) get `[(unit.name, 1)]`.
15357///
15358/// After this pass every unit carries a non-empty, canonical-form signature so cross-type
15359/// arithmetic can combine them deterministically and `signature_index` can be built.
15360fn canonicalize_unit_signatures(types: TypeMap) -> TypeMap {
15361    types
15362        .into_iter()
15363        .map(|(name, arc)| {
15364            (
15365                name,
15366                Arc::new(canonicalize_lemma_unit_signatures(
15367                    unwrap_unique_resolved_type(arc),
15368                )),
15369            )
15370        })
15371        .collect()
15372}
15373
15374fn canonicalize_lemma_unit_signatures(lemma_type: LemmaType) -> LemmaType {
15375    lemma_type.map_measure(|units, decomposition| {
15376        let units = units.map(canonicalize_unit_for_measure);
15377        (units, decomposition)
15378    })
15379}
15380
15381fn reserved_calendar_units_in_measure(lemma_type: &LemmaType) -> Vec<String> {
15382    let TypeSpecification::Measure { units, .. } = &lemma_type.specifications else {
15383        return Vec::new();
15384    };
15385    units
15386        .iter()
15387        .filter(|u| calendar_unit_factor(&u.name).is_some())
15388        .map(|u| u.name.clone())
15389        .collect()
15390}
15391
15392fn measure_unit_is_simple(unit: &MeasureUnit) -> bool {
15393    unit.derived_measure_factors.is_empty()
15394        || (unit.derived_measure_factors.len() == 1
15395            && unit.derived_measure_factors[0].0 == unit.name
15396            && unit.derived_measure_factors[0].1 == 1)
15397}
15398
15399fn canonicalize_unit_for_measure(unit: MeasureUnit) -> MeasureUnit {
15400    let factors = if measure_unit_is_simple(&unit) {
15401        vec![(unit.name.clone(), 1)]
15402    } else {
15403        canonicalize_signature(&unit.derived_measure_factors)
15404    };
15405    unit.with_derived_measure_factors(factors)
15406}
15407
15408fn resolve_compound_unit(
15409    spec_name: &str,
15410    declaring_type_name: &str,
15411    unit: &MeasureUnit,
15412    lookup: &UnitDecompLookup,
15413    unit_index: &PlanningUnitIndex,
15414    resolved: &IndexMap<String, Arc<LemmaType>>,
15415    source: Option<&Source>,
15416) -> Result<(BaseMeasureVector, RationalInteger), Error> {
15417    use crate::computation::bigint::BigInt;
15418    use crate::computation::rational::{checked_mul, checked_pow_integer};
15419
15420    let mut result: BaseMeasureVector = BaseMeasureVector::new();
15421    if unit.is_dynamic() {
15422        return Err(Error::validation(
15423            format!(
15424                "In spec '{spec_name}': unit '{}' in measure type '{declaring_type_name}' cannot be used in a compound unit expression because its conversion factor is a reference.",
15425                unit.name
15426            ),
15427            source.cloned(),
15428            None::<String>,
15429        ));
15430    }
15431    let mut derived_factor = unit.require_literal_factor().clone();
15432
15433    for (measure_ref, exponent) in &unit.derived_measure_factors {
15434        let (owning_measure_name, owning_decomp, unit_factor) = if let Some(entry) =
15435            lookup.get(measure_ref.as_str())
15436        {
15437            (entry.0.clone(), entry.1.clone(), entry.2.clone())
15438        } else {
15439            let (bare, owner) = unit_index
15440                .resolve_owner(measure_ref, resolved)
15441                .map_err(|err| {
15442                    Error::validation(
15443                        format!(
15444                            "In spec '{}': unit '{}' in measure type '{}' references '{}': {}. \
15445                             Add `uses <spec>` (or declare the owning measure type in this spec) \
15446                             so its units are in scope.",
15447                            spec_name, unit.name, declaring_type_name, measure_ref, err
15448                        ),
15449                        source.cloned(),
15450                        None::<String>,
15451                    )
15452                })?;
15453            let owning_type = owner.lemma_type(resolved);
15454            let TypeSpecification::Measure {
15455                decomposition: Some(decomposition),
15456                units,
15457                ..
15458            } = &owning_type.specifications
15459            else {
15460                return Err(Error::validation(
15461                    format!(
15462                        "In spec '{}': unit '{}' in measure type '{}' references '{}' which did not resolve to a measure unit",
15463                        spec_name, unit.name, declaring_type_name, measure_ref
15464                    ),
15465                    source.cloned(),
15466                    None::<String>,
15467                ));
15468            };
15469            let referenced = units.iter().find(|u| u.name == bare).unwrap_or_else(|| {
15470                panic!(
15471                    "BUG: resolved unit '{}' must be declared on type '{}'",
15472                    bare,
15473                    owning_type.name()
15474                )
15475            });
15476            let Some(factor) = referenced.literal_factor().cloned() else {
15477                return Err(Error::validation(
15478                    format!(
15479                        "In spec '{spec_name}': unit '{}' in measure type '{declaring_type_name}' cannot use unit '{bare}' in a compound expression because its conversion factor is a reference.",
15480                        unit.name
15481                    ),
15482                    source.cloned(),
15483                    None::<String>,
15484                ));
15485            };
15486            (local_owner_name(owner), decomposition.clone(), factor)
15487        };
15488        let owning_decomp = &owning_decomp;
15489        let unit_factor = &unit_factor;
15490
15491        if owning_measure_name.as_deref() == Some(declaring_type_name) {
15492            return Err(Error::validation(
15493                format!(
15494                    "In spec '{}': unit '{}' in measure type '{}' references unit '{}' which \
15495                     belongs to the same measure type. A measure cannot reference its own units \
15496                     in a compound expression.",
15497                    spec_name, unit.name, declaring_type_name, measure_ref
15498                ),
15499                source.cloned(),
15500                None::<String>,
15501            ));
15502        }
15503
15504        for (dim, &dim_exp) in owning_decomp {
15505            let combined = dim_exp
15506                .checked_mul(*exponent)
15507                .and_then(|value| accumulate(&mut result, dim, value));
15508            if combined.is_none() {
15509                return Err(unit_exponent_limit_error(
15510                    spec_name,
15511                    &unit.name,
15512                    declaring_type_name,
15513                    source,
15514                ));
15515            }
15516        }
15517
15518        let component_contribution =
15519            checked_pow_integer(unit_factor, &BigInt::from_i64(i64::from(*exponent))).map_err(
15520                |error| {
15521                    overflow_to_validation_error(
15522                        spec_name,
15523                        &unit.name,
15524                        declaring_type_name,
15525                        measure_ref,
15526                        error,
15527                        source,
15528                    )
15529                },
15530            )?;
15531        derived_factor =
15532            checked_mul(&derived_factor, &component_contribution).map_err(|error| {
15533                overflow_to_validation_error(
15534                    spec_name,
15535                    &unit.name,
15536                    declaring_type_name,
15537                    measure_ref,
15538                    error,
15539                    source,
15540                )
15541            })?;
15542    }
15543
15544    Ok((result, derived_factor))
15545}
15546
15547fn overflow_to_validation_error(
15548    spec_name: &str,
15549    unit_name: &str,
15550    declaring_type_name: &str,
15551    measure_ref: &str,
15552    failure: NumericFailure,
15553    source: Option<&Source>,
15554) -> Error {
15555    Error::validation(
15556        format!(
15557            "In spec '{}': unit '{}' in measure type '{}' overflowed while combining '{}': {}",
15558            spec_name, unit_name, declaring_type_name, measure_ref, failure
15559        ),
15560        source.cloned(),
15561        None::<String>,
15562    )
15563}
15564
15565fn unit_exponent_limit_error(
15566    spec_name: &str,
15567    unit_name: &str,
15568    declaring_type_name: &str,
15569    source: Option<&Source>,
15570) -> Error {
15571    Error::validation(
15572        format!(
15573            "In spec '{}': unit '{}' in measure type '{}' has a unit exponent exceeding the maximum of {}",
15574            spec_name,
15575            unit_name,
15576            declaring_type_name,
15577            MAX_UNIT_EXPONENT
15578        ),
15579        source.cloned(),
15580        None::<String>,
15581    )
15582}
15583
15584/// `None` when the accumulated exponent leaves [`crate::limits::MAX_UNIT_EXPONENT`].
15585fn accumulate(
15586    result: &mut BaseMeasureVector,
15587    dim: &semantics::Dimension,
15588    value: i32,
15589) -> Option<()> {
15590    let entry = result.entry(dim.clone()).or_insert(0);
15591    *entry = entry.checked_add(value)?;
15592    if entry.unsigned_abs() > MAX_UNIT_EXPONENT.unsigned_abs() {
15593        return None;
15594    }
15595    if *entry == 0 {
15596        result.remove(dim);
15597    }
15598    Some(())
15599}
15600
15601#[cfg(test)]
15602mod tests {
15603    use super::*;
15604
15605    use crate::parsing::ast::{BooleanValue, Reference, Span, Value};
15606
15607    #[test]
15608    fn nodes_on_cycles_skips_nodes_that_only_depend_on_a_cycle() {
15609        // a <-> b is a cycle, c depends on it, d sits between it and the cycle e <-> f.
15610        let edges: BTreeMap<&str, Vec<&str>> = BTreeMap::from([
15611            ("a", vec!["b"]),
15612            ("b", vec!["a"]),
15613            ("c", vec!["a"]),
15614            ("d", vec!["b"]),
15615            ("e", vec!["d", "f"]),
15616            ("f", vec!["e"]),
15617            ("g", vec!["g"]),
15618        ]);
15619        let unsorted: Vec<&&str> = edges.keys().collect();
15620        let on_cycles = nodes_on_cycles(&unsorted, |node| edges[node].iter().collect());
15621        assert_eq!(on_cycles, vec![&"a", &"b", &"e", &"f", &"g"]);
15622    }
15623
15624    fn test_source() -> Source {
15625        Source::new(
15626            crate::parsing::source::SourceType::Volatile,
15627            Span {
15628                start: 0,
15629                end: 0,
15630                line: 1,
15631                col: 0,
15632            },
15633        )
15634    }
15635
15636    fn build_graph(
15637        main_spec: &LemmaSpec,
15638        all_specs: &[LemmaSpec],
15639    ) -> Result<Graph<'static>, Vec<Error>> {
15640        use crate::engine::Context;
15641        use crate::planning::discovery;
15642
15643        let mut ctx = Context::new();
15644        let repository = ctx.workspace();
15645        for s in all_specs {
15646            ctx.insert_spec(Arc::clone(&repository), s.clone())?;
15647        }
15648        let effective = EffectiveDate::from_option(main_spec.effective_from().cloned());
15649        let ctx = Box::leak(Box::new(ctx));
15650        let repository = ctx.workspace();
15651        let main_spec_ref = ctx
15652            .spec_set(&repository, main_spec.name.as_str())
15653            .and_then(|ss| ss.get_exact(main_spec.effective_from()))
15654            .expect("main_spec must be in all_specs");
15655        let limits = Box::leak(Box::new(crate::ResourceLimits::default()));
15656        let ordered_dependencies = discovery::discover_dependency_order(
15657            ctx,
15658            &repository,
15659            main_spec_ref,
15660            &effective,
15661            limits,
15662        )
15663        .map_err(|e| match e {
15664            discovery::DependencyDiscoveryError::Cycle(es)
15665            | discovery::DependencyDiscoveryError::Other(es) => es,
15666        })?;
15667        let ordered_dependencies = Box::leak(Box::new(ordered_dependencies));
15668        let mut interners = instance_list::PassInterners::default();
15669        match Graph::build(
15670            ctx,
15671            &repository,
15672            main_spec_ref,
15673            ordered_dependencies,
15674            &effective,
15675            limits,
15676            &mut interners,
15677        ) {
15678            Ok((graph, _)) => Ok(graph),
15679            Err(errors) => Err(errors),
15680        }
15681    }
15682
15683    fn create_test_spec(name: &str) -> LemmaSpec {
15684        LemmaSpec::new(name.to_string())
15685    }
15686
15687    fn create_literal_data(name: &str, value: Value) -> LemmaData {
15688        LemmaData {
15689            reference: Reference {
15690                segments: Vec::new(),
15691                name: name.to_string(),
15692            },
15693            value: ParsedDataValue::Definition {
15694                base: None,
15695                constraints: None,
15696                value: Some(value),
15697
15698                list: None,
15699            },
15700            source_location: test_source(),
15701        }
15702    }
15703
15704    fn create_literal_expr(value: Value) -> ast::Expression {
15705        ast::Expression::leaf(ast::ExpressionKind::Literal(value), Some(test_source()))
15706    }
15707
15708    #[test]
15709    fn dotted_data_definition_rows_are_not_bindings() {
15710        // Bindings live on `uses` blocks only; dotted `LemmaData` rows are not binding paths.
15711        let mut spec = create_test_spec("test");
15712        spec = spec.add_data(create_literal_data("x", Value::Number(1.into())));
15713
15714        spec = spec.add_data(LemmaData {
15715            reference: Reference::from_path(vec!["x".to_string(), "y".to_string()]),
15716            value: ParsedDataValue::Definition {
15717                base: None,
15718                constraints: None,
15719                value: Some(Value::Number(2.into())),
15720
15721                list: None,
15722            },
15723            source_location: test_source(),
15724        });
15725
15726        let result = build_graph(&spec, &[spec.clone()]);
15727        assert!(
15728            result.is_ok(),
15729            "dotted definition rows are ignored by binding collection"
15730        );
15731    }
15732
15733    #[test]
15734    fn programmatic_ast_without_source_location_is_validation_error() {
15735        let mut spec = create_test_spec("test");
15736        spec = spec.add_rule(LemmaRule {
15737            name: "no_location".to_string(),
15738            expression: ast::Expression::leaf(
15739                ast::ExpressionKind::Literal(Value::Number(1.into())),
15740                None,
15741            ),
15742            unless_clauses: Vec::new(),
15743            source_location: test_source(),
15744        });
15745
15746        let result = build_graph(&spec, &[spec.clone()]);
15747        let errors = result.expect_err("missing source_location must be a planning error");
15748        assert!(
15749            errors
15750                .iter()
15751                .any(|e| e.to_string().contains("source location")),
15752            "expected source-location boundary error, got: {errors:?}"
15753        );
15754    }
15755
15756    #[test]
15757    fn programmatic_ast_nested_expression_without_source_location_is_validation_error() {
15758        let mut table = ast::ExprTable::default();
15759        let first = table.push(
15760            ast::ExpressionKind::Literal(Value::Number(2.into())),
15761            Some(test_source()),
15762        );
15763        let operand = table.push(ast::ExpressionKind::Literal(Value::Number(1.into())), None);
15764        let root = table.push(
15765            ast::ExpressionKind::Arithmetic {
15766                first,
15767                steps: vec![ast::ChainStep {
15768                    op: ast::ArithmeticComputation::Add,
15769                    operand,
15770                    source_location: test_source(),
15771                }],
15772            },
15773            Some(test_source()),
15774        );
15775        let outer = ast::Expression::new(table, root);
15776        let mut spec = create_test_spec("test");
15777        spec = spec.add_rule(LemmaRule {
15778            name: "nested_no_location".to_string(),
15779            expression: outer,
15780            unless_clauses: Vec::new(),
15781            source_location: test_source(),
15782        });
15783
15784        let result = build_graph(&spec, &[spec.clone()]);
15785        let errors = result.expect_err("nested missing source_location must be a planning error");
15786        assert!(
15787            errors
15788                .iter()
15789                .any(|e| e.to_string().contains("source location")),
15790            "expected source-location boundary error, got: {errors:?}"
15791        );
15792    }
15793
15794    #[test]
15795    fn should_reject_data_and_rule_name_collision() {
15796        // Higher-standard language rule: data and rule names should not collide.
15797        // It's ambiguous for humans and leads to confusing error messages.
15798        //
15799        // This is currently expected to FAIL until the language enforces it.
15800        let mut spec = create_test_spec("test");
15801        spec = spec.add_data(create_literal_data("x", Value::Number(1.into())));
15802        spec = spec.add_rule(LemmaRule {
15803            name: "x".to_string(),
15804            expression: create_literal_expr(Value::Number(2.into())),
15805            unless_clauses: Vec::new(),
15806            source_location: test_source(),
15807        });
15808
15809        let result = build_graph(&spec, &[spec.clone()]);
15810        assert!(
15811            result.is_err(),
15812            "Data and rule name collisions should be rejected"
15813        );
15814    }
15815
15816    #[test]
15817    fn test_duplicate_data() {
15818        let mut spec = create_test_spec("test");
15819        spec = spec.add_data(create_literal_data(
15820            "age",
15821            Value::Number(rust_decimal::Decimal::from(25)),
15822        ));
15823        spec = spec.add_data(create_literal_data(
15824            "age",
15825            Value::Number(rust_decimal::Decimal::from(30)),
15826        ));
15827
15828        let result = build_graph(&spec, &[spec.clone()]);
15829        assert!(result.is_err(), "Should detect duplicate data");
15830
15831        let errors = result.unwrap_err();
15832        assert!(errors.iter().any(|e| {
15833            let s = e.to_string();
15834            s.contains("already used") && s.contains("age")
15835        }));
15836    }
15837
15838    #[test]
15839    fn test_duplicate_rule() {
15840        let mut spec = create_test_spec("test");
15841
15842        let rule1 = LemmaRule {
15843            name: "test_rule".to_string(),
15844            expression: create_literal_expr(Value::Boolean(BooleanValue::True)),
15845            unless_clauses: Vec::new(),
15846            source_location: test_source(),
15847        };
15848        let rule2 = LemmaRule {
15849            name: "test_rule".to_string(),
15850            expression: create_literal_expr(Value::Boolean(BooleanValue::False)),
15851            unless_clauses: Vec::new(),
15852            source_location: test_source(),
15853        };
15854
15855        spec = spec.add_rule(rule1);
15856        spec = spec.add_rule(rule2);
15857
15858        let result = build_graph(&spec, &[spec.clone()]);
15859        assert!(result.is_err(), "Should detect duplicate rule");
15860
15861        let errors = result.unwrap_err();
15862        assert!(errors.iter().any(
15863            |e| e.to_string().contains("Duplicate rule") && e.to_string().contains("test_rule")
15864        ));
15865    }
15866
15867    #[test]
15868    fn test_missing_data_reference() {
15869        let mut spec = create_test_spec("test");
15870
15871        let missing_data_expr = ast::Expression::leaf(
15872            ast::ExpressionKind::Reference(Reference {
15873                segments: Vec::new(),
15874                name: "nonexistent".to_string(),
15875            }),
15876            Some(test_source()),
15877        );
15878
15879        let rule = LemmaRule {
15880            name: "test_rule".to_string(),
15881            expression: missing_data_expr,
15882            unless_clauses: Vec::new(),
15883            source_location: test_source(),
15884        };
15885        spec = spec.add_rule(rule);
15886
15887        let result = build_graph(&spec, &[spec.clone()]);
15888        assert!(result.is_err(), "Should detect missing data");
15889
15890        let errors = result.unwrap_err();
15891        assert!(errors
15892            .iter()
15893            .any(|e| e.to_string().contains("Reference 'nonexistent' not found")));
15894    }
15895
15896    fn rule_node_of<'g>(graph: &'g Graph<'static>, rule: &str) -> &'g RuleNode<'static> {
15897        graph
15898            .rules()
15899            .iter()
15900            .find(|(path, _)| path.rule == rule)
15901            .map(|(_, node)| node)
15902            .expect("rule in graph")
15903    }
15904
15905    fn graph_of(source: &str) -> Graph<'static> {
15906        let specs = crate::parse(
15907            source,
15908            crate::parsing::source::SourceType::Volatile,
15909            &crate::ResourceLimits::default(),
15910        )
15911        .expect("fixture parses")
15912        .into_flattened_specs();
15913        let s = specs.iter().find(|d| d.name == "s").expect("spec s");
15914        build_graph(s, &specs).expect("fixture plans")
15915    }
15916
15917    #[test]
15918    fn rule_without_unless_is_its_default_expression() {
15919        let graph = graph_of("spec s\ndata x: number\nrule r: x + 9\n");
15920        let node = rule_node_of(&graph, "r");
15921        node.expression.assert_is_tree();
15922        assert_eq!(node.branches, vec![(None, node.expression.root)]);
15923        assert!(matches!(
15924            node.expression.root_kind(),
15925            ExpressionKind::Arithmetic { .. }
15926        ));
15927    }
15928
15929    #[test]
15930    fn unless_rule_is_one_piecewise_table_in_source_order() {
15931        let graph = graph_of(
15932            "spec s\ndata x: number\nrule r: 0\n unless x > 10 then 100\n unless x > 5 then 50\n",
15933        );
15934        let node = rule_node_of(&graph, "r");
15935        node.expression.assert_is_tree();
15936        let ExpressionKind::Piecewise(arms) = node.expression.root_kind() else {
15937            panic!(
15938                "expected Piecewise root, got {:?}",
15939                node.expression.root_kind()
15940            );
15941        };
15942        let number = |id: ExprId| match node.expression.at(id).kind() {
15943            ExpressionKind::Literal(literal) => match &literal.value {
15944                ValueKind::Number(n) => n.clone(),
15945                other => panic!("expected number, got {other:?}"),
15946            },
15947            other => panic!("expected literal, got {other:?}"),
15948        };
15949        assert_eq!(arms.len(), 3);
15950        assert!(matches!(
15951            node.expression.at(arms[0].0).kind(),
15952            ExpressionKind::Literal(literal) if matches!(literal.value, ValueKind::Boolean(true))
15953        ));
15954        assert_eq!(number(arms[0].1), number(node.branches[0].1));
15955        for (index, expected) in [(1, 100), (2, 50)] {
15956            assert!(matches!(
15957                node.expression.at(arms[index].0).kind(),
15958                ExpressionKind::Comparison(..)
15959            ));
15960            assert_eq!(
15961                (Some(arms[index].0), arms[index].1),
15962                node.branches[index],
15963                "branch ids name the Piecewise arm"
15964            );
15965            assert_eq!(
15966                number(arms[index].1),
15967                crate::computation::rational::rational_new(expected, 1)
15968            );
15969        }
15970        assert_eq!(node.branches[0].0, None);
15971    }
15972
15973    #[test]
15974    fn chained_add_reports_all_missing_references() {
15975        let minimal = r#"spec s
15976data a: 1
15977rule bad: a + missing_one + missing_two
15978"#;
15979        let specs = crate::parse(
15980            minimal,
15981            crate::parsing::source::SourceType::Volatile,
15982            &crate::ResourceLimits::default(),
15983        )
15984        .expect("minimal fixture parses")
15985        .into_flattened_specs();
15986        let s = specs.iter().find(|d| d.name == "s").expect("spec s");
15987        let result = build_graph(s, &specs);
15988        let errors = result.expect_err("chained + with two missing refs must fail planning");
15989        let not_found: Vec<_> = errors
15990            .iter()
15991            .filter(|e| e.to_string().contains("not found"))
15992            .collect();
15993        assert_eq!(
15994            not_found.len(),
15995            2,
15996            "expected two missing-reference errors, got: {}",
15997            errors
15998                .iter()
15999                .map(|e| e.to_string())
16000                .collect::<Vec<_>>()
16001                .join("; ")
16002        );
16003        assert!(
16004            not_found
16005                .iter()
16006                .any(|e| e.to_string().contains("missing_one")),
16007            "expected error for missing_one"
16008        );
16009        assert!(
16010            not_found
16011                .iter()
16012                .any(|e| e.to_string().contains("missing_two")),
16013            "expected error for missing_two"
16014        );
16015        for error in &not_found {
16016            let source = error
16017                .location()
16018                .expect("missing-reference error must carry source span");
16019            assert_ne!(
16020                source.span.start, source.span.end,
16021                "error span must not be empty"
16022            );
16023        }
16024
16025        let user_shaped = r#"spec s
16026data base_cost: 1
16027data starch_levy: 2
16028data quality_extra: 3
16029rule landed_cost_per_kg:
16030  base_cost + starch_levy + quality_extra + transport_extra_per_kg + amortization_per_kg
16031"#;
16032        let specs = crate::parse(
16033            user_shaped,
16034            crate::parsing::source::SourceType::Volatile,
16035            &crate::ResourceLimits::default(),
16036        )
16037        .expect("user-shaped fixture parses")
16038        .into_flattened_specs();
16039        let s = specs.iter().find(|d| d.name == "s").expect("spec s");
16040        let result = build_graph(s, &specs);
16041        let errors = result.expect_err("landed_cost_per_kg with two typos must fail planning");
16042        let not_found: Vec<_> = errors
16043            .iter()
16044            .filter(|e| e.to_string().contains("not found"))
16045            .collect();
16046        assert_eq!(
16047            not_found.len(),
16048            2,
16049            "expected two missing-reference errors, got: {}",
16050            errors
16051                .iter()
16052                .map(|e| e.to_string())
16053                .collect::<Vec<_>>()
16054                .join("; ")
16055        );
16056        assert!(
16057            not_found
16058                .iter()
16059                .any(|e| e.to_string().contains("transport_extra_per_kg")),
16060            "expected error for transport_extra_per_kg"
16061        );
16062        assert!(
16063            not_found
16064                .iter()
16065                .any(|e| e.to_string().contains("amortization_per_kg")),
16066            "expected error for amortization_per_kg"
16067        );
16068        for error in &not_found {
16069            let source = error
16070                .location()
16071                .expect("missing-reference error must carry source span");
16072            assert_ne!(
16073                source.span.start, source.span.end,
16074                "error span must not be empty"
16075            );
16076        }
16077    }
16078
16079    #[test]
16080    fn test_missing_spec_reference() {
16081        let mut spec = create_test_spec("test");
16082
16083        let data = LemmaData {
16084            reference: Reference {
16085                segments: Vec::new(),
16086                name: "contract".to_string(),
16087            },
16088            value: ParsedDataValue::import(crate::parsing::ast::SpecRef::same_repository(
16089                "nonexistent",
16090            )),
16091            source_location: test_source(),
16092        };
16093        spec = spec.add_data(data);
16094
16095        let result = build_graph(&spec, &[spec.clone()]);
16096        assert!(result.is_err(), "Should detect missing spec");
16097
16098        let errors = result.unwrap_err();
16099        assert!(
16100            errors.iter().any(|e| e.to_string().contains("nonexistent")),
16101            "Error should mention nonexistent spec: {:?}",
16102            errors.iter().map(|e| e.to_string()).collect::<Vec<_>>()
16103        );
16104    }
16105
16106    #[test]
16107    fn test_data_reference_conversion() {
16108        let mut spec = create_test_spec("test");
16109        spec = spec.add_data(create_literal_data(
16110            "age",
16111            Value::Number(rust_decimal::Decimal::from(25)),
16112        ));
16113
16114        let age_expr = ast::Expression::leaf(
16115            ast::ExpressionKind::Reference(Reference {
16116                segments: Vec::new(),
16117                name: "age".to_string(),
16118            }),
16119            Some(test_source()),
16120        );
16121
16122        let rule = LemmaRule {
16123            name: "test_rule".to_string(),
16124            expression: age_expr,
16125            unless_clauses: Vec::new(),
16126            source_location: test_source(),
16127        };
16128        spec = spec.add_rule(rule);
16129
16130        let result = build_graph(&spec, &[spec.clone()]);
16131        assert!(result.is_ok(), "Should build graph successfully");
16132
16133        let graph = result.unwrap();
16134        let rule_node = graph.rules().values().next().unwrap();
16135
16136        assert!(matches!(
16137            rule_node.expression.at(rule_node.branches[0].1).kind(),
16138            ExpressionKind::DataPath(_)
16139        ));
16140    }
16141
16142    #[test]
16143    fn test_rule_reference_conversion() {
16144        let mut spec = create_test_spec("test");
16145
16146        let rule1_expr = ast::Expression::leaf(
16147            ast::ExpressionKind::Reference(Reference {
16148                segments: Vec::new(),
16149                name: "age".to_string(),
16150            }),
16151            Some(test_source()),
16152        );
16153
16154        let rule1 = LemmaRule {
16155            name: "rule1".to_string(),
16156            expression: rule1_expr,
16157            unless_clauses: Vec::new(),
16158            source_location: test_source(),
16159        };
16160        spec = spec.add_rule(rule1);
16161
16162        let rule2_expr = ast::Expression::leaf(
16163            ast::ExpressionKind::Reference(Reference {
16164                segments: Vec::new(),
16165                name: "rule1".to_string(),
16166            }),
16167            Some(test_source()),
16168        );
16169
16170        let rule2 = LemmaRule {
16171            name: "rule2".to_string(),
16172            expression: rule2_expr,
16173            unless_clauses: Vec::new(),
16174            source_location: test_source(),
16175        };
16176        spec = spec.add_rule(rule2);
16177
16178        spec = spec.add_data(create_literal_data(
16179            "age",
16180            Value::Number(rust_decimal::Decimal::from(25)),
16181        ));
16182
16183        let result = build_graph(&spec, &[spec.clone()]);
16184        assert!(result.is_ok(), "Should build graph successfully");
16185
16186        let graph = result.unwrap();
16187        let rule2_node = graph
16188            .rules()
16189            .get(&RulePath {
16190                segments: Vec::new(),
16191                rule: "rule2".to_string(),
16192            })
16193            .unwrap();
16194
16195        assert_eq!(rule2_node.depends_on_rules.len(), 1);
16196        // Source branches preserve RulePath; normalized_expression must inline deps and must not.
16197        assert!(matches!(
16198            rule2_node.expression.at(rule2_node.branches[0].1).kind(),
16199            ExpressionKind::RulePath(_)
16200        ));
16201    }
16202
16203    #[test]
16204    fn test_collect_multiple_errors() {
16205        let mut spec = create_test_spec("test");
16206        spec = spec.add_data(create_literal_data(
16207            "age",
16208            Value::Number(rust_decimal::Decimal::from(25)),
16209        ));
16210        spec = spec.add_data(create_literal_data(
16211            "age",
16212            Value::Number(rust_decimal::Decimal::from(30)),
16213        ));
16214
16215        let missing_data_expr = ast::Expression::leaf(
16216            ast::ExpressionKind::Reference(Reference {
16217                segments: Vec::new(),
16218                name: "nonexistent".to_string(),
16219            }),
16220            Some(test_source()),
16221        );
16222
16223        let rule = LemmaRule {
16224            name: "test_rule".to_string(),
16225            expression: missing_data_expr,
16226            unless_clauses: Vec::new(),
16227            source_location: test_source(),
16228        };
16229        spec = spec.add_rule(rule);
16230
16231        let result = build_graph(&spec, &[spec.clone()]);
16232        assert!(result.is_err(), "Should collect multiple errors");
16233
16234        let errors = result.unwrap_err();
16235        assert!(errors.len() >= 2, "Should have at least 2 errors");
16236        assert!(errors
16237            .iter()
16238            .any(|e| e.to_string().contains("already used")));
16239        assert!(errors
16240            .iter()
16241            .any(|e| e.to_string().contains("Reference 'nonexistent' not found")));
16242    }
16243
16244    #[test]
16245    fn test_type_registration_collects_multiple_errors() {
16246        use crate::parsing::ast::{DataValue, ParentType, PrimitiveKind, SpecRef};
16247
16248        let type_source = Source::new(
16249            crate::parsing::source::SourceType::Volatile,
16250            Span {
16251                start: 0,
16252                end: 0,
16253                line: 1,
16254                col: 0,
16255            },
16256        );
16257        let spec_a = create_test_spec("spec_a")
16258            .with_source_type(crate::parsing::source::SourceType::Volatile)
16259            .add_data(LemmaData {
16260                reference: Reference::local("dep".to_string()),
16261                value: DataValue::import(SpecRef::same_repository("spec_b")),
16262                source_location: type_source.clone(),
16263            })
16264            .add_data(LemmaData {
16265                reference: Reference::local("money".to_string()),
16266                value: DataValue::Definition {
16267                    base: Some(ParentType::Primitive {
16268                        primitive: PrimitiveKind::Number,
16269                    }),
16270                    constraints: None,
16271                    value: None,
16272
16273                    list: None,
16274                },
16275                source_location: type_source.clone(),
16276            })
16277            .add_data(LemmaData {
16278                reference: Reference::local("money".to_string()),
16279                value: DataValue::Definition {
16280                    base: Some(ParentType::Primitive {
16281                        primitive: PrimitiveKind::Number,
16282                    }),
16283                    constraints: None,
16284                    value: None,
16285
16286                    list: None,
16287                },
16288                source_location: type_source,
16289            });
16290
16291        let type_source_b = Source::new(
16292            crate::parsing::source::SourceType::Volatile,
16293            Span {
16294                start: 0,
16295                end: 0,
16296                line: 1,
16297                col: 0,
16298            },
16299        );
16300        let spec_b = create_test_spec("spec_b")
16301            .with_source_type(crate::parsing::source::SourceType::Volatile)
16302            .add_data(LemmaData {
16303                reference: Reference::local("length".to_string()),
16304                value: DataValue::Definition {
16305                    base: Some(ParentType::Primitive {
16306                        primitive: PrimitiveKind::Number,
16307                    }),
16308                    constraints: None,
16309                    value: None,
16310
16311                    list: None,
16312                },
16313                source_location: type_source_b.clone(),
16314            })
16315            .add_data(LemmaData {
16316                reference: Reference::local("length".to_string()),
16317                value: DataValue::Definition {
16318                    base: Some(ParentType::Primitive {
16319                        primitive: PrimitiveKind::Number,
16320                    }),
16321                    constraints: None,
16322                    value: None,
16323
16324                    list: None,
16325                },
16326                source_location: type_source_b,
16327            });
16328
16329        let mut sources = HashMap::new();
16330        sources.insert(
16331            crate::parsing::source::SourceType::Volatile.to_string(),
16332            "spec spec_a\nuses dep: spec_b\ndata money: number\ndata money: number".to_string(),
16333        );
16334        sources.insert(
16335            crate::parsing::source::SourceType::Volatile.to_string(),
16336            "spec spec_b\ndata length: number\ndata length: number".to_string(),
16337        );
16338
16339        let result = build_graph(&spec_a, &[spec_a.clone(), spec_b.clone()]);
16340        assert!(
16341            result.is_err(),
16342            "Should fail with duplicate type/data errors"
16343        );
16344    }
16345
16346    // =================================================================
16347    // Versioned spec identifiers: latest-resolution (section 6.3)
16348    // =================================================================
16349
16350    #[test]
16351    fn spec_ref_resolves_to_single_spec_by_name() {
16352        let code = r#"spec myspec
16353data x: 10
16354
16355spec consumer
16356uses m: myspec
16357rule result: m.x"#;
16358        let specs = crate::parse(
16359            code,
16360            crate::parsing::source::SourceType::Volatile,
16361            &crate::ResourceLimits::default(),
16362        )
16363        .unwrap()
16364        .into_flattened_specs();
16365        let consumer = specs.iter().find(|d| d.name == "consumer").unwrap();
16366
16367        let graph = build_graph(consumer, &specs).unwrap();
16368        let Some(DataDefinition::Import { row, .. }) = graph.data.get(&DataPath::local("m".into()))
16369        else {
16370            panic!("`m` is an import slot of the consumer graph");
16371        };
16372        assert_eq!(row.identity.spec, "myspec");
16373        assert!(
16374            matches!(
16375                graph
16376                    .template_of_row(row)
16377                    .data
16378                    .get(&DataPath::local("x".into())),
16379                Some(DataDefinition::Value { .. })
16380            ),
16381            "`m` reads `x` of the one `myspec` template"
16382        );
16383    }
16384
16385    #[test]
16386    fn spec_ref_to_nonexistent_spec_is_error() {
16387        let code = r#"spec myspec
16388data x: 10
16389
16390spec consumer
16391uses m: nonexistent
16392rule result: m.x"#;
16393        let specs = crate::parse(
16394            code,
16395            crate::parsing::source::SourceType::Volatile,
16396            &crate::ResourceLimits::default(),
16397        )
16398        .unwrap()
16399        .into_flattened_specs();
16400        let consumer = specs.iter().find(|d| d.name == "consumer").unwrap();
16401        let result = build_graph(consumer, &specs);
16402        assert!(result.is_err(), "Should fail for non-existent spec");
16403    }
16404
16405    // =================================================================
16406    // Self-reference: same spec body via uses (planning)
16407    // =================================================================
16408
16409    #[test]
16410    fn import_alias_registered_in_graph() {
16411        let code = r#"
16412spec inner
16413data x: number -> suggest 1
16414
16415spec outer
16416uses i: inner
16417rule r: i.x
16418"#;
16419        let specs = crate::parse(
16420            code,
16421            crate::parsing::source::SourceType::Volatile,
16422            &crate::ResourceLimits::default(),
16423        )
16424        .unwrap()
16425        .into_flattened_specs();
16426        let outer = specs.iter().find(|s| s.name == "outer").unwrap();
16427        let graph = build_graph(outer, &specs).expect("uses i: inner must plan");
16428
16429        let alias_path = DataPath {
16430            segments: Vec::new(),
16431            data: "i".to_string(),
16432        };
16433        let Some(DataDefinition::Import { row, .. }) = graph.data().get(&alias_path) else {
16434            panic!(
16435                "alias path 'i' must be DataDefinition::Import, got {:?}",
16436                graph.data().get(&alias_path)
16437            );
16438        };
16439        assert_eq!(row.identity.spec, "inner");
16440        assert!(matches!(row.origin, RowOrigin::Import { ref alias } if alias.as_ref() == "i"));
16441        assert!(
16442            row.bindings.iter().all(Option::is_none),
16443            "`uses i: inner` without `-> with` binds no slot of the import row"
16444        );
16445        assert!(
16446            graph.data().keys().all(|path| path.segments.is_empty()),
16447            "the consumer graph holds its own data only, got {:?}",
16448            graph.data().keys().collect::<Vec<_>>()
16449        );
16450    }
16451
16452    #[test]
16453    fn self_reference_is_error() {
16454        let code = "spec myspec\nuses m: myspec";
16455        let specs = crate::parse(
16456            code,
16457            crate::parsing::source::SourceType::Volatile,
16458            &crate::ResourceLimits::default(),
16459        )
16460        .unwrap()
16461        .into_flattened_specs();
16462        let result = build_graph(&specs[0], &specs);
16463        assert!(result.is_err(), "Self-reference should be an error");
16464        let errors = result.unwrap_err();
16465        let joined: String = errors
16466            .iter()
16467            .map(|e| e.to_string())
16468            .collect::<Vec<_>>()
16469            .join(" ");
16470        assert!(
16471            joined.contains("cannot reference itself") && joined.contains("myspec"),
16472            "Error should name self-reference: {:?}",
16473            errors.iter().map(|e| e.to_string()).collect::<Vec<_>>()
16474        );
16475    }
16476
16477    mod type_resolution {
16478        use super::super::*;
16479        use crate::computation::rational::rational_new;
16480        use crate::parsing::ast::{
16481            CommandArg, LemmaSpec, ParentType, PrimitiveKind, TypeConstraintCommand,
16482        };
16483        use crate::parsing::parse;
16484        use crate::ResourceLimits;
16485        use rust_decimal::Decimal;
16486        use std::sync::Arc;
16487
16488        fn test_context_and_effective(
16489            specs: &[&LemmaSpec],
16490        ) -> (&'static Context, &'static EffectiveDate) {
16491            use crate::engine::Context;
16492            let mut ctx = Context::new();
16493            let repository = ctx.workspace();
16494            for s in specs {
16495                ctx.insert_spec(Arc::clone(&repository), (*s).clone())
16496                    .unwrap();
16497            }
16498            let ctx = Box::leak(Box::new(ctx));
16499            let eff = Box::leak(Box::new(EffectiveDate::Origin));
16500            (ctx, eff)
16501        }
16502
16503        fn context_with_test_spec() -> (&'static Context, &'static LemmaSpec) {
16504            use crate::engine::Context;
16505            let mut ctx = Context::new();
16506            let repository = ctx.workspace();
16507            let spec = LemmaSpec::new("test_spec".to_string());
16508            ctx.insert_spec(Arc::clone(&repository), spec).unwrap();
16509            let ctx: &'static Context = Box::leak(Box::new(ctx));
16510            let repository = ctx.workspace();
16511            let spec = ctx
16512                .spec_set(&repository, "test_spec")
16513                .and_then(|ss| ss.get_exact(None))
16514                .expect("inserted");
16515            (ctx, spec)
16516        }
16517
16518        fn resolver_for_code(code: &str) -> (TypeResolver<'static>, Vec<&'static LemmaSpec>) {
16519            use crate::engine::Context;
16520            let owned = parse(
16521                code,
16522                crate::parsing::source::SourceType::Volatile,
16523                &ResourceLimits::default(),
16524            )
16525            .unwrap()
16526            .into_flattened_specs();
16527            let mut ctx = Context::new();
16528            let repository = ctx.workspace();
16529            for s in owned {
16530                ctx.insert_spec(Arc::clone(&repository), s).unwrap();
16531            }
16532            let ctx = Box::leak(Box::new(ctx));
16533            let repository = ctx.workspace();
16534            let mut spec_refs: Vec<&'static LemmaSpec> = Vec::new();
16535            for ss in ctx.spec_sets_for(&repository) {
16536                for s in ss.iter_specs() {
16537                    spec_refs.push(s);
16538                }
16539            }
16540            let mut resolver = TypeResolver::new(ctx, HashMap::new());
16541            for spec in &spec_refs {
16542                resolver.register_all(&repository, spec);
16543            }
16544            (resolver, spec_refs)
16545        }
16546
16547        fn resolver_single_spec(code: &str) -> (TypeResolver<'static>, &LemmaSpec) {
16548            let (resolver, spec_arcs) = resolver_for_code(code);
16549            let spec = spec_arcs.into_iter().next().expect("at least one spec");
16550            (resolver, spec)
16551        }
16552
16553        #[test]
16554        fn test_type_spec_for_primitive_covers_all_variants() {
16555            use crate::parsing::ast::PrimitiveKind;
16556            use crate::planning::semantics::type_spec_for_primitive;
16557
16558            for kind in [
16559                PrimitiveKind::Boolean,
16560                PrimitiveKind::Measure,
16561                PrimitiveKind::MeasureRange,
16562                PrimitiveKind::Number,
16563                PrimitiveKind::NumberRange,
16564                PrimitiveKind::Ratio,
16565                PrimitiveKind::RatioRange,
16566                PrimitiveKind::Text,
16567                PrimitiveKind::Date,
16568                PrimitiveKind::DateRange,
16569                PrimitiveKind::Time,
16570                PrimitiveKind::TimeRange,
16571            ] {
16572                let spec = type_spec_for_primitive(kind);
16573                assert!(
16574                    !matches!(
16575                        spec,
16576                        crate::planning::semantics::TypeSpecification::Undetermined
16577                    ),
16578                    "type_spec_for_primitive({:?}) returned Undetermined",
16579                    kind
16580                );
16581            }
16582        }
16583
16584        #[test]
16585        fn test_register_data_type_def() {
16586            let (ctx, spec) = context_with_test_spec();
16587            let mut resolver = TypeResolver::new(ctx, HashMap::new());
16588            let ftd = DataTypeDef {
16589                parent: ParentType::Primitive {
16590                    primitive: PrimitiveKind::Number,
16591                },
16592                constraints: Some(vec![
16593                    Constraint::new(
16594                        TypeConstraintCommand::Minimum,
16595                        vec![CommandArg::Literal(crate::literals::Value::Number(
16596                            Decimal::ZERO,
16597                        ))],
16598                        crate::parsing::source::Source::new(
16599                            crate::parsing::source::SourceType::Volatile,
16600                            crate::parsing::ast::Span {
16601                                start: 0,
16602                                end: 0,
16603                                line: 1,
16604                                col: 0,
16605                            },
16606                        ),
16607                    ),
16608                    Constraint::new(
16609                        TypeConstraintCommand::Maximum,
16610                        vec![CommandArg::Literal(crate::literals::Value::Number(
16611                            Decimal::from(150),
16612                        ))],
16613                        crate::parsing::source::Source::new(
16614                            crate::parsing::source::SourceType::Volatile,
16615                            crate::parsing::ast::Span {
16616                                start: 0,
16617                                end: 0,
16618                                line: 1,
16619                                col: 0,
16620                            },
16621                        ),
16622                    ),
16623                ]),
16624                source: crate::parsing::source::Source::new(
16625                    crate::parsing::source::SourceType::Volatile,
16626                    crate::parsing::ast::Span {
16627                        start: 0,
16628                        end: 0,
16629                        line: 1,
16630                        col: 0,
16631                    },
16632                ),
16633                name: "age".to_string(),
16634                bound_literal: None,
16635            };
16636
16637            let result = resolver.register_type(&ctx.workspace(), spec, ftd);
16638            assert!(result.is_ok());
16639            let resolved = resolver
16640                .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
16641                .unwrap();
16642            assert!(resolved.resolved.contains_key("age"));
16643        }
16644
16645        #[test]
16646        fn test_register_duplicate_type_fails() {
16647            let (ctx, spec) = context_with_test_spec();
16648            let mut resolver = TypeResolver::new(ctx, HashMap::new());
16649            let ftd = DataTypeDef {
16650                parent: ParentType::Primitive {
16651                    primitive: PrimitiveKind::Number,
16652                },
16653                constraints: None,
16654                source: crate::parsing::source::Source::new(
16655                    crate::parsing::source::SourceType::Volatile,
16656                    crate::parsing::ast::Span {
16657                        start: 0,
16658                        end: 0,
16659                        line: 1,
16660                        col: 0,
16661                    },
16662                ),
16663                name: "money".to_string(),
16664                bound_literal: None,
16665            };
16666            let repository = ctx.workspace();
16667            resolver
16668                .register_type(&repository, spec, ftd.clone())
16669                .unwrap();
16670            let result = resolver.register_type(&repository, spec, ftd);
16671            assert!(result.is_err());
16672        }
16673
16674        #[test]
16675        fn test_resolve_custom_type_from_primitive() {
16676            let (ctx, spec) = context_with_test_spec();
16677            let mut resolver = TypeResolver::new(ctx, HashMap::new());
16678            let ftd = DataTypeDef {
16679                parent: ParentType::Primitive {
16680                    primitive: PrimitiveKind::Number,
16681                },
16682                constraints: None,
16683                source: crate::parsing::source::Source::new(
16684                    crate::parsing::source::SourceType::Volatile,
16685                    crate::parsing::ast::Span {
16686                        start: 0,
16687                        end: 0,
16688                        line: 1,
16689                        col: 0,
16690                    },
16691                ),
16692                name: "money".to_string(),
16693                bound_literal: None,
16694            };
16695
16696            resolver.register_type(&ctx.workspace(), spec, ftd).unwrap();
16697            let resolved = resolver
16698                .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
16699                .unwrap();
16700
16701            assert!(resolved.resolved.contains_key("money"));
16702            let money_type = resolved.resolved.get("money").unwrap();
16703            assert_eq!(money_type.name, Some("money".to_string()));
16704        }
16705
16706        #[test]
16707        fn test_child_measure_type_keeps_declared_name_and_child_units() {
16708            let (resolver, spec) = resolver_single_spec(
16709                r#"spec test
16710    data length: measure
16711      -> unit meter: 1
16712    data road_length: length
16713      -> unit kilometer: 1000"#,
16714            );
16715
16716            let resolved_types = resolver
16717                .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
16718                .unwrap();
16719
16720            let road_length_type = resolved_types.resolved.get("road_length").unwrap();
16721            assert_eq!(road_length_type.name.as_deref(), Some("road_length"));
16722
16723            match &road_length_type.specifications {
16724                TypeSpecification::Measure { units, .. } => {
16725                    assert!(units.iter().any(|unit| unit.name == "kilometer"));
16726                }
16727                _ => panic!("Expected Measure type specifications"),
16728            }
16729
16730            let kilometer_owner = resolved_types.unit_index.unique_owner("kilometer").unwrap();
16731            assert_eq!(kilometer_owner.name.as_deref(), Some("road_length"));
16732            assert!(
16733                Arc::ptr_eq(
16734                    kilometer_owner,
16735                    resolved_types.resolved.get("road_length").unwrap()
16736                ),
16737                "bound kilometer owner must be the resolved road_length Arc"
16738            );
16739        }
16740
16741        #[test]
16742        fn test_type_definition_resolution() {
16743            let (resolver, spec) = resolver_single_spec(
16744                r#"spec test
16745    data dice: number -> minimum 0 -> maximum 6"#,
16746            );
16747
16748            let resolved_types = resolver
16749                .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
16750                .unwrap();
16751            let dice_type = resolved_types.resolved.get("dice").unwrap();
16752
16753            match &dice_type.specifications {
16754                TypeSpecification::Number {
16755                    minimum, maximum, ..
16756                } => {
16757                    assert_eq!(minimum, &Some(rational_new(0, 1)));
16758                    assert_eq!(maximum, &Some(rational_new(6, 1)));
16759                }
16760                _ => panic!("Expected Number type specifications"),
16761            }
16762        }
16763
16764        #[test]
16765        fn test_type_definition_with_multiple_commands() {
16766            let (resolver, spec) = resolver_single_spec(
16767                r#"spec test
16768    data money: measure -> unit eur: 1.0 -> unit usd: 1.18 -> minimum 0 eur"#,
16769            );
16770
16771            let resolved_types = resolver
16772                .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
16773                .unwrap();
16774            let money_type = resolved_types.resolved.get("money").unwrap();
16775
16776            match &money_type.specifications {
16777                TypeSpecification::Measure { minimum, units, .. } => {
16778                    assert_eq!(minimum.as_ref().map(|(_, unit)| unit.as_str()), Some("eur"));
16779                    assert_eq!(units.len(), 2);
16780                    assert!(units.iter().any(|u| u.name == "eur"));
16781                    assert!(units.iter().any(|u| u.name == "usd"));
16782                }
16783                _ => panic!("Expected Measure type specifications"),
16784            }
16785        }
16786
16787        #[test]
16788        fn test_number_type_with_decimals() {
16789            let (resolver, spec) = resolver_single_spec(
16790                r#"spec test
16791    data price: number -> decimals 2 -> minimum 0"#,
16792            );
16793
16794            let resolved_types = resolver
16795                .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
16796                .unwrap();
16797            let price_type = resolved_types.resolved.get("price").unwrap();
16798
16799            match &price_type.specifications {
16800                TypeSpecification::Number {
16801                    decimals, minimum, ..
16802                } => {
16803                    assert_eq!(*decimals, Some(2));
16804                    assert_eq!(minimum, &Some(rational_new(0, 1)));
16805                }
16806                _ => panic!("Expected Number type specifications with decimals"),
16807            }
16808        }
16809
16810        #[test]
16811        fn test_number_type_decimals_only() {
16812            let (resolver, spec) = resolver_single_spec(
16813                r#"spec test
16814    data precise_number: number -> decimals 4"#,
16815            );
16816
16817            let resolved_types = resolver
16818                .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
16819                .unwrap();
16820            let precise_type = resolved_types.resolved.get("precise_number").unwrap();
16821
16822            match &precise_type.specifications {
16823                TypeSpecification::Number { decimals, .. } => {
16824                    assert_eq!(*decimals, Some(4));
16825                }
16826                _ => panic!("Expected Number type with decimals 4"),
16827            }
16828        }
16829
16830        #[test]
16831        fn test_measure_type_rejects_decimals() {
16832            let (resolver, spec) = resolver_single_spec(
16833                r#"spec test
16834    data weight: measure -> unit kg: 1 -> decimals 3"#,
16835            );
16836
16837            let errors = resolver
16838                .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
16839                .expect_err("decimals on a measure must be a planning error");
16840            let messages: Vec<String> = errors.iter().map(|e| e.to_string()).collect();
16841            assert!(
16842                messages.iter().any(|m| m.contains(
16843                    "'decimals' is not valid on measure type 'weight': precision depends on the unit. Round in a rule instead, for example 'weight -> round 0.001 kg'."
16844                )),
16845                "got: {messages:?}"
16846            );
16847        }
16848
16849        #[test]
16850        fn test_ratio_type_rejects_decimals() {
16851            let (resolver, spec) = resolver_single_spec(
16852                r#"spec test
16853    data ratio_type: ratio -> decimals 2"#,
16854            );
16855
16856            let errors = resolver
16857                .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
16858                .expect_err("decimals on a ratio must be a planning error");
16859            let messages: Vec<String> = errors.iter().map(|e| e.to_string()).collect();
16860            assert!(
16861                messages.iter().any(|m| m.contains(
16862                    "'decimals' is not valid on ratio type 'ratio_type': precision depends on the unit. Round in a rule instead, for example 'ratio_type -> round 0.01 percent'."
16863                )),
16864                "got: {messages:?}"
16865            );
16866        }
16867
16868        #[test]
16869        fn typedef_default_inherits_through_extension_chain() {
16870            let (resolver, spec) = resolver_single_spec(
16871                r#"spec test
16872    data money: measure -> unit eur: 1 -> suggest 4 eur
16873    data price: money
16874    data final_price: price"#,
16875            );
16876
16877            let resolved = resolver
16878                .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
16879                .expect("extension chain must resolve");
16880
16881            assert!(
16882                resolved.declared_suggestions.contains_key("money"),
16883                "base typedef default must convert to ValueKind"
16884            );
16885            assert!(
16886                resolved.declared_suggestions.contains_key("final_price"),
16887                "suggestion must inherit through extension chain (money → price → final_price)"
16888            );
16889        }
16890
16891        #[test]
16892        fn test_ratio_type_with_default_command() {
16893            let (resolver, spec) = resolver_single_spec(
16894                r#"spec test
16895    data percentage: ratio -> minimum 0% -> maximum 100% -> suggest 50%"#,
16896            );
16897
16898            let resolved_types = resolver
16899                .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
16900                .unwrap();
16901            let percentage_type = resolved_types.resolved.get("percentage").unwrap();
16902
16903            match &percentage_type.specifications {
16904                TypeSpecification::Ratio {
16905                    minimum, maximum, ..
16906                } => {
16907                    assert_eq!(
16908                        *minimum,
16909                        Some(rational_new(0, 1)),
16910                        "ratio type should have minimum 0"
16911                    );
16912                    assert_eq!(
16913                        *maximum,
16914                        Some(rational_new(1, 1)),
16915                        "ratio type should have maximum 1"
16916                    );
16917                }
16918                _ => panic!("Expected Ratio type with minimum and maximum"),
16919            }
16920
16921            let declared = resolved_types
16922                .declared_suggestions
16923                .get("percentage")
16924                .expect("declared default must be tracked for percentage");
16925            match &declared.value {
16926                ValueKind::Ratio(v) => {
16927                    assert_eq!(v, &rational_new(1, 2));
16928                }
16929                other => panic!("expected Ratio declared default, got {:?}", other),
16930            }
16931        }
16932
16933        #[test]
16934        fn test_measure_extension_chain_same_family_units_allowed() {
16935            let (resolver, spec) = resolver_single_spec(
16936                r#"spec test
16937    data money: measure -> unit eur: 1
16938    data money2: money -> unit usd: 1.24"#,
16939            );
16940
16941            let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
16942            assert!(
16943                result.is_ok(),
16944                "Measure extension chain should resolve: {:?}",
16945                result.err()
16946            );
16947
16948            let resolved = result.unwrap();
16949            assert!(
16950                resolved.unit_index.unique_owner("eur").is_some(),
16951                "eur should be in unit_index"
16952            );
16953            assert!(
16954                resolved.unit_index.unique_owner("usd").is_some(),
16955                "usd should be in unit_index"
16956            );
16957            let eur_type = resolved.unit_index.unique_owner("eur").unwrap();
16958            let usd_type = resolved.unit_index.unique_owner("usd").unwrap();
16959            assert_eq!(
16960                eur_type.name.as_deref(),
16961                Some("money"),
16962                "declarer money must own inherited eur, not extension"
16963            );
16964            assert_eq!(
16965                usd_type.name.as_deref(),
16966                Some("money2"),
16967                "usd defined on money2 should be owned by money2"
16968            );
16969            assert!(
16970                Arc::ptr_eq(eur_type, resolved.resolved.get("money").unwrap()),
16971                "bound eur owner must be the resolved money Arc"
16972            );
16973            assert!(
16974                Arc::ptr_eq(usd_type, resolved.resolved.get("money2").unwrap()),
16975                "bound usd owner must be the resolved money2 Arc"
16976            );
16977        }
16978
16979        #[test]
16980        fn test_invalid_parent_type_in_named_type_should_error() {
16981            let (resolver, spec) = resolver_single_spec(
16982                r#"spec test
16983    data invalid: nonexistent_type -> minimum 0"#,
16984            );
16985
16986            let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
16987            assert!(result.is_err(), "Should reject invalid parent type");
16988
16989            let errs = result.unwrap_err();
16990            assert!(!errs.is_empty(), "expected at least one error");
16991            let error_msg = errs[0].to_string();
16992            assert!(
16993                error_msg.contains("Unknown parent") && error_msg.contains("nonexistent_type"),
16994                "Error should mention unknown type. Got: {}",
16995                error_msg
16996            );
16997        }
16998
16999        #[test]
17000        fn test_invalid_primitive_type_name_should_error() {
17001            let (resolver, spec) = resolver_single_spec(
17002                r#"spec test
17003    data invalid: choice -> option "a""#,
17004            );
17005
17006            let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
17007            assert!(result.is_err(), "Should reject invalid type base 'choice'");
17008
17009            let errs = result.unwrap_err();
17010            assert!(!errs.is_empty(), "expected at least one error");
17011            let error_msg = errs[0].to_string();
17012            assert!(
17013                error_msg.contains("Unknown parent") && error_msg.contains("choice"),
17014                "Error should mention unknown type 'choice'. Got: {}",
17015                error_msg
17016            );
17017        }
17018
17019        #[test]
17020        fn extension_unit_factor_override_errors() {
17021            let (resolver, spec) = resolver_single_spec(
17022                r#"spec test
17023    data money: measure
17024      -> unit eur: 1
17025    data money2: money
17026      -> unit eur: 1.10"#,
17027            );
17028
17029            let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
17030            assert!(
17031                result.is_err(),
17032                "extension child must not override inherited unit factor"
17033            );
17034            let error_msg = result
17035                .unwrap_err()
17036                .iter()
17037                .map(ToString::to_string)
17038                .collect::<Vec<_>>()
17039                .join("; ");
17040            assert!(
17041                error_msg.contains("eur"),
17042                "error must name unit eur, got: {error_msg}"
17043            );
17044            assert!(
17045                error_msg.contains("inherited") || error_msg.contains("cannot change"),
17046                "error must reject inherited unit redefinition, got: {error_msg}"
17047            );
17048        }
17049
17050        #[test]
17051        fn inherited_unit_idempotent_redeclare_ok() {
17052            let (resolver, spec) = resolver_single_spec(
17053                r#"spec test
17054    data money: measure
17055      -> unit eur: 1
17056    data money2: money
17057      -> unit eur: 1"#,
17058            );
17059
17060            let resolved = resolver
17061                .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
17062                .expect("idempotent inherited unit redeclare must resolve");
17063            let money2 = resolved.resolved.get("money2").expect("money2");
17064            match &money2.specifications {
17065                TypeSpecification::Measure { units, .. } => {
17066                    let eur = units.iter().find(|u| u.name == "eur").expect("eur");
17067                    assert_eq!(
17068                        eur.require_literal_factor().try_to_decimal().unwrap(),
17069                        Decimal::ONE,
17070                        "eur factor must remain 1"
17071                    );
17072                }
17073                other => panic!("expected Measure, got {other:?}"),
17074            }
17075        }
17076
17077        #[test]
17078        fn extension_additive_unit_registers_in_unit_index() {
17079            let (resolver, spec) = resolver_single_spec(
17080                r#"spec test
17081    data money: measure
17082      -> unit eur: 1
17083    data money2: money
17084      -> unit usd: 1.24"#,
17085            );
17086
17087            let resolved = resolver
17088                .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
17089                .expect("additive extension unit must resolve");
17090            let money = resolved
17091                .resolved
17092                .get("money")
17093                .expect("money resolved")
17094                .clone();
17095            let money2 = resolved
17096                .resolved
17097                .get("money2")
17098                .expect("money2 resolved")
17099                .clone();
17100            assert!(
17101                resolved.unit_index.unique_owner("eur").is_some(),
17102                "eur must be in unit_index"
17103            );
17104            assert!(
17105                resolved.unit_index.unique_owner("usd").is_some(),
17106                "usd must be in unit_index"
17107            );
17108            let eur_owner = resolved.unit_index.unique_owner("eur").expect("eur owner");
17109            let usd_owner = resolved.unit_index.unique_owner("usd").expect("usd owner");
17110            assert_eq!(
17111                eur_owner.name.as_deref(),
17112                Some("money"),
17113                "declarer money must own eur, not extension money2"
17114            );
17115            assert_eq!(
17116                usd_owner.name.as_deref(),
17117                Some("money2"),
17118                "money2 must own new unit usd"
17119            );
17120            assert_eq!(eur_owner.as_ref(), money.as_ref());
17121            assert_eq!(usd_owner.as_ref(), money2.as_ref());
17122        }
17123
17124        #[test]
17125        fn find_unique_reports_multiple_families_with_same_decomposition() {
17126            let code = r#"spec units
17127    data money: measure
17128      -> unit eur: 1
17129      -> unit usd: 0.86
17130    data mass: measure
17131      -> unit kg: 1
17132    data price_eur_per_kg: measure
17133      -> unit eur_per_kg: eur/kg
17134    data price_usd_per_kg: measure
17135      -> unit usd_per_kg: usd/kg
17136    "#;
17137            let (resolver, spec_arcs) = resolver_for_code(code);
17138            let units_arc = spec_arcs
17139                .iter()
17140                .find(|spec| spec.name == "units")
17141                .cloned()
17142                .expect("units spec");
17143            let (context, _) = test_context_and_effective(&spec_arcs);
17144            let repository = context.workspace();
17145            let resolved = resolver
17146                .resolve_and_validate(units_arc, &EffectiveDate::Origin, &ResolvedTypesMap::new())
17147                .expect("units resolves");
17148            let eur_decomp = resolved
17149                .unit_index
17150                .unique_owner("eur_per_kg")
17151                .expect("eur_per_kg")
17152                .measure_type_decomposition()
17153                .expect("eur_per_kg decomposition")
17154                .clone();
17155            let map = vec![(repository, units_arc, resolved)];
17156
17157            let unique = find_unique_measure_type_in_unit_index(
17158                &find_types_by_spec(&map, units_arc)
17159                    .expect("units in map")
17160                    .unit_index,
17161                &eur_decomp,
17162            );
17163            match unique {
17164                DecompositionMatch::Multiple(families) => {
17165                    assert_eq!(families.len(), 2);
17166                    assert!(families.contains(&"price_eur_per_kg".to_string()));
17167                    assert!(families.contains(&"price_usd_per_kg".to_string()));
17168                }
17169                other => panic!("expected Multiple families, got {other:?}"),
17170            }
17171        }
17172
17173        #[test]
17174        fn unit_index_maps_family_root_not_binding_alias() {
17175            let code = r#"spec units
17176    data money: measure
17177      -> unit eur: 1
17178    data mass: measure
17179      -> unit kg: 1
17180    data price_per_weight: measure
17181      -> unit eur_per_kg: eur/kg
17182
17183    spec consumer
17184    uses u: units
17185    data starch_levy_per_kg: u.price_per_weight
17186      -> suggest 0.1 eur_per_kg
17187    data amortization_per_kg: u.price_per_weight
17188      -> suggest 0.2 eur_per_kg
17189    "#;
17190            let (resolver, spec_arcs) = resolver_for_code(code);
17191            let units_arc = spec_arcs
17192                .iter()
17193                .find(|spec| spec.name == "units")
17194                .cloned()
17195                .expect("units spec");
17196            let consumer_arc = spec_arcs
17197                .iter()
17198                .find(|spec| spec.name == "consumer")
17199                .cloned()
17200                .expect("consumer spec");
17201            let (context, _) = test_context_and_effective(&spec_arcs);
17202            let repository = context.workspace();
17203
17204            let mut already_resolved = ResolvedTypesMap::new();
17205            let units_types = resolver
17206                .resolve_and_validate(units_arc, &EffectiveDate::Origin, &already_resolved)
17207                .expect("units spec resolves");
17208            let units_price_per_weight = units_types
17209                .resolved
17210                .get("price_per_weight")
17211                .expect("units defines price_per_weight")
17212                .clone();
17213            already_resolved.push((Arc::clone(&repository), units_arc, units_types));
17214
17215            let consumer_types = resolver
17216                .resolve_and_validate(consumer_arc, &EffectiveDate::Origin, &already_resolved)
17217                .expect("consumer spec resolves");
17218
17219            let eur_per_kg_owner = consumer_types
17220                .unit_index
17221                .unique_owner("eur_per_kg")
17222                .expect("eur_per_kg in unit_index");
17223            assert_eq!(
17224                eur_per_kg_owner.name.as_deref(),
17225                Some("price_per_weight"),
17226                "unit must belong to family root, not binding alias row name"
17227            );
17228            assert_eq!(
17229                eur_per_kg_owner.measure_family_name(),
17230                Some("price_per_weight")
17231            );
17232            assert_eq!(eur_per_kg_owner.as_ref(), units_price_per_weight.as_ref());
17233
17234            let starch_alias = consumer_types
17235                .resolved
17236                .get("starch_levy_per_kg")
17237                .expect("alias row in resolved");
17238            assert!(
17239                !std::ptr::eq(eur_per_kg_owner.as_ref(), starch_alias.as_ref()),
17240                "unit_index must not store binding alias arc"
17241            );
17242        }
17243
17244        #[test]
17245        fn same_named_local_root_does_not_hide_imported_units() {
17246            let code = r#"spec std_units
17247    data mass: measure
17248      -> unit kilogram: 1
17249      -> unit kilograms: 1
17250      -> unit gram: 0.001
17251
17252    spec s
17253    uses u: std_units
17254    data mass: measure
17255      -> unit kg: 1
17256      -> unit tonne: 1000
17257    rule smoke: true
17258    "#;
17259            let (resolver, spec_arcs) = resolver_for_code(code);
17260            let std_units_arc = spec_arcs
17261                .iter()
17262                .find(|spec| spec.name == "std_units")
17263                .cloned()
17264                .expect("std_units spec");
17265            let consumer_arc = spec_arcs
17266                .iter()
17267                .find(|spec| spec.name == "s")
17268                .cloned()
17269                .expect("consumer spec");
17270            let (context, _) = test_context_and_effective(&spec_arcs);
17271            let repository = context.workspace();
17272
17273            let mut already_resolved = ResolvedTypesMap::new();
17274            let std_units_types = resolver
17275                .resolve_and_validate(std_units_arc, &EffectiveDate::Origin, &already_resolved)
17276                .expect("std_units resolves");
17277            already_resolved.push((Arc::clone(&repository), std_units_arc, std_units_types));
17278
17279            let consumer_types = resolver
17280                .resolve_and_validate(consumer_arc, &EffectiveDate::Origin, &already_resolved)
17281                .expect("consumer resolves");
17282
17283            let local_mass = consumer_types
17284                .resolved
17285                .get("mass")
17286                .expect("consumer defines mass")
17287                .clone();
17288
17289            assert!(
17290                consumer_types.unit_index.unique_owner("kg").is_some(),
17291                "local kg must be indexed"
17292            );
17293            assert!(
17294                consumer_types.unit_index.unique_owner("tonne").is_some(),
17295                "local tonne must be indexed"
17296            );
17297            for imported_unit in ["kilogram", "kilograms", "gram"] {
17298                let owner = consumer_types
17299                    .unit_index
17300                    .unique_owner(imported_unit)
17301                    .unwrap_or_else(|| panic!("imported {imported_unit} must stay in scope"));
17302                assert_eq!(
17303                    owner.measure_family().map(|family| family.spec.as_str()),
17304                    Some("std_units"),
17305                    "{imported_unit} must belong to std_units.mass, not the local mass"
17306                );
17307            }
17308
17309            let kg_owner = consumer_types
17310                .unit_index
17311                .unique_owner("kg")
17312                .expect("kg owner");
17313            let tonne_owner = consumer_types
17314                .unit_index
17315                .unique_owner("tonne")
17316                .expect("tonne owner");
17317            assert_eq!(kg_owner.as_ref(), local_mass.as_ref());
17318            assert_eq!(tonne_owner.as_ref(), local_mass.as_ref());
17319        }
17320
17321        #[test]
17322        fn same_family_same_unit_same_factor_is_idempotent() {
17323            let code = r#"spec units_a
17324    data duration: measure
17325      -> unit hour: 1
17326
17327    spec consumer
17328    uses a: units_a
17329    uses b: units_a
17330    rule smoke: true
17331    "#;
17332            let (resolver, spec_arcs) = resolver_for_code(code);
17333            let units_a_arc = spec_arcs
17334                .iter()
17335                .find(|spec| spec.name == "units_a")
17336                .cloned()
17337                .expect("units_a spec");
17338            let consumer_arc = spec_arcs
17339                .iter()
17340                .find(|spec| spec.name == "consumer")
17341                .cloned()
17342                .expect("consumer spec");
17343            let (context, _) = test_context_and_effective(&spec_arcs);
17344            let repository = context.workspace();
17345
17346            let mut already_resolved = ResolvedTypesMap::new();
17347            let units_a_types = resolver
17348                .resolve_and_validate(units_a_arc, &EffectiveDate::Origin, &already_resolved)
17349                .expect("units_a resolves");
17350            already_resolved.push((Arc::clone(&repository), units_a_arc, units_a_types));
17351
17352            let consumer_types = resolver
17353                .resolve_and_validate(consumer_arc, &EffectiveDate::Origin, &already_resolved)
17354                .expect("double import of same duration must be idempotent");
17355
17356            assert_eq!(
17357                consumer_types.unit_index.owners_for("hour").len(),
17358                2,
17359                "each import path keeps its hour owner"
17360            );
17361            consumer_types
17362                .unit_index
17363                .resolve("hour")
17364                .expect("bare hour collapses across identical import paths");
17365        }
17366
17367        #[test]
17368        fn different_version_families_keep_separate_hour_owners() {
17369            // Two versions are different families; merge keeps both owners.
17370            // Bare `hour` is ambiguous at use (see type_identity_versions).
17371            let code = r#"spec units 2024-01-01
17372    data duration: measure
17373      -> unit hour: 1
17374
17375    spec units 2025-01-01
17376    data duration: measure
17377      -> unit hour: 60
17378
17379    spec consumer
17380    uses a: units 2024-01-01
17381    uses b: units 2025-01-01
17382    rule smoke: true
17383    "#;
17384            let (resolver, spec_arcs) = resolver_for_code(code);
17385            let consumer_arc = spec_arcs
17386                .iter()
17387                .find(|spec| spec.name == "consumer")
17388                .cloned()
17389                .expect("consumer spec");
17390            let (context, _) = test_context_and_effective(&spec_arcs);
17391            let repository = context.workspace();
17392
17393            let mut already_resolved = ResolvedTypesMap::new();
17394            for units_arc in spec_arcs.iter().filter(|spec| spec.name == "units") {
17395                let units_types = resolver
17396                    .resolve_and_validate(units_arc, &units_arc.effective_from, &already_resolved)
17397                    .expect("units version resolves");
17398                already_resolved.push((Arc::clone(&repository), units_arc, units_types));
17399            }
17400
17401            let consumer_types = resolver
17402                .resolve_and_validate(consumer_arc, &EffectiveDate::Origin, &already_resolved)
17403                .expect("different version families resolve without merge conflict");
17404            assert_eq!(
17405                consumer_types.unit_index.owners_for("hour").len(),
17406                2,
17407                "each version keeps its own hour owner"
17408            );
17409        }
17410
17411        #[test]
17412        fn test_spec_level_same_unit_names_keep_every_declarer() {
17413            let (resolver, spec) = resolver_single_spec(
17414                r#"spec test
17415    data money_a: measure
17416      -> unit eur: 1.00
17417      -> unit usd: 0.84
17418
17419    data money_b: measure
17420      -> unit eur: 1.00
17421      -> unit usd: 1.20
17422
17423    data length_a: measure
17424      -> unit meter: 1.0
17425
17426    data length_b: measure
17427      -> unit meter: 1.0"#,
17428            );
17429
17430            let resolved = resolver
17431                .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
17432                .expect("independent declarers of one unit name must load");
17433            assert_eq!(
17434                resolved.unit_index.owners_for("eur").len(),
17435                2,
17436                "every declarer of 'eur' stays in scope"
17437            );
17438            assert!(
17439                resolved.unit_index.resolve("eur").is_err(),
17440                "bare 'eur' with two declarers must be qualified"
17441            );
17442            assert_eq!(
17443                resolved.unit_index.owners_for("usd").len(),
17444                2,
17445                "every declarer of 'usd' stays in scope"
17446            );
17447            assert!(
17448                resolved.unit_index.resolve("usd").is_err(),
17449                "bare 'usd' with two declarers must be qualified"
17450            );
17451            assert_eq!(
17452                resolved.unit_index.owners_for("meter").len(),
17453                2,
17454                "every declarer of 'meter' stays in scope"
17455            );
17456            assert!(
17457                resolved.unit_index.resolve("meter").is_err(),
17458                "bare 'meter' with two declarers must be qualified"
17459            );
17460            let (_, owner) = resolved
17461                .unit_index
17462                .resolve("money_b.usd")
17463                .expect("qualified unit must resolve");
17464            assert_eq!(owner.name(), "money_b");
17465        }
17466
17467        #[test]
17468        fn test_measure_and_ratio_sharing_a_unit_name_keep_both_declarers() {
17469            let (resolver, spec) = resolver_single_spec(
17470                r#"spec test
17471    data q: measure
17472      -> unit foo: 1
17473
17474    data r: ratio
17475      -> unit foo: 100"#,
17476            );
17477
17478            let resolved = resolver
17479                .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
17480                .expect("independent declarers of one unit name must load");
17481            assert_eq!(
17482                resolved.unit_index.owners_for("foo").len(),
17483                2,
17484                "every declarer of 'foo' stays in scope"
17485            );
17486            assert!(
17487                resolved.unit_index.resolve("foo").is_err(),
17488                "bare 'foo' with two declarers must be qualified"
17489            );
17490            let (_, owner) = resolved
17491                .unit_index
17492                .resolve("r.foo")
17493                .expect("qualified unit must resolve");
17494            assert_eq!(owner.name(), "r");
17495        }
17496
17497        #[test]
17498        fn test_same_ratio_unit_across_types_keeps_both_declarers() {
17499            let (resolver, spec) = resolver_single_spec(
17500                r#"spec test
17501    data spread_a: ratio
17502      -> unit basis_points: 10000
17503
17504    data spread_b: ratio
17505      -> unit basis_points: 10000"#,
17506            );
17507
17508            let resolved = resolver
17509                .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
17510                .expect("independent declarers of one unit name must load");
17511            assert_eq!(
17512                resolved.unit_index.owners_for("basis_points").len(),
17513                2,
17514                "every declarer of 'basis_points' stays in scope"
17515            );
17516            assert!(
17517                resolved.unit_index.resolve("basis_points").is_err(),
17518                "bare 'basis_points' with two declarers must be qualified"
17519            );
17520            let (_, owner) = resolved
17521                .unit_index
17522                .resolve("spread_a.basis_points")
17523                .expect("qualified unit must resolve");
17524            assert_eq!(owner.name(), "spread_a");
17525        }
17526
17527        #[test]
17528        fn test_different_ratio_unit_factor_across_types_keeps_both_declarers() {
17529            let (resolver, spec) = resolver_single_spec(
17530                r#"spec test
17531    data spread_a: ratio
17532      -> unit basis_points: 10000
17533
17534    data spread_b: ratio
17535      -> unit basis_points: 5000"#,
17536            );
17537
17538            let resolved = resolver
17539                .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
17540                .expect("independent declarers of one unit name must load");
17541            assert_eq!(
17542                resolved.unit_index.owners_for("basis_points").len(),
17543                2,
17544                "every declarer of 'basis_points' stays in scope"
17545            );
17546            assert!(
17547                resolved.unit_index.resolve("basis_points").is_err(),
17548                "bare 'basis_points' with two declarers must be qualified"
17549            );
17550            let (_, owner) = resolved
17551                .unit_index
17552                .resolve("spread_b.basis_points")
17553                .expect("qualified unit must resolve");
17554            assert_eq!(owner.name(), "spread_b");
17555        }
17556
17557        #[test]
17558        fn test_multiple_builtin_ratio_types_share_percent_in_unit_index() {
17559            let (resolver, spec) = resolver_single_spec(
17560                r#"spec targets
17561    data standard_margin_pct: ratio
17562      -> minimum 0%
17563      -> suggest 15%
17564
17565    data default_credit_insurance_pct: ratio
17566      -> suggest 1.5%"#,
17567            );
17568
17569            resolver
17570                .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
17571                .expect("multiple ratio types with built-in percent must not conflict");
17572        }
17573
17574        #[test]
17575        fn test_three_ratio_types_share_builtin_and_custom_unit() {
17576            let (resolver, spec) = resolver_single_spec(
17577                r#"spec test
17578    data margin: ratio -> suggest 10%
17579    data fee: ratio
17580      -> unit tenths: 10
17581    data tax: ratio -> suggest 1%"#,
17582            );
17583
17584            resolver
17585                .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
17586                .expect("builtin percent/permille plus custom tenths on second type must load");
17587        }
17588
17589        #[test]
17590        fn test_ratio_unit_index_allows_builtin_after_two_named_types() {
17591            let (resolver, spec) = resolver_single_spec(
17592                r#"spec test
17593    data first_ratio: ratio -> suggest 5%
17594    data second_ratio: ratio -> suggest 10%"#,
17595            );
17596
17597            let resolved = resolver
17598                .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
17599                .expect("two ratio types with only builtin units");
17600
17601            let (_, percent) = resolved
17602                .unit_index
17603                .resolve("percent")
17604                .expect("bare percent binds PrimitiveRatio");
17605            assert_eq!(percent.name(), "ratio");
17606            let (_, permille) = resolved
17607                .unit_index
17608                .resolve("permille")
17609                .expect("bare permille binds PrimitiveRatio");
17610            assert_eq!(permille.name(), "ratio");
17611        }
17612
17613        #[test]
17614        fn test_number_type_cannot_have_units() {
17615            let (resolver, spec) = resolver_single_spec(
17616                r#"spec test
17617    data price: number
17618      -> unit eur: 1.00"#,
17619            );
17620
17621            let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
17622            assert!(result.is_err(), "Number types must reject unit commands");
17623
17624            let errs = result.unwrap_err();
17625            assert!(!errs.is_empty(), "expected at least one error");
17626            let error_msg = errs[0].to_string();
17627            assert!(
17628                error_msg.contains("unit") && error_msg.contains("number"),
17629                "Error should mention units are invalid on number. Got: {}",
17630                error_msg
17631            );
17632        }
17633
17634        #[test]
17635        fn test_extending_type_inherits_units() {
17636            let (resolver, spec) = resolver_single_spec(
17637                r#"spec test
17638    data money: measure
17639      -> unit eur: 1.00
17640      -> unit usd: 0.84
17641
17642    data my_money: money
17643      -> unit gbp: 1.30"#,
17644            );
17645
17646            let resolved = resolver
17647                .resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new())
17648                .unwrap();
17649            let my_money_type = resolved.resolved.get("my_money").unwrap();
17650
17651            match &my_money_type.specifications {
17652                TypeSpecification::Measure { units, .. } => {
17653                    assert_eq!(units.len(), 3);
17654                    assert!(units.iter().any(|u| u.name == "eur"));
17655                    assert!(units.iter().any(|u| u.name == "usd"));
17656                    assert!(units.iter().any(|u| u.name == "gbp"));
17657                }
17658                other => panic!("Expected Measure type specifications, got {:?}", other),
17659            }
17660        }
17661
17662        #[test]
17663        fn binding_unit_factor_override_errors() {
17664            let (resolver, spec) = resolver_single_spec(
17665                r#"spec test
17666    data source_measure: measure
17667      -> unit usd: 1.00
17668    data z: source_measure
17669      -> unit usd: 0.84"#,
17670            );
17671
17672            let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
17673            assert!(
17674                result.is_err(),
17675                "binding row must not override inherited unit factor"
17676            );
17677            let error_msg = result
17678                .unwrap_err()
17679                .iter()
17680                .map(ToString::to_string)
17681                .collect::<Vec<_>>()
17682                .join("; ");
17683            assert!(
17684                error_msg.contains("usd"),
17685                "error must name unit usd, got: {error_msg}"
17686            );
17687        }
17688
17689        #[test]
17690        fn ratio_extension_unit_factor_override_errors() {
17691            let (resolver, spec) = resolver_single_spec(
17692                r#"spec test
17693    data parent: ratio
17694      -> unit basis: 1
17695    data child: parent
17696      -> unit basis: 100"#,
17697            );
17698
17699            let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
17700            assert!(
17701                result.is_err(),
17702                "ratio extension must not override inherited unit factor"
17703            );
17704            let error_msg = result
17705                .unwrap_err()
17706                .iter()
17707                .map(ToString::to_string)
17708                .collect::<Vec<_>>()
17709                .join("; ");
17710            assert!(
17711                error_msg.contains("basis"),
17712                "error must name unit basis, got: {error_msg}"
17713            );
17714        }
17715
17716        #[test]
17717        fn test_duplicate_unit_name_in_same_type_is_error() {
17718            let (resolver, spec) = resolver_single_spec(
17719                r#"spec test
17720    data money: measure
17721      -> unit eur: 1.00
17722      -> unit eur: 1.19"#,
17723            );
17724
17725            let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
17726            assert!(result.is_err(), "duplicate unit name must be rejected");
17727            let errs = result.unwrap_err();
17728            assert!(!errs.is_empty());
17729            let msg = errs[0].to_string();
17730            assert!(
17731                msg.contains("eur"),
17732                "error must name the duplicate unit; got: {msg}"
17733            );
17734        }
17735
17736        #[test]
17737        fn test_duplicate_unit_name_compound_is_error() {
17738            let (resolver, spec) = resolver_single_spec(
17739                r#"spec test
17740    data currency: measure
17741      -> unit usd: 1.00
17742      -> unit eur: 0.92
17743
17744    data time_period: measure
17745      -> unit year: 1
17746
17747    data run_rate: measure
17748      -> unit arr: usd/year
17749      -> unit arr: eur/year"#,
17750            );
17751
17752            let result = resolver.resolve_and_validate(spec, &EffectiveDate::Origin, &Vec::new());
17753            assert!(
17754                result.is_err(),
17755                "duplicate compound unit name must be rejected"
17756            );
17757            let errs = result.unwrap_err();
17758            assert!(!errs.is_empty());
17759            let msg = errs[0].to_string();
17760            assert!(
17761                msg.contains("arr"),
17762                "error must name the duplicate unit; got: {msg}"
17763            );
17764        }
17765    }
17766
17767    mod validation {
17768        use super::super::*;
17769        use crate::computation::rational::rational_new;
17770        use crate::literals::UnitScale;
17771        use crate::parsing::ast::{CommandArg, TypeConstraintCommand};
17772        use crate::planning::semantics::TypeSpecification;
17773        use rust_decimal::Decimal;
17774
17775        fn test_source() -> Source {
17776            Source::new(
17777                crate::parsing::source::SourceType::Volatile,
17778                crate::parsing::ast::Span {
17779                    start: 0,
17780                    end: 0,
17781                    line: 1,
17782                    col: 0,
17783                },
17784            )
17785        }
17786
17787        fn apply(
17788            mut specs: TypeSpecification,
17789            command: TypeConstraintCommand,
17790            args: &[CommandArg],
17791        ) -> TypeSpecification {
17792            let mut suggestion: Option<RawSuggestion> = None;
17793            specs
17794                .apply_constraint(
17795                    ConstraintNames {
17796                        type_name: "test",
17797                        data_name: "test",
17798                    },
17799                    command,
17800                    args,
17801                    &mut suggestion,
17802                    &mut None,
17803                    None,
17804                )
17805                .unwrap();
17806            specs
17807        }
17808
17809        fn number_arg(n: i64) -> CommandArg {
17810            CommandArg::Literal(crate::literals::Value::Number(Decimal::from(n)))
17811        }
17812
17813        fn date_arg(s: &str) -> CommandArg {
17814            let dt = s.parse::<crate::literals::DateTimeValue>().expect("date");
17815            CommandArg::Literal(crate::literals::Value::Date(dt))
17816        }
17817
17818        fn time_arg(s: &str) -> CommandArg {
17819            let t = s.parse::<crate::literals::TimeValue>().expect("time");
17820            CommandArg::Literal(crate::literals::Value::Time(t))
17821        }
17822
17823        #[test]
17824        fn validate_number_minimum_greater_than_maximum() {
17825            let mut specs = TypeSpecification::number();
17826            specs = apply(specs, TypeConstraintCommand::Minimum, &[number_arg(100)]);
17827            specs = apply(specs, TypeConstraintCommand::Maximum, &[number_arg(50)]);
17828
17829            let src = test_source();
17830            let errors = validate_type_specifications(
17831                &specs,
17832                None,
17833                "suggestion",
17834                "test",
17835                &src,
17836                None,
17837                &UnitIndex::new(),
17838            );
17839            assert_eq!(errors.len(), 1);
17840            assert!(errors[0]
17841                .to_string()
17842                .contains("minimum 100 is greater than maximum 50"));
17843        }
17844
17845        #[test]
17846        fn validate_number_default_below_minimum() {
17847            let specs = TypeSpecification::Number {
17848                minimum: Some(rational_new(10, 1)),
17849                maximum: None,
17850                decimals: None,
17851                help: String::new(),
17852            };
17853            let default = BoundValueKind::unbound(ValueKind::Number(rational_new(5, 1)));
17854
17855            let src = test_source();
17856            let errors = validate_type_specifications(
17857                &specs,
17858                Some(&default),
17859                "suggestion",
17860                "test",
17861                &src,
17862                None,
17863                &UnitIndex::new(),
17864            );
17865            assert_eq!(errors.len(), 1);
17866            assert!(errors[0]
17867                .to_string()
17868                .contains("suggestion value 5 is less than minimum 10"));
17869        }
17870
17871        #[test]
17872        fn validate_number_default_above_maximum() {
17873            let specs = TypeSpecification::Number {
17874                minimum: None,
17875                maximum: Some(rational_new(100, 1)),
17876                decimals: None,
17877                help: String::new(),
17878            };
17879            let default = BoundValueKind::unbound(ValueKind::Number(rational_new(150, 1)));
17880
17881            let src = test_source();
17882            let errors = validate_type_specifications(
17883                &specs,
17884                Some(&default),
17885                "suggestion",
17886                "test",
17887                &src,
17888                None,
17889                &UnitIndex::new(),
17890            );
17891            assert_eq!(errors.len(), 1);
17892            assert!(errors[0]
17893                .to_string()
17894                .contains("suggestion value 150 is greater than maximum 100"));
17895        }
17896
17897        #[test]
17898        fn validate_number_default_valid() {
17899            let specs = TypeSpecification::Number {
17900                minimum: Some(rational_new(0, 1)),
17901                maximum: Some(rational_new(100, 1)),
17902                decimals: None,
17903                help: String::new(),
17904            };
17905            let default = BoundValueKind::unbound(ValueKind::Number(rational_new(50, 1)));
17906
17907            let src = test_source();
17908            let errors = validate_type_specifications(
17909                &specs,
17910                Some(&default),
17911                "suggestion",
17912                "test",
17913                &src,
17914                None,
17915                &UnitIndex::new(),
17916            );
17917            assert!(errors.is_empty());
17918        }
17919
17920        #[test]
17921        fn text_minimum_command_is_rejected() {
17922            let mut specs = TypeSpecification::text();
17923            let res = specs.apply_constraint(
17924                ConstraintNames {
17925                    type_name: "test",
17926                    data_name: "test",
17927                },
17928                TypeConstraintCommand::Minimum,
17929                &[number_arg(5)],
17930                &mut None,
17931                &mut None,
17932                None,
17933            );
17934            assert!(res.is_err());
17935            assert!(res
17936                .unwrap_err()
17937                .contains("Invalid command 'minimum' for text type"));
17938        }
17939
17940        #[test]
17941        fn text_maximum_command_is_rejected() {
17942            let mut specs = TypeSpecification::text();
17943            let res = specs.apply_constraint(
17944                ConstraintNames {
17945                    type_name: "test",
17946                    data_name: "test",
17947                },
17948                TypeConstraintCommand::Maximum,
17949                &[number_arg(5)],
17950                &mut None,
17951                &mut None,
17952                None,
17953            );
17954            assert!(res.is_err());
17955            assert!(res
17956                .unwrap_err()
17957                .contains("Invalid command 'maximum' for text type"));
17958        }
17959
17960        #[test]
17961        fn validate_text_default_not_in_options() {
17962            let mut lemma_type = LemmaType::primitive(TypeSpecification::Text {
17963                length: None,
17964                help: String::new(),
17965            });
17966            lemma_type.options.values = vec![
17967                crate::planning::semantics::OptionValue {
17968                    value: crate::planning::semantics::BoundValueKind::unbound(ValueKind::Text(
17969                        Arc::from("red"),
17970                    )),
17971                },
17972                crate::planning::semantics::OptionValue {
17973                    value: crate::planning::semantics::BoundValueKind::unbound(ValueKind::Text(
17974                        Arc::from("blue"),
17975                    )),
17976                },
17977            ];
17978            let default = BoundValueKind::unbound(ValueKind::Text(Arc::from("green")));
17979
17980            let src = test_source();
17981            let errors = validate_type_options(
17982                &lemma_type,
17983                Some(&default),
17984                "suggestion",
17985                "test",
17986                &src,
17987                None,
17988                &UnitIndex::new(),
17989            );
17990            assert_eq!(errors.len(), 1);
17991            assert!(
17992                errors[0].to_string().contains("green")
17993                    && errors[0].to_string().contains("allowed options")
17994            );
17995        }
17996
17997        #[test]
17998        fn validate_ratio_minimum_greater_than_maximum() {
17999            let specs = TypeSpecification::Ratio {
18000                minimum: Some(rational_new(2, 1)),
18001                maximum: Some(rational_new(1, 1)),
18002                units: crate::planning::semantics::RatioUnits::new(),
18003                unit: None,
18004                help: String::new(),
18005            };
18006
18007            let src = test_source();
18008            let errors = validate_type_specifications(
18009                &specs,
18010                None,
18011                "suggestion",
18012                "test",
18013                &src,
18014                None,
18015                &UnitIndex::new(),
18016            );
18017            assert_eq!(errors.len(), 1);
18018            assert!(errors[0]
18019                .to_string()
18020                .contains("minimum 2 is greater than maximum 1"));
18021        }
18022
18023        #[test]
18024        fn validate_date_minimum_after_maximum() {
18025            let mut specs = TypeSpecification::date();
18026            specs = apply(
18027                specs,
18028                TypeConstraintCommand::Minimum,
18029                &[date_arg("2024-12-31")],
18030            );
18031            specs = apply(
18032                specs,
18033                TypeConstraintCommand::Maximum,
18034                &[date_arg("2024-01-01")],
18035            );
18036
18037            let src = test_source();
18038            let errors = validate_type_specifications(
18039                &specs,
18040                None,
18041                "suggestion",
18042                "test",
18043                &src,
18044                None,
18045                &UnitIndex::new(),
18046            );
18047            assert_eq!(errors.len(), 1);
18048            assert!(
18049                errors[0].to_string().contains("minimum")
18050                    && errors[0].to_string().contains("is after maximum")
18051            );
18052        }
18053
18054        #[test]
18055        fn validate_date_valid_range() {
18056            let mut specs = TypeSpecification::date();
18057            specs = apply(
18058                specs,
18059                TypeConstraintCommand::Minimum,
18060                &[date_arg("2024-01-01")],
18061            );
18062            specs = apply(
18063                specs,
18064                TypeConstraintCommand::Maximum,
18065                &[date_arg("2024-12-31")],
18066            );
18067
18068            let src = test_source();
18069            let errors = validate_type_specifications(
18070                &specs,
18071                None,
18072                "suggestion",
18073                "test",
18074                &src,
18075                None,
18076                &UnitIndex::new(),
18077            );
18078            assert!(errors.is_empty());
18079        }
18080
18081        #[test]
18082        fn validate_time_minimum_after_maximum() {
18083            let mut specs = TypeSpecification::time();
18084            specs = apply(
18085                specs,
18086                TypeConstraintCommand::Minimum,
18087                &[time_arg("23:00:00")],
18088            );
18089            specs = apply(
18090                specs,
18091                TypeConstraintCommand::Maximum,
18092                &[time_arg("10:00:00")],
18093            );
18094
18095            let src = test_source();
18096            let errors = validate_type_specifications(
18097                &specs,
18098                None,
18099                "suggestion",
18100                "test",
18101                &src,
18102                None,
18103                &UnitIndex::new(),
18104            );
18105            assert_eq!(errors.len(), 1);
18106            assert!(
18107                errors[0].to_string().contains("minimum")
18108                    && errors[0].to_string().contains("is after maximum")
18109            );
18110        }
18111
18112        #[test]
18113        fn large_magnitude_minimum_does_not_fail_type_validation() {
18114            use crate::computation::rational::rational_one;
18115            use crate::literals::MeasureUnits;
18116            use crate::planning::semantics::TypeSpecification;
18117
18118            let too_large = magnitude_beyond_decimal_max();
18119            assert_eq!(
18120                too_large.try_to_decimal().unwrap_err(),
18121                crate::computation::rational::NumericFailure::Overflow,
18122            );
18123
18124            let spec = TypeSpecification::Measure {
18125                minimum: Some((too_large, "eur".to_string())),
18126                maximum: None,
18127                units: MeasureUnits(vec![crate::literals::MeasureUnit {
18128                    name: "eur".to_string(),
18129                    scale: UnitScale::Literal(rational_one()),
18130                    derived_measure_factors: Default::default(),
18131                    decomposition: Default::default(),
18132                    minimum: None,
18133                    maximum: None,
18134                    suggestion_magnitude: None,
18135                }]),
18136                traits: vec![],
18137                decomposition: None,
18138                unit: None,
18139                help: String::new(),
18140            };
18141
18142            let src = test_source();
18143            let errors = validate_type_specifications(
18144                &spec,
18145                None,
18146                "suggestion",
18147                "money",
18148                &src,
18149                None,
18150                &UnitIndex::new(),
18151            );
18152            assert!(
18153                errors.is_empty(),
18154                "internal Q bounds must not fail type validation; API decimal rounding: {:?}",
18155                errors.iter().map(|e| e.to_string()).collect::<Vec<_>>()
18156            );
18157        }
18158
18159        #[test]
18160        fn large_magnitude_maximum_does_not_fail_type_validation() {
18161            use crate::computation::rational::rational_one;
18162            use crate::literals::MeasureUnits;
18163            use crate::planning::semantics::TypeSpecification;
18164
18165            let too_large = magnitude_beyond_decimal_max();
18166            assert_eq!(
18167                too_large.try_to_decimal().unwrap_err(),
18168                crate::computation::rational::NumericFailure::Overflow,
18169            );
18170
18171            let spec = TypeSpecification::Measure {
18172                minimum: None,
18173                maximum: Some((too_large, "eur".to_string())),
18174                units: MeasureUnits(vec![crate::literals::MeasureUnit {
18175                    name: "eur".to_string(),
18176                    scale: UnitScale::Literal(rational_one()),
18177                    derived_measure_factors: Default::default(),
18178                    decomposition: Default::default(),
18179                    minimum: None,
18180                    maximum: None,
18181                    suggestion_magnitude: None,
18182                }]),
18183                traits: vec![],
18184                decomposition: None,
18185                unit: None,
18186                help: String::new(),
18187            };
18188
18189            let src = test_source();
18190            let errors = validate_type_specifications(
18191                &spec,
18192                None,
18193                "suggestion",
18194                "money",
18195                &src,
18196                None,
18197                &UnitIndex::new(),
18198            );
18199            assert!(errors.is_empty(), "got: {:?}", errors);
18200        }
18201
18202        #[test]
18203        fn large_magnitude_default_does_not_fail_type_validation() {
18204            use crate::computation::rational::rational_one;
18205            use crate::literals::MeasureUnits;
18206            use crate::planning::semantics::TypeSpecification;
18207
18208            let too_large = magnitude_beyond_decimal_max();
18209            assert_eq!(
18210                too_large.try_to_decimal().unwrap_err(),
18211                crate::computation::rational::NumericFailure::Overflow,
18212            );
18213
18214            let spec = TypeSpecification::Measure {
18215                minimum: None,
18216                maximum: None,
18217                units: MeasureUnits(vec![crate::literals::MeasureUnit {
18218                    name: "eur".to_string(),
18219                    scale: UnitScale::Literal(rational_one()),
18220                    derived_measure_factors: Default::default(),
18221                    decomposition: Default::default(),
18222                    minimum: None,
18223                    maximum: None,
18224                    suggestion_magnitude: Some(too_large),
18225                }]),
18226                traits: vec![],
18227                decomposition: None,
18228                unit: None,
18229                help: String::new(),
18230            };
18231
18232            let src = test_source();
18233            let errors = validate_type_specifications(
18234                &spec,
18235                None,
18236                "suggestion",
18237                "money",
18238                &src,
18239                None,
18240                &UnitIndex::new(),
18241            );
18242            assert!(errors.is_empty(), "got: {:?}", errors);
18243        }
18244
18245        fn magnitude_beyond_decimal_max() -> crate::computation::rational::RationalInteger {
18246            use crate::computation::rational::{decimal_to_rational, rational_new, try_mul};
18247            use rust_decimal::Decimal;
18248            let max = Decimal::MAX.normalize();
18249            let max_rational = decimal_to_rational(max).expect("BUG: Decimal::MAX must lift to Q");
18250            try_mul(&max_rational, &rational_new(2, 1))
18251                .expect("BUG: test rational multiply must succeed")
18252        }
18253
18254        #[test]
18255        fn empty_measure_signature_unit_must_not_coerce_as_unknown_empty_string() {
18256            use crate::computation::rational::rational_new;
18257            use crate::literals::MeasureUnits;
18258            use crate::planning::semantics::{
18259                LemmaType, TypeSpecification, TypedLiteral, ValueKind,
18260            };
18261
18262            let schema_type = Arc::new(LemmaType {
18263                name: Some("money".to_string()),
18264                extends: TypeExtends::Primitive,
18265                specifications: TypeSpecification::Measure {
18266                    minimum: None,
18267                    maximum: None,
18268                    units: MeasureUnits(vec![crate::literals::MeasureUnit {
18269                        name: "eur".to_string(),
18270                        scale: UnitScale::Literal(rational_new(1, 1)),
18271                        derived_measure_factors: Default::default(),
18272                        decomposition: Default::default(),
18273                        minimum: None,
18274                        maximum: None,
18275                        suggestion_magnitude: None,
18276                    }]),
18277                    traits: vec![],
18278                    decomposition: Default::default(),
18279                    unit: None,
18280                    help: String::new(),
18281                },
18282                options: TypeOptions::default(),
18283            });
18284
18285            let literal = TypedLiteral {
18286                value: ValueKind::Measure(rational_new(1, 1)),
18287                lemma_type: Arc::clone(&schema_type),
18288            };
18289
18290            let result = Graph::coerce_literal_to_schema_type(&literal, &schema_type);
18291
18292            // Binding unit lives on lemma_type; equal Measure specs coerce without a
18293            // ValueKind signature. Empty binding is valid.
18294            assert!(
18295                result.is_ok(),
18296                "measure with matching schema type must coerce, got: {result:?}"
18297            );
18298        }
18299
18300        #[test]
18301        fn refresh_named_range_specs_missing_element_must_not_leave_unconverted_measure() {
18302            use crate::engine::Context;
18303            use crate::parsing::ast::{LemmaSpec, ParentType, Span};
18304            use crate::parsing::source::{Source, SourceType};
18305            use crate::planning::semantics::tests::primitive_measure_arc;
18306
18307            let mut ctx = Context::new();
18308            let repository = ctx.workspace();
18309            ctx.insert_spec(Arc::clone(&repository), LemmaSpec::new("t".to_string()))
18310                .unwrap();
18311            let ctx: &'static Context = Box::leak(Box::new(ctx));
18312            let mut resolver = TypeResolver::new(ctx, HashMap::new());
18313            let spec = ctx
18314                .spec_set(&ctx.workspace(), "t")
18315                .and_then(|ss| ss.get_exact(None))
18316                .expect("inserted");
18317            assert!(resolver.register_all(&ctx.workspace(), spec).is_empty());
18318            let source = Source::new(
18319                SourceType::Volatile,
18320                Span {
18321                    start: 0,
18322                    end: 0,
18323                    line: 1,
18324                    col: 0,
18325                },
18326            );
18327            let mut data_defs = HashMap::new();
18328            data_defs.insert(
18329                "band".to_string(),
18330                DataTypeDef {
18331                    parent: ParentType::Ranged {
18332                        inner: Box::new(ParentType::custom("ghost")),
18333                    },
18334                    constraints: None,
18335                    source: source.clone(),
18336                    name: "band".to_string(),
18337                    bound_literal: None,
18338                },
18339            );
18340            let mut resolved_types = ResolvedSpecTypes::default();
18341            resolved_types
18342                .resolved
18343                .insert("band".to_string(), primitive_measure_arc().clone());
18344            let already_resolved = ResolvedTypesMap::new();
18345
18346            let errors = refresh_named_range_specs(
18347                &resolver,
18348                spec,
18349                &data_defs,
18350                &mut resolved_types,
18351                &already_resolved,
18352                &EffectiveDate::Origin,
18353            );
18354
18355            assert!(
18356                !errors.is_empty(),
18357                "planning must error when ranged inner type is missing"
18358            );
18359            assert!(
18360                errors[0]
18361                    .to_string()
18362                    .contains("Unknown parent 'ghost' for data definition"),
18363                "got: {}",
18364                errors[0]
18365            );
18366        }
18367
18368        fn apply_n_text_options(option_count: usize) -> LemmaType {
18369            let spec = LemmaSpec::new("wide_options".to_string());
18370            let source = test_source();
18371            let constraints: Vec<Constraint> = (0..option_count)
18372                .map(|i| {
18373                    Constraint::new(
18374                        TypeConstraintCommand::Option,
18375                        vec![CommandArg::Literal(crate::literals::Value::Text(
18376                            i.to_string(),
18377                        ))],
18378                        source.clone(),
18379                    )
18380                })
18381                .collect();
18382            let mut suggestion = None;
18383            let mut value = None;
18384            let mut lemma_type = LemmaType::primitive(TypeSpecification::text());
18385            apply_constraints_to_spec(
18386                &spec,
18387                "code",
18388                "code",
18389                &mut lemma_type,
18390                &constraints,
18391                &source,
18392                ConstraintSuggestionSink {
18393                    suggestion: &mut suggestion,
18394                    value: &mut value,
18395                    resolve_unit: None,
18396                },
18397            )
18398            .expect("options must apply");
18399            lemma_type
18400        }
18401
18402        #[test]
18403        fn apply_constraints_many_text_options_preserve_count() {
18404            for count in [16_usize, 64] {
18405                assert_eq!(apply_n_text_options(count).options.values.len(), count);
18406            }
18407        }
18408    }
18409
18410    mod decomposition_promotion {
18411        use super::super::*;
18412        use crate::engine::Context;
18413        use crate::literals::UnitScale;
18414        use crate::parsing::parse;
18415        use crate::planning::typing::range_span_type;
18416        use crate::ResourceLimits;
18417        use std::sync::Arc;
18418
18419        fn insert_parsed_repos(ctx: &mut Context, code: &str) {
18420            let result = parse(
18421                code,
18422                crate::parsing::source::SourceType::Volatile,
18423                &ResourceLimits::default(),
18424            )
18425            .expect("parse");
18426            for (repo_arc, specs) in result.repositories {
18427                for spec in specs {
18428                    ctx.insert_spec(Arc::clone(&repo_arc), spec)
18429                        .expect("insert spec");
18430                }
18431            }
18432        }
18433
18434        fn repo_by_name(ctx: &Context, name: &str) -> Arc<LemmaRepository> {
18435            ctx.repositories()
18436                .keys()
18437                .find(|r| r.name.as_deref() == Some(name))
18438                .cloned()
18439                .expect("repository must exist after insert")
18440        }
18441
18442        fn resolve_specs_in_order<'a>(
18443            resolver: &TypeResolver<'a>,
18444            repo: &Arc<LemmaRepository>,
18445            specs: &[&'a LemmaSpec],
18446            local_types: &mut ResolvedTypesMap<'a>,
18447        ) {
18448            for spec in specs {
18449                let types = resolver
18450                    .resolve_and_validate(spec, &EffectiveDate::Origin, local_types)
18451                    .unwrap_or_else(|e| panic!("resolve {} failed: {:?}", spec.name, e));
18452                local_types.push((Arc::clone(repo), *spec, types));
18453            }
18454        }
18455
18456        fn worker_torque_fixture() -> (
18457            ResolvedTypesMap<'static>,
18458            &'static LemmaSpec,
18459            BaseMeasureVector,
18460            Arc<LemmaType>,
18461        ) {
18462            let mut ctx = Context::new();
18463            insert_parsed_repos(&mut ctx, crate::stdlib::UNITS_LEMMA);
18464            let alpha_code = r#"repo alpha
18465    spec units
18466    data force: measure
18467      -> unit newton: 1
18468    data length: measure
18469      -> unit meter: 1
18470    data torque: measure
18471      -> unit nm: newton*meter
18472
18473    spec worker
18474    uses u: alpha units
18475    data f: 3 newton
18476    data d: 4 meter
18477    rule t: f * d
18478    "#;
18479            insert_parsed_repos(&mut ctx, alpha_code);
18480            let ctx = Box::leak(Box::new(ctx));
18481            let alpha_repo = repo_by_name(ctx, "alpha");
18482            let worker = ctx
18483                .spec_set(&alpha_repo, "worker")
18484                .and_then(|ss| ss.get_exact(None))
18485                .expect("worker spec");
18486            let units = ctx
18487                .spec_set(&alpha_repo, "units")
18488                .and_then(|ss| ss.get_exact(None))
18489                .expect("alpha units spec");
18490            let lemma_repo = repo_by_name(ctx, crate::engine::EMBEDDED_STDLIB_REPOSITORY);
18491            let lemma_units = ctx
18492                .spec_set(&lemma_repo, "units")
18493                .expect("lemma repo")
18494                .get_exact(None)
18495                .expect("lemma units spec");
18496
18497            let mut resolver = TypeResolver::new(ctx, HashMap::new());
18498            for spec in [units, worker] {
18499                resolver
18500                    .register_all(&alpha_repo, spec)
18501                    .into_iter()
18502                    .for_each(|e| panic!("register alpha: {e:?}"));
18503            }
18504            resolver
18505                .register_all(&lemma_repo, lemma_units)
18506                .into_iter()
18507                .for_each(|e| panic!("register lemma units: {e:?}"));
18508
18509            let mut local_types = ResolvedTypesMap::new();
18510            resolve_specs_in_order(
18511                &resolver,
18512                &lemma_repo,
18513                std::slice::from_ref(&lemma_units),
18514                &mut local_types,
18515            );
18516            resolve_specs_in_order(
18517                &resolver,
18518                &alpha_repo,
18519                std::slice::from_ref(&units),
18520                &mut local_types,
18521            );
18522            resolve_specs_in_order(
18523                &resolver,
18524                &alpha_repo,
18525                std::slice::from_ref(&worker),
18526                &mut local_types,
18527            );
18528
18529            let alpha_torque = local_types
18530                .iter()
18531                .find(|(_, s, _)| discovery::same_loaded_spec(s, units))
18532                .and_then(|(_, _, t)| t.resolved.get("torque"))
18533                .expect("alpha torque type")
18534                .clone();
18535            let decomp = alpha_torque
18536                .measure_type_decomposition()
18537                .expect("torque decomposition")
18538                .clone();
18539
18540            (local_types, worker, decomp, alpha_torque)
18541        }
18542
18543        #[test]
18544        fn unique_decomposition_carries_matching_arc() {
18545            let (map, worker_spec, decomp, alpha_torque) = worker_torque_fixture();
18546            let unique = find_unique_measure_type_in_unit_index(
18547                &find_types_by_spec(&map, worker_spec)
18548                    .expect("worker in map")
18549                    .unit_index,
18550                &decomp,
18551            );
18552            match unique {
18553                DecompositionMatch::Unique(arc) => {
18554                    assert_eq!(*arc, *alpha_torque);
18555                    assert_eq!(arc.name.as_deref(), Some("torque"));
18556                }
18557                other => panic!("expected Unique(Arc) torque match, got {other:?}"),
18558            }
18559        }
18560
18561        #[test]
18562        fn find_unique_ignores_polluted_resolved_map() {
18563            let (mut map, worker_spec, decomp, alpha_torque) = worker_torque_fixture();
18564            map.iter_mut()
18565                .find(|(_, s, _)| discovery::same_loaded_spec(s, worker_spec))
18566                .expect("worker must be in resolved map")
18567                .2
18568                .resolved = IndexMap::new();
18569
18570            let unique = find_unique_measure_type_in_unit_index(
18571                &find_types_by_spec(&map, worker_spec)
18572                    .expect("worker in map")
18573                    .unit_index,
18574                &decomp,
18575            );
18576            match unique {
18577                DecompositionMatch::Unique(arc) => {
18578                    assert_eq!(*arc, *alpha_torque);
18579                    assert_eq!(arc.name.as_deref(), Some("torque"));
18580                }
18581                other => panic!("expected Unique(alpha torque) via unit_index, got {other:?}"),
18582            }
18583        }
18584
18585        fn weight_measure_type() -> Arc<LemmaType> {
18586            use crate::computation::rational::{decimal_to_rational, rational_one};
18587            use rust_decimal::Decimal;
18588            Arc::new(LemmaType::new(
18589                "weight".to_string(),
18590                TypeSpecification::Measure {
18591                    minimum: None,
18592                    maximum: None,
18593                    units: MeasureUnits(vec![
18594                        MeasureUnit {
18595                            name: "gram".to_string(),
18596                            scale: UnitScale::Literal(rational_one()),
18597                            derived_measure_factors: Vec::new(),
18598                            decomposition: Default::default(),
18599                            minimum: None,
18600                            maximum: None,
18601                            suggestion_magnitude: None,
18602                        },
18603                        MeasureUnit {
18604                            name: "kilogram".to_string(),
18605                            scale: UnitScale::Literal(
18606                                decimal_to_rational(Decimal::from(1000))
18607                                    .expect("BUG: kilogram factor must be rational"),
18608                            ),
18609                            derived_measure_factors: Vec::new(),
18610                            decomposition: Default::default(),
18611                            minimum: None,
18612                            maximum: None,
18613                            suggestion_magnitude: None,
18614                        },
18615                    ]),
18616                    traits: Vec::new(),
18617                    decomposition: None,
18618                    unit: None,
18619                    help: String::new(),
18620                },
18621                TypeExtends::custom_local(
18622                    "measure".to_string(),
18623                    crate::planning::semantics::tests::test_type_family("weight"),
18624                ),
18625            ))
18626        }
18627
18628        fn weight_measure_range_type() -> Arc<LemmaType> {
18629            let weight = weight_measure_type();
18630            Arc::new(LemmaType {
18631                name: weight.name.clone(),
18632                specifications: weight
18633                    .specifications
18634                    .range_from_element()
18635                    .expect("BUG: weight measure defines MeasureRange"),
18636                extends: weight.extends.clone(),
18637                options: TypeOptions::default(),
18638            })
18639        }
18640
18641        fn duration_like_measure_type() -> Arc<LemmaType> {
18642            Arc::new(LemmaType::anonymous_for_decomposition(
18643                duration_decomposition(),
18644            ))
18645        }
18646
18647        fn calendar_like_measure_type() -> Arc<LemmaType> {
18648            use crate::computation::rational::rational_one;
18649            use crate::planning::semantics::MeasureTrait;
18650            Arc::new(LemmaType::new(
18651                "calendar".to_string(),
18652                TypeSpecification::Measure {
18653                    minimum: None,
18654                    maximum: None,
18655                    units: MeasureUnits(vec![MeasureUnit {
18656                        name: "month".to_string(),
18657                        scale: UnitScale::Literal(rational_one()),
18658                        derived_measure_factors: Vec::new(),
18659                        decomposition: calendar_decomposition(),
18660                        minimum: None,
18661                        maximum: None,
18662                        suggestion_magnitude: None,
18663                    }]),
18664                    traits: vec![MeasureTrait::Calendar],
18665                    decomposition: Some(calendar_decomposition()),
18666                    unit: None,
18667                    help: String::new(),
18668                },
18669                TypeExtends::Primitive,
18670            ))
18671        }
18672
18673        #[test]
18674        fn range_span_type_date_range_is_duration_span_not_date_range() {
18675            let date_range = Arc::new(LemmaType::primitive(TypeSpecification::date_range()));
18676            let span = range_span_type(&date_range);
18677            assert!(span.is_duration_like_measure());
18678            assert!(!span.is_date_range());
18679            assert!(!span.is_date());
18680        }
18681
18682        #[test]
18683        fn range_span_type_time_range_is_duration_span() {
18684            let time_range = Arc::new(LemmaType::primitive(TypeSpecification::time_range()));
18685            let span = range_span_type(&time_range);
18686            assert!(span.is_duration_like_measure());
18687            assert!(!span.is_time_range());
18688            assert!(!span.is_time());
18689        }
18690
18691        #[test]
18692        fn range_span_type_number_range_is_number() {
18693            let number_range = Arc::new(LemmaType::primitive(TypeSpecification::number_range()));
18694            let span = range_span_type(&number_range);
18695            assert!(span.is_number());
18696        }
18697
18698        #[test]
18699        fn range_span_type_measure_range_preserves_name_and_extends() {
18700            let weight_range = weight_measure_range_type();
18701            let span = range_span_type(&weight_range);
18702            assert!(span.is_measure());
18703            assert!(!span.is_measure_range());
18704            assert_eq!(span.name.as_deref(), Some("weight"));
18705            assert_eq!(span.extends, weight_range.extends);
18706            assert!(span.same_measure_family(weight_measure_type().as_ref()));
18707        }
18708
18709        #[test]
18710        fn range_span_type_anonymous_measure_range_preserves_units() {
18711            use crate::computation::rational::{decimal_to_rational, rational_one};
18712            use rust_decimal::Decimal;
18713            let units = MeasureUnits(vec![
18714                MeasureUnit {
18715                    name: "gram".to_string(),
18716                    scale: UnitScale::Literal(rational_one()),
18717                    derived_measure_factors: Vec::new(),
18718                    decomposition: Default::default(),
18719                    minimum: None,
18720                    maximum: None,
18721                    suggestion_magnitude: None,
18722                },
18723                MeasureUnit {
18724                    name: "kilogram".to_string(),
18725                    scale: UnitScale::Literal(
18726                        decimal_to_rational(Decimal::from(1000))
18727                            .expect("BUG: kilogram factor must be rational"),
18728                    ),
18729                    derived_measure_factors: Vec::new(),
18730                    decomposition: Default::default(),
18731                    minimum: None,
18732                    maximum: None,
18733                    suggestion_magnitude: None,
18734                },
18735            ]);
18736            let measure_range = Arc::new(LemmaType::without_name(
18737                TypeSpecification::MeasureRange {
18738                    lower: None,
18739                    upper: None,
18740                    minimum: None,
18741                    maximum: None,
18742                    units: units.clone(),
18743                    decomposition: None,
18744                    help: String::new(),
18745                },
18746                TypeExtends::Primitive,
18747            ));
18748            let span = range_span_type(&measure_range);
18749            assert!(span.is_measure());
18750            assert!(span.is_anonymous_measure());
18751            match &span.specifications {
18752                TypeSpecification::Measure {
18753                    units: span_units, ..
18754                } => {
18755                    assert_eq!(span_units, &units);
18756                }
18757                other => panic!("expected Measure span, got {other:?}"),
18758            }
18759        }
18760
18761        #[test]
18762        fn range_span_type_ratio_range_is_ratio() {
18763            let ratio_range = Arc::new(LemmaType::primitive(TypeSpecification::ratio_range()));
18764            let span = range_span_type(&ratio_range);
18765            assert!(span.is_ratio());
18766        }
18767
18768        #[test]
18769        fn range_span_type_non_range_is_undetermined() {
18770            let boolean = Arc::new(LemmaType::primitive(TypeSpecification::boolean()));
18771            let span = range_span_type(&boolean);
18772            assert!(span.is_undetermined());
18773        }
18774
18775        #[test]
18776        fn arithmetic_measure_range_plus_measure_yields_named_measure_span() {
18777            let weight_range = weight_measure_range_type();
18778            let gram = weight_measure_type();
18779            let unit_index = UnitIndex::new();
18780            let signature_index = crate::computation::arithmetic::SignatureIndex::new();
18781            let named = IndexMap::new();
18782            let scope = MeasureScope {
18783                unit_index: &unit_index,
18784                signature_index: &signature_index,
18785                named_types: &named,
18786            };
18787            let result = compute_arithmetic_result_type(
18788                weight_range,
18789                &ArithmeticComputation::Add,
18790                gram,
18791                &scope,
18792            );
18793            assert!(result.is_measure());
18794            assert!(!result.is_measure_range());
18795            assert_eq!(result.name.as_deref(), Some("weight"));
18796            assert!(result.same_measure_family(weight_measure_type().as_ref()));
18797        }
18798
18799        #[test]
18800        fn arithmetic_measure_range_minus_measure_yields_named_measure_span() {
18801            let weight_range = weight_measure_range_type();
18802            let gram = weight_measure_type();
18803            let unit_index = UnitIndex::new();
18804            let signature_index = crate::computation::arithmetic::SignatureIndex::new();
18805            let named = IndexMap::new();
18806            let scope = MeasureScope {
18807                unit_index: &unit_index,
18808                signature_index: &signature_index,
18809                named_types: &named,
18810            };
18811            let result = compute_arithmetic_result_type(
18812                weight_range,
18813                &ArithmeticComputation::Subtract,
18814                gram,
18815                &scope,
18816            );
18817            assert!(result.is_measure());
18818            assert!(!result.is_measure_range());
18819            assert_eq!(result.name.as_deref(), Some("weight"));
18820            assert!(result.same_measure_family(weight_measure_type().as_ref()));
18821        }
18822
18823        #[test]
18824        fn arithmetic_date_range_plus_duration_yields_duration_span_not_date_range() {
18825            let date_range = Arc::new(LemmaType::primitive(TypeSpecification::date_range()));
18826            let duration = duration_like_measure_type();
18827            let unit_index = UnitIndex::new();
18828            let signature_index = crate::computation::arithmetic::SignatureIndex::new();
18829            let named = IndexMap::new();
18830            let scope = MeasureScope {
18831                unit_index: &unit_index,
18832                signature_index: &signature_index,
18833                named_types: &named,
18834            };
18835            let result = compute_arithmetic_result_type(
18836                date_range,
18837                &ArithmeticComputation::Add,
18838                duration,
18839                &scope,
18840            );
18841            assert!(result.is_duration_like_measure());
18842            assert!(!result.is_date_range());
18843            assert!(!result.is_date());
18844        }
18845
18846        #[test]
18847        fn arithmetic_date_range_plus_calendar_yields_date_range() {
18848            let date_range = Arc::new(LemmaType::primitive(TypeSpecification::date_range()));
18849            let calendar = calendar_like_measure_type();
18850            let unit_index = UnitIndex::new();
18851            let signature_index = crate::computation::arithmetic::SignatureIndex::new();
18852            let named = IndexMap::new();
18853            let scope = MeasureScope {
18854                unit_index: &unit_index,
18855                signature_index: &signature_index,
18856                named_types: &named,
18857            };
18858            let result = compute_arithmetic_result_type(
18859                date_range,
18860                &ArithmeticComputation::Add,
18861                calendar,
18862                &scope,
18863            );
18864            assert!(result.is_date_range());
18865        }
18866    }
18867}
18868
18869// ============================================================================
18870// Type resolution
18871// ============================================================================
18872
18873/// Fully resolved types for a single spec.
18874/// After resolution, all imports are inlined — specs are independent.
18875#[derive(Debug, Clone, Default)]
18876pub struct ResolvedSpecTypes {
18877    /// Resolved [`LemmaType`] for each **data type row name** declared in this spec (`data name: …`).
18878    /// Planning-only: includes measure units and post-`resolve_measure_decompositions` decomposition.
18879    pub resolved: IndexMap<String, Arc<LemmaType>>,
18880
18881    /// Declared default per named type (e.g. `type rate: ratio -> suggest 50%`).
18882    /// Only present for types that declared a `-> suggest ...` constraint anywhere
18883    /// in their extension chain; the inner-most `-> suggest` wins. Defaults live
18884    /// outside [`TypeSpecification`] so the type itself stays free of binding data.
18885    /// Populated after [`bound_value_kind_from_raw_suggestion`] (post-decomposition).
18886    pub declared_suggestions: IndexMap<String, BoundValueKind>,
18887
18888    /// Declared value per named type (e.g. `type rate: ratio -> with 50%`).
18889    /// Only present for types that declared a `-> with ...` constraint anywhere
18890    /// in their extension chain; the inner-most `-> with` wins.
18891    pub declared_values: IndexMap<String, BoundValueKind>,
18892
18893    /// Defaults captured during type resolution, before measure unit factors are final.
18894    pub(crate) raw_suggestions: Vec<(String, RawSuggestion)>,
18895
18896    /// Raw fills captured during type resolution, before measure unit factors are final.
18897    pub(crate) raw_values: Vec<(String, RawSuggestion)>,
18898
18899    /// Raw defaults retained after [`bound_value_kind_from_raw_suggestion`] for cross-spec parent lookup during later specs.
18900    pub(crate) source_defaults: IndexMap<String, RawSuggestion>,
18901
18902    /// Raw fills retained for cross-spec parent lookup during later specs.
18903    pub(crate) source_value_defaults: IndexMap<String, RawSuggestion>,
18904
18905    /// List types written in this spec's rules (`money list -> with ...`), keyed by the
18906    /// written type, in order of first appearance.
18907    pub(crate) rule_list_types: Vec<(ParentType, Arc<LemmaType>)>,
18908
18909    /// Expression-scope units. One declarer per bare unit name; qualify with Type.unit or alias.Type.unit.
18910    /// Binding aliases never appear as index keys.
18911    pub unit_index: crate::planning::unit_index::UnitIndex,
18912
18913    /// Cross-type unit-signature collisions. Emitted after rules are typed, unless a
18914    /// rule result is already the anonymous measure of that decomposition.
18915    pub(crate) deferred_signature_collisions:
18916        Vec<(String, crate::planning::semantics::BaseMeasureVector)>,
18917
18918    /// Names of unit owners until [`PlanningUnitIndex::bind`] after magnitudes.
18919    planning_unit_index: PlanningUnitIndex,
18920
18921    /// Reverse index: canonical-form unit signature → (unit_name, owning type).
18922    /// Built from [`Self::unit_index`] during resolve; cloned into the execution plan.
18923    pub signature_index: crate::computation::arithmetic::SignatureIndex,
18924}
18925
18926/// Intermediate type definition extracted from [`DataValue::Definition`] data.
18927#[derive(Debug, Clone, PartialEq)]
18928pub(crate) struct DataTypeDef {
18929    pub parent: ParentType,
18930    pub constraints: Option<Vec<Constraint>>,
18931    pub source: crate::parsing::source::Source,
18932    pub name: String,
18933    /// When the source row was `data N: <literal>` (no explicit parent type), the AST literal.
18934    pub bound_literal: Option<ast::Value>,
18935}
18936
18937///
18938/// Named types are extracted from [`DataValue::Definition`] data and stored in
18939/// DAG-parallel maps keyed by Context-owned `&LemmaSpec` identity.
18940#[derive(Debug, Clone)]
18941pub(crate) struct TypeResolver<'a> {
18942    data_types: Vec<(&'a LemmaSpec, HashMap<String, DataTypeDef>)>,
18943    context: &'a Context,
18944    all_registered_specs: Vec<(Arc<LemmaRepository>, &'a LemmaSpec)>,
18945    /// Specs for which [`TypeResolver::register_all`] could not register every
18946    /// type-declaring data row. Planning must skip building these specs:
18947    /// downstream code (`add_data`) relies on every type-resolving data row
18948    /// being present in the resolved type map.
18949    specs_with_failed_registration: Vec<&'a LemmaSpec>,
18950    /// Slices of every spec version in the dependency order of the graph being built.
18951    slices: HashMap<SpecVersionKey, Arc<instance_list::VersionSlices>>,
18952}
18953
18954/// Parent type used for measure-family lookup, unwrapping [`ParentType::Ranged`].
18955fn element_parent_type(parent: &ParentType) -> &ParentType {
18956    let mut current = parent;
18957    while let ParentType::Ranged { inner } | ParentType::List { inner } = current {
18958        current = inner.as_ref();
18959    }
18960    current
18961}
18962
18963fn time_range_endpoints_share_timezone(
18964    left: &semantics::SemanticTime,
18965    right: &semantics::SemanticTime,
18966) -> bool {
18967    match (&left.timezone, &right.timezone) {
18968        (None, None) => true,
18969        (Some(left_tz), Some(right_tz)) => left_tz.offset_minutes == right_tz.offset_minutes,
18970        _ => false,
18971    }
18972}
18973
18974/// Display name for a parser literal's base type, matching the names produced
18975/// by [`LemmaType::name`] for the corresponding semantic literal. Used in
18976/// range endpoint validation error messages.
18977fn parser_value_base_type_display_name(value: &ast::Value) -> &'static str {
18978    match value {
18979        ast::Value::Number(_) => "number",
18980        ast::Value::Text(_) => "text",
18981        ast::Value::Boolean(_) => "boolean",
18982        ast::Value::Date(_) => "date",
18983        ast::Value::Time(_) => "time",
18984        ast::Value::NumberWithUnit(_, unit) => match unit.as_str() {
18985            "percent" | "permille" => "ratio",
18986            _ => "measure",
18987        },
18988        ast::Value::Range(_, _) => "range",
18989    }
18990}
18991
18992/// Infer primitive [`ParentType`] from a literal RHS (`data x: 3.14`).
18993///
18994/// Returns an error for range literals whose endpoint types are not a
18995/// supported range element combination (e.g. `1 ... yes`, `"a" ... "b"`).
18996fn inferred_parent_type_from_literal(value: &ast::Value) -> Result<ParentType, String> {
18997    let parent_type = match value {
18998        ast::Value::Number(_) => ParentType::Primitive {
18999            primitive: PrimitiveKind::Number,
19000        },
19001        ast::Value::Text(_) => ParentType::Primitive {
19002            primitive: PrimitiveKind::Text,
19003        },
19004        ast::Value::Boolean(_) => ParentType::Primitive {
19005            primitive: PrimitiveKind::Boolean,
19006        },
19007        ast::Value::Date(_) => ParentType::Primitive {
19008            primitive: PrimitiveKind::Date,
19009        },
19010        ast::Value::Time(_) => ParentType::Primitive {
19011            primitive: PrimitiveKind::Time,
19012        },
19013        ast::Value::NumberWithUnit(_, _) => ParentType::Primitive {
19014            primitive: PrimitiveKind::Measure,
19015        },
19016        ast::Value::Range(left, right) => {
19017            let primitive = match (left.as_ref(), right.as_ref()) {
19018                (ast::Value::Number(_), ast::Value::Number(_)) => PrimitiveKind::NumberRange,
19019                (ast::Value::Date(_), ast::Value::Date(_)) => PrimitiveKind::DateRange,
19020                (ast::Value::Time(_), ast::Value::Time(_)) => PrimitiveKind::TimeRange,
19021                (ast::Value::NumberWithUnit(_, u1), ast::Value::NumberWithUnit(_, u2))
19022                    if u1 == u2 && matches!(u1.as_str(), "percent" | "permille") =>
19023                {
19024                    PrimitiveKind::RatioRange
19025                }
19026                (ast::Value::NumberWithUnit(_, _), ast::Value::NumberWithUnit(_, _)) => {
19027                    PrimitiveKind::MeasureRange
19028                }
19029                (left_value, right_value) => {
19030                    return Err(format!(
19031                        "range endpoints must have the same supported base type, got {} and {}",
19032                        parser_value_base_type_display_name(left_value),
19033                        parser_value_base_type_display_name(right_value)
19034                    ));
19035                }
19036            };
19037            ParentType::Primitive { primitive }
19038        }
19039    };
19040    Ok(parent_type)
19041}
19042
19043impl<'a> TypeResolver<'a> {
19044    pub fn new(
19045        context: &'a Context,
19046        slices: HashMap<SpecVersionKey, Arc<instance_list::VersionSlices>>,
19047    ) -> Self {
19048        TypeResolver {
19049            data_types: Vec::new(),
19050            context,
19051            all_registered_specs: Vec::new(),
19052            specs_with_failed_registration: Vec::new(),
19053            slices,
19054        }
19055    }
19056
19057    /// Identity of `spec`, resolved at `at`, in the slice that holds `at`.
19058    fn identity_at(
19059        &self,
19060        repository: &LemmaRepository,
19061        spec: &LemmaSpec,
19062        at: &EffectiveDate,
19063    ) -> SpecIdentity {
19064        self.slices
19065            .get(&SpecVersionKey::of(repository, spec))
19066            .expect("BUG: a spec type resolution reaches is in the graph's dependency order")
19067            .identity_at(repository, spec, at)
19068    }
19069
19070    /// Spec-element `name list` written in a rule: list type with Spec element (numbered key;
19071    /// `-> key` refines the key during conversion).
19072    fn rule_spec_element_list_type(
19073        &self,
19074        spec: &LemmaSpec,
19075        parent: &ParentType,
19076        source: &Source,
19077        at: &EffectiveDate,
19078    ) -> Option<Arc<LemmaType>> {
19079        let (name, spec_ref) = Self::list_of_spec_name(spec, parent)?;
19080        let (row_repository, row_spec) = match discovery::resolve_spec_ref_or_self(
19081            self.context,
19082            &spec_ref,
19083            self.registered_repository(spec),
19084            spec,
19085            at,
19086            Some(source.clone()),
19087        ) {
19088            Ok(pair) => pair,
19089            Err(_) => return None,
19090        };
19091        let identity = self.identity_at(&row_repository, row_spec, &spec_ref.at(at));
19092        let element = Arc::new(LemmaType::primitive(TypeSpecification::Spec {
19093            spec: identity,
19094        }));
19095        Some(Arc::new(LemmaType {
19096            name: Some(format!("{name} list")),
19097            specifications: TypeSpecification::List {
19098                element,
19099                key: ListKey::Numbered,
19100                help: String::new(),
19101            },
19102            extends: TypeExtends::Primitive,
19103            options: TypeOptions::default(),
19104        }))
19105    }
19106
19107    /// `name` and its row spec when `parent` is `name list` of spec rows: `name` is a local
19108    /// name of a `uses` line (`uses tb: tax_bracket` then `data brackets: tb list`) or the
19109    /// spec's own name ([`discovery::list_element_spec_ref`]).
19110    fn list_of_spec_name<'p>(
19111        spec: &LemmaSpec,
19112        parent: &'p ParentType,
19113    ) -> Option<(&'p str, ast::SpecRef)> {
19114        let ParentType::List { inner } = parent else {
19115            return None;
19116        };
19117        let ParentType::Custom { name } = inner.as_ref() else {
19118            return None;
19119        };
19120        discovery::list_element_spec_ref(spec, inner).map(|spec_ref| (name.as_str(), spec_ref))
19121    }
19122
19123    pub fn is_registered(&self, spec: &LemmaSpec) -> bool {
19124        self.all_registered_specs
19125            .iter()
19126            .any(|(_, s)| discovery::same_loaded_spec(s, spec))
19127    }
19128
19129    /// Repository `spec` was registered under by [`Self::register_all`].
19130    fn registered_repository(&self, spec: &LemmaSpec) -> &Arc<LemmaRepository> {
19131        self.all_registered_specs
19132            .iter()
19133            .find(|(_, registered)| discovery::same_loaded_spec(registered, spec))
19134            .map(|(repository, _)| repository)
19135            .expect("BUG: types are resolved only for specs registered with their repository")
19136    }
19137
19138    /// Record that a type-declaring data row of `spec` could not be registered.
19139    fn mark_registration_failure(&mut self, spec: &'a LemmaSpec) {
19140        if !self
19141            .specs_with_failed_registration
19142            .iter()
19143            .any(|s| discovery::same_loaded_spec(s, spec))
19144        {
19145            self.specs_with_failed_registration.push(spec);
19146        }
19147    }
19148
19149    /// Whether [`TypeResolver::register_all`] failed to register a
19150    /// type-declaring data row of `spec`. The corresponding errors were
19151    /// returned by `register_all`; specs with failed registrations must not
19152    /// be built.
19153    pub fn registration_failed(&self, spec: &LemmaSpec) -> bool {
19154        self.specs_with_failed_registration
19155            .iter()
19156            .any(|s| discovery::same_loaded_spec(s, spec))
19157    }
19158
19159    /// Register all type-declaring data from a spec.
19160    pub fn register_all(
19161        &mut self,
19162        repository: &Arc<LemmaRepository>,
19163        spec: &'a LemmaSpec,
19164    ) -> Vec<Error> {
19165        self.record_repository(repository, spec);
19166
19167        let mut errors = Vec::new();
19168        for data in &spec.data {
19169            match &data.value {
19170                ParsedDataValue::Definition {
19171                    base,
19172                    constraints,
19173                    value,
19174                    list: _,
19175                } => {
19176                    if matches!(
19177                        (base.as_ref(), constraints.as_ref(), value.as_ref()),
19178                        (None, None, Some(Value::NumberWithUnit(_, _)),)
19179                    ) {
19180                        continue;
19181                    }
19182                    let name = &data.reference.name;
19183                    let parent = match (base.as_ref(), value.as_ref()) {
19184                        (Some(b), _) => b.clone(),
19185                        (None, Some(v)) => match inferred_parent_type_from_literal(v) {
19186                            Ok(parent) => parent,
19187                            Err(message) => {
19188                                errors.push(Error::validation_with_context(
19189                                    message,
19190                                    Some(data.source_location.clone()),
19191                                    None::<String>,
19192                                    Some(spec),
19193                                    None,
19194                                ));
19195                                self.mark_registration_failure(spec);
19196                                continue;
19197                            }
19198                        },
19199                        (None, None) => {
19200                            errors.push(Error::validation_with_context(
19201                                format!(
19202                                    "Data '{name}' in spec '{}' must declare a type or a literal value",
19203                                    spec.name
19204                                ),
19205                                Some(data.source_location.clone()),
19206                                None::<String>,
19207                                Some(spec),
19208                                None,
19209                            ));
19210                            self.mark_registration_failure(spec);
19211                            continue;
19212                        }
19213                    };
19214                    // Spec-element instance lists become DataDefinition::InstanceList in
19215                    // add_instance_list; they are not named types in the type registry.
19216                    if Self::list_of_spec_name(spec, &parent).is_some() {
19217                        continue;
19218                    }
19219                    let ftd = DataTypeDef {
19220                        parent,
19221                        constraints: constraints.clone(),
19222                        source: data.source_location.clone(),
19223                        name: name.clone(),
19224                        bound_literal: value.clone(),
19225                    };
19226                    if let Err(e) = self.register_type(repository, spec, ftd) {
19227                        errors.push(e);
19228                    }
19229                }
19230                ParsedDataValue::Import { .. } | ParsedDataValue::ImportList { .. } => {}
19231            }
19232        }
19233        errors
19234    }
19235
19236    fn record_repository(&mut self, repository: &Arc<LemmaRepository>, spec: &'a LemmaSpec) {
19237        if !self
19238            .all_registered_specs
19239            .iter()
19240            .any(|(_, s)| discovery::same_loaded_spec(s, spec))
19241        {
19242            self.all_registered_specs
19243                .push((Arc::clone(repository), spec));
19244        }
19245    }
19246
19247    /// Register a type from a data declaration of `spec`, loaded under `repository`.
19248    pub fn register_type(
19249        &mut self,
19250        repository: &Arc<LemmaRepository>,
19251        spec: &'a LemmaSpec,
19252        def: DataTypeDef,
19253    ) -> Result<(), Error> {
19254        self.record_repository(repository, spec);
19255        let spec_types = if let Some(pos) = self
19256            .data_types
19257            .iter()
19258            .position(|(s, _)| discovery::same_loaded_spec(s, spec))
19259        {
19260            &mut self.data_types[pos].1
19261        } else {
19262            self.data_types.push((spec, HashMap::new()));
19263            let last = self.data_types.len() - 1;
19264            &mut self.data_types[last].1
19265        };
19266        if spec_types.contains_key(&def.name) {
19267            return Err(Error::validation_with_context(
19268                format!(
19269                    "The name '{}' is already used for data in this spec.",
19270                    def.name
19271                ),
19272                Some(def.source.clone()),
19273                None::<String>,
19274                Some(spec),
19275                None,
19276            ));
19277        }
19278        spec_types.insert(def.name.clone(), def);
19279        Ok(())
19280    }
19281
19282    /// Resolve types for a single spec and validate their specifications.
19283    /// `at` is the planning instant for this spec (nested qualified refs use their pin).
19284    pub fn resolve_and_validate(
19285        &self,
19286        spec: &LemmaSpec,
19287        at: &EffectiveDate,
19288        already_resolved: &ResolvedTypesMap,
19289    ) -> Result<ResolvedSpecTypes, Vec<Error>> {
19290        let (mut resolved_types, list_type_names) =
19291            self.resolve_types_internal(spec, at, already_resolved)?;
19292        resolved_types.source_defaults = resolved_types
19293            .raw_suggestions
19294            .iter()
19295            .map(|(name, raw)| (name.clone(), raw.clone()))
19296            .collect();
19297        resolved_types.source_value_defaults = resolved_types
19298            .raw_values
19299            .iter()
19300            .map(|(name, raw)| (name.clone(), raw.clone()))
19301            .collect();
19302        let mut errors = Vec::new();
19303
19304        // Build the type-name → source map for precise error reporting.
19305        let type_sources: std::collections::HashMap<String, Source> = resolved_types
19306            .resolved
19307            .keys()
19308            .chain(list_type_names.iter())
19309            .filter_map(|type_name| {
19310                self.data_types
19311                    .iter()
19312                    .find(|(s, _)| discovery::same_loaded_spec(s, spec))
19313                    .and_then(|(_, defs)| defs.get(type_name.as_str()))
19314                    .map(|ftd| (type_name.clone(), ftd.source.clone()))
19315            })
19316            .collect();
19317
19318        // Run the decomposition pass to populate `BaseMeasureVector` on all Measure types.
19319        errors.extend(rewrite_unit_path_scale_payloads(
19320            spec,
19321            &mut resolved_types.resolved,
19322            &resolved_types.planning_unit_index,
19323            &type_sources,
19324        ));
19325        let this_spec = SpecVersionKey::of(self.registered_repository(spec), spec);
19326        let (new_resolved, decomp_errors) = resolve_measure_decompositions(
19327            &spec.name,
19328            &this_spec,
19329            std::mem::take(&mut resolved_types.resolved),
19330            &resolved_types.planning_unit_index,
19331            &type_sources,
19332        );
19333        resolved_types.resolved = new_resolved;
19334        errors.extend(decomp_errors);
19335
19336        for (type_name, raw) in std::mem::take(&mut resolved_types.raw_suggestions) {
19337            let lemma_type = resolved_types
19338                .resolved
19339                .get(&type_name)
19340                .expect("BUG: raw default for type not in resolved");
19341            match bound_value_kind_from_raw_suggestion(raw, &lemma_type.specifications, &type_name)
19342            {
19343                Ok(value_kind) => {
19344                    resolved_types
19345                        .declared_suggestions
19346                        .insert(type_name, value_kind);
19347                }
19348                Err(message) => {
19349                    let source = type_sources
19350                        .get(&type_name)
19351                        .cloned()
19352                        .unwrap_or_else(|| unreachable!("BUG: type '{}' has no source", type_name));
19353                    errors.push(Error::validation_with_context(
19354                        message,
19355                        Some(source),
19356                        None::<String>,
19357                        Some(spec),
19358                        None,
19359                    ));
19360                }
19361            }
19362        }
19363
19364        for (type_name, raw) in std::mem::take(&mut resolved_types.raw_values) {
19365            let lemma_type = resolved_types
19366                .resolved
19367                .get(&type_name)
19368                .expect("BUG: raw fill for type not in resolved");
19369            match bound_value_kind_from_raw_suggestion(raw, &lemma_type.specifications, &type_name)
19370            {
19371                Ok(value_kind) => {
19372                    resolved_types.declared_values.insert(type_name, value_kind);
19373                }
19374                Err(message) => {
19375                    let source = type_sources
19376                        .get(&type_name)
19377                        .cloned()
19378                        .unwrap_or_else(|| unreachable!("BUG: type '{}' has no source", type_name));
19379                    errors.push(Error::validation_with_context(
19380                        message,
19381                        Some(source),
19382                        None::<String>,
19383                        Some(spec),
19384                        None,
19385                    ));
19386                }
19387            }
19388        }
19389
19390        if let Some((_, data_defs)) = self
19391            .data_types
19392            .iter()
19393            .find(|(s, _)| discovery::same_loaded_spec(s, spec))
19394        {
19395            errors.extend(refresh_named_range_specs(
19396                self,
19397                spec,
19398                data_defs,
19399                &mut resolved_types,
19400                already_resolved,
19401                at,
19402            ));
19403            errors.extend(apply_deferred_named_range_constraints(
19404                spec,
19405                data_defs,
19406                &mut resolved_types.resolved,
19407                &mut resolved_types.declared_suggestions,
19408                &mut resolved_types.declared_values,
19409                &type_sources,
19410                &resolved_types.planning_unit_index,
19411            ));
19412        }
19413
19414        let (new_resolved, resolved_errors) = finalize_measure_magnitudes_in_resolved(
19415            std::mem::take(&mut resolved_types.resolved),
19416            &resolved_types.declared_suggestions,
19417            &type_sources,
19418            &spec.name,
19419            spec,
19420        );
19421        resolved_types.resolved = new_resolved;
19422
19423        resolved_types.unit_index = std::mem::take(&mut resolved_types.planning_unit_index)
19424            .bind(&resolved_types.resolved, &this_spec);
19425
19426        match build_signature_index(&spec.name, &resolved_types.unit_index, already_resolved) {
19427            Ok(built) => {
19428                resolved_types.signature_index = built.index;
19429                resolved_types.deferred_signature_collisions = built.deferred_cross_type;
19430            }
19431            Err(error) => errors.push(error),
19432        }
19433
19434        if !resolved_errors.is_empty() {
19435            errors.extend(resolved_errors);
19436            return Err(errors);
19437        }
19438
19439        if let Err(list_errors) = self.resolve_list_types(
19440            spec,
19441            at,
19442            already_resolved,
19443            &list_type_names,
19444            &mut resolved_types,
19445        ) {
19446            errors.extend(list_errors);
19447            return Err(errors);
19448        }
19449
19450        if let Err(rule_list_errors) =
19451            self.resolve_rule_list_types(spec, at, already_resolved, &mut resolved_types)
19452        {
19453            errors.extend(rule_list_errors);
19454            return Err(errors);
19455        }
19456
19457        let mut validated_in_unit_index: HashSet<String> = HashSet::new();
19458        for lemma_type in resolved_types.unit_index.values() {
19459            let Some(measure_family) = lemma_type.measure_family_name() else {
19460                continue;
19461            };
19462            if !lemma_type.is_measure()
19463                || !validated_in_unit_index.insert(measure_family.to_string())
19464            {
19465                continue;
19466            }
19467            let type_name = lemma_type
19468                .name
19469                .as_deref()
19470                .filter(|name| *name == measure_family)
19471                .unwrap_or(measure_family);
19472            let source = type_sources
19473                .get(type_name)
19474                .cloned()
19475                .or_else(|| {
19476                    self.data_types
19477                        .iter()
19478                        .find_map(|(_, defs)| defs.get(type_name).map(|def| def.source.clone()))
19479                })
19480                .unwrap_or_else(|| {
19481                    unreachable!(
19482                        "BUG: measure type '{}' in unit_index has no DataTypeDef source",
19483                        type_name
19484                    )
19485                });
19486            errors.extend(validate_type_specifications(
19487                &lemma_type.specifications,
19488                resolved_types.declared_suggestions.get(type_name),
19489                "suggestion",
19490                type_name,
19491                &source,
19492                Some(spec),
19493                &resolved_types.unit_index,
19494            ));
19495            errors.extend(validate_type_specifications(
19496                &lemma_type.specifications,
19497                resolved_types.declared_values.get(type_name),
19498                "with",
19499                type_name,
19500                &source,
19501                Some(spec),
19502                &resolved_types.unit_index,
19503            ));
19504        }
19505
19506        for (type_name, lemma_type) in &resolved_types.resolved {
19507            let source = type_sources.get(type_name).cloned().unwrap_or_else(|| {
19508                unreachable!(
19509                    "BUG: resolved type '{}' has no corresponding DataTypeDef in spec '{}'",
19510                    type_name, spec.name
19511                )
19512            });
19513            let mut spec_errors = validate_type_specifications(
19514                &lemma_type.specifications,
19515                resolved_types.declared_suggestions.get(type_name),
19516                "suggestion",
19517                type_name,
19518                &source,
19519                Some(spec),
19520                &resolved_types.unit_index,
19521            );
19522            errors.append(&mut spec_errors);
19523            let mut fill_errors = validate_type_specifications(
19524                &lemma_type.specifications,
19525                resolved_types.declared_values.get(type_name),
19526                "with",
19527                type_name,
19528                &source,
19529                Some(spec),
19530                &resolved_types.unit_index,
19531            );
19532            errors.append(&mut fill_errors);
19533            errors.extend(validate_type_options(
19534                lemma_type,
19535                resolved_types.declared_suggestions.get(type_name),
19536                "suggestion",
19537                type_name,
19538                &source,
19539                Some(spec),
19540                &resolved_types.unit_index,
19541            ));
19542            errors.extend(validate_type_options(
19543                lemma_type,
19544                resolved_types.declared_values.get(type_name),
19545                "with",
19546                type_name,
19547                &source,
19548                Some(spec),
19549                &resolved_types.unit_index,
19550            ));
19551        }
19552
19553        for (type_name, lemma_type) in &resolved_types.resolved {
19554            let is_range = matches!(
19555                &lemma_type.specifications,
19556                TypeSpecification::NumberRange { .. }
19557                    | TypeSpecification::DateRange { .. }
19558                    | TypeSpecification::TimeRange { .. }
19559                    | TypeSpecification::MeasureRange { .. }
19560                    | TypeSpecification::RatioRange { .. }
19561            );
19562            if !is_range {
19563                continue;
19564            }
19565            let source = type_sources.get(type_name).cloned().unwrap_or_else(|| {
19566                unreachable!(
19567                    "BUG: resolved type '{}' has no corresponding DataTypeDef in spec '{}'",
19568                    type_name, spec.name
19569                )
19570            });
19571            if let Some(default_kind) = resolved_types.declared_suggestions.get(type_name.as_str())
19572            {
19573                let lit = TypedLiteral {
19574                    value: default_kind.value.clone(),
19575                    lemma_type: Arc::clone(lemma_type),
19576                };
19577                if let Err(message) = crate::planning::execution_plan::validate_value_against_type(
19578                    lemma_type.as_ref(),
19579                    &lit.to_literal(),
19580                    &resolved_types.unit_index,
19581                ) {
19582                    errors.push(Error::validation_with_context(
19583                        format!("Type '{}' suggestion is invalid: {}", type_name, message),
19584                        Some(source.clone()),
19585                        None::<String>,
19586                        Some(spec),
19587                        None,
19588                    ));
19589                }
19590            }
19591            if let Some(default_kind) = resolved_types.declared_values.get(type_name.as_str()) {
19592                let lit = TypedLiteral {
19593                    value: default_kind.value.clone(),
19594                    lemma_type: Arc::clone(lemma_type),
19595                };
19596                if let Err(message) = crate::planning::execution_plan::validate_value_against_type(
19597                    lemma_type.as_ref(),
19598                    &lit.to_literal(),
19599                    &resolved_types.unit_index,
19600                ) {
19601                    errors.push(Error::validation_with_context(
19602                        format!("Type '{}' with is invalid: {}", type_name, message),
19603                        Some(source),
19604                        None::<String>,
19605                        Some(spec),
19606                        None,
19607                    ));
19608                }
19609            }
19610        }
19611
19612        if errors.is_empty() {
19613            Ok(resolved_types)
19614        } else {
19615            Err(errors)
19616        }
19617    }
19618
19619    // =========================================================================
19620    // Private resolution methods
19621    // =========================================================================
19622
19623    /// Resolve list types once every scalar type is final, and convert their declared rows.
19624    fn resolve_list_types(
19625        &self,
19626        spec: &LemmaSpec,
19627        at: &EffectiveDate,
19628        already_resolved: &ResolvedTypesMap,
19629        list_type_names: &[String],
19630        resolved_types: &mut ResolvedSpecTypes,
19631    ) -> Result<(), Vec<Error>> {
19632        if list_type_names.is_empty() {
19633            return Ok(());
19634        }
19635        let data_defs = self
19636            .data_types
19637            .iter()
19638            .find(|(s, _)| discovery::same_loaded_spec(s, spec))
19639            .map(|(_, defs)| defs)
19640            .expect("BUG: list type names come from this spec's type definitions");
19641        let mut local = local_type_entries(resolved_types);
19642        let mut errors = Vec::new();
19643        for type_name in list_type_names {
19644            let ftd = data_defs
19645                .get(type_name.as_str())
19646                .expect("BUG: topo-sorted list type missing from registry");
19647            let (lemma_type, suggestion, value) =
19648                self.resolve_type_def(spec, type_name, ftd, at, already_resolved, &local)?;
19649            let to_error = |message: String| {
19650                Error::validation_with_context(
19651                    message,
19652                    Some(ftd.source.clone()),
19653                    None::<String>,
19654                    Some(spec),
19655                    None,
19656                )
19657            };
19658            if let Some(raw) = &suggestion {
19659                resolved_types
19660                    .source_defaults
19661                    .insert(type_name.clone(), raw.clone());
19662                match bound_value_kind_from_raw_suggestion(
19663                    raw.clone(),
19664                    &lemma_type.specifications,
19665                    type_name,
19666                ) {
19667                    Ok(bound) => {
19668                        resolved_types
19669                            .declared_suggestions
19670                            .insert(type_name.clone(), bound);
19671                    }
19672                    Err(message) => errors.push(to_error(message)),
19673                }
19674            }
19675            if let Some(raw) = &value {
19676                resolved_types
19677                    .source_value_defaults
19678                    .insert(type_name.clone(), raw.clone());
19679                match bound_value_kind_from_raw_suggestion(
19680                    raw.clone(),
19681                    &lemma_type.specifications,
19682                    type_name,
19683                ) {
19684                    Ok(bound) => {
19685                        resolved_types
19686                            .declared_values
19687                            .insert(type_name.clone(), bound);
19688                    }
19689                    Err(message) => errors.push(to_error(message)),
19690                }
19691            }
19692            resolved_types
19693                .resolved
19694                .insert(type_name.clone(), Arc::clone(&lemma_type));
19695            local.insert(type_name.clone(), (lemma_type, suggestion, value));
19696        }
19697        if errors.is_empty() {
19698            Ok(())
19699        } else {
19700            Err(errors)
19701        }
19702    }
19703
19704    /// Resolve the list types written in `spec`'s rules against the same scope as its data
19705    /// types, once every local type is final.
19706    fn resolve_rule_list_types<'s>(
19707        &self,
19708        spec: &'s LemmaSpec,
19709        at: &EffectiveDate,
19710        already_resolved: &ResolvedTypesMap,
19711        resolved_types: &mut ResolvedSpecTypes,
19712    ) -> Result<(), Vec<Error>> {
19713        let mut written: Vec<(&'s ParentType, &'s Source)> = Vec::new();
19714        for rule in &spec.rules {
19715            let trees = std::iter::once(&rule.expression).chain(
19716                rule.unless_clauses
19717                    .iter()
19718                    .flat_map(|clause| [&clause.condition, &clause.result]),
19719            );
19720            for tree in trees {
19721                let mut pending = vec![tree.root()];
19722                while let Some(node) = pending.pop() {
19723                    if let ast::ExpressionKind::ListType { element } = node.kind() {
19724                        if written.iter().all(|(seen, _)| *seen != element) {
19725                            let source = node
19726                                .source()
19727                                .expect("BUG: AST expression missing source location");
19728                            written.push((element, source));
19729                        }
19730                    }
19731                    node.push_children(&mut pending);
19732                }
19733            }
19734        }
19735        if written.is_empty() {
19736            return Ok(());
19737        }
19738        let local = local_type_entries(resolved_types);
19739        let mut errors = Vec::new();
19740        for (element, source) in written {
19741            let scope = ParentLookup {
19742                resolver: self,
19743                spec,
19744                source,
19745                at,
19746                already_resolved,
19747                resolved: &local,
19748                site: TypeUseSite::RuleListType,
19749            };
19750            match lookup_parent_type(&scope, element) {
19751                Ok((.., lemma_type)) => {
19752                    resolved_types
19753                        .rule_list_types
19754                        .push((element.clone(), lemma_type));
19755                }
19756                Err(lookup_errors) => {
19757                    if let Some(list_type) =
19758                        self.rule_spec_element_list_type(spec, element, source, at)
19759                    {
19760                        resolved_types
19761                            .rule_list_types
19762                            .push((element.clone(), list_type));
19763                    } else {
19764                        errors.extend(lookup_errors);
19765                    }
19766                }
19767            }
19768        }
19769        if errors.is_empty() {
19770            Ok(())
19771        } else {
19772            Err(errors)
19773        }
19774    }
19775
19776    /// Resolve one data type declaration against `resolved` (already resolved local types).
19777    fn resolve_type_def(
19778        &self,
19779        spec: &LemmaSpec,
19780        type_name: &str,
19781        ftd: &DataTypeDef,
19782        at: &EffectiveDate,
19783        already_resolved: &ResolvedTypesMap,
19784        resolved: &HashMap<String, ResolvedTypeEntry>,
19785    ) -> Result<ResolvedTypeEntry, Vec<Error>> {
19786        let parent = &ftd.parent;
19787        let constraints = &ftd.constraints;
19788
19789        let scope = ParentLookup {
19790            resolver: self,
19791            spec,
19792            source: &ftd.source,
19793            at,
19794            already_resolved,
19795            resolved,
19796            site: TypeUseSite::DataDefinition,
19797        };
19798        let (parent_specs, parent_suggestion, parent_value, parent_type) =
19799            lookup_parent_type(&scope, parent)?;
19800
19801        let mut declared_suggestion = parent_suggestion;
19802        let mut declared_value = parent_value;
19803        let mut final_type = (*parent_type).clone();
19804        final_type.specifications = parent_specs;
19805        if !should_defer_ranged_constraints(parent) {
19806            if let Some(constraints) = &constraints {
19807                let resolve_unit = UnitTokenResolver::Search(
19808                    already_resolved
19809                        .iter()
19810                        .map(|(_, _, rts)| (&rts.unit_index, &rts.resolved))
19811                        .collect(),
19812                );
19813                apply_constraints_to_spec(
19814                    spec,
19815                    &constraint_application_type_name(parent, type_name),
19816                    type_name,
19817                    &mut final_type,
19818                    constraints,
19819                    &ftd.source,
19820                    ConstraintSuggestionSink {
19821                        suggestion: &mut declared_suggestion,
19822                        value: &mut declared_value,
19823                        resolve_unit: Some(&resolve_unit),
19824                    },
19825                )?;
19826            }
19827        }
19828        let final_specs = final_type.specifications;
19829        let final_options = final_type.options;
19830
19831        if matches!(&final_specs, TypeSpecification::List { .. }) && declared_value.is_none() {
19832            declared_value = Some(RawSuggestion::List(Vec::new()));
19833        }
19834
19835        let this_spec = SpecVersionKey::of(self.registered_repository(spec), spec);
19836        let family = match (element_parent_type(parent), parent_type.family()) {
19837            (ParentType::Primitive { .. }, _) | (_, None) => {
19838                TypeFamily::declared_in(&this_spec, type_name)
19839            }
19840            (_, Some(parent_family)) => parent_family.clone(),
19841        };
19842        let defining_spec = match element_parent_type(parent) {
19843            ParentType::Qualified { .. } => TypeDefiningSpec::Import,
19844            _ => TypeDefiningSpec::Local,
19845        };
19846        let extends = TypeExtends::Custom {
19847            parent: parent.to_string(),
19848            family,
19849            defining_spec,
19850        };
19851
19852        let lemma_type = Arc::new(LemmaType {
19853            name: Some(type_name.to_string()),
19854            specifications: final_specs,
19855            extends,
19856            options: final_options,
19857        });
19858
19859        let declared_suggestion = match &ftd.bound_literal {
19860            Some(literal) => {
19861                let resolve_unit = UnitTokenResolver::Search(
19862                    already_resolved
19863                        .iter()
19864                        .map(|(_, _, rts)| (&rts.unit_index, &rts.resolved))
19865                        .collect(),
19866                );
19867                match semantics::parser_value_to_value_kind(
19868                    literal,
19869                    lemma_type.as_ref(),
19870                    Some(&resolve_unit),
19871                ) {
19872                    Ok(value_kind) => Some(RawSuggestion::Value(value_kind)),
19873                    Err(message) => {
19874                        return Err(vec![Error::validation_with_context(
19875                            message,
19876                            Some(ftd.source.clone()),
19877                            None::<String>,
19878                            Some(spec),
19879                            None,
19880                        )]);
19881                    }
19882                }
19883            }
19884            None => declared_suggestion,
19885        };
19886
19887        Ok((lemma_type, declared_suggestion, declared_value))
19888    }
19889
19890    /// Resolve the spec's scalar types. Returns them with the topo-sorted list type names,
19891    /// which [`Self::resolve_list_types`] resolves once the scalar passes are done.
19892    fn resolve_types_internal(
19893        &self,
19894        spec: &LemmaSpec,
19895        at: &EffectiveDate,
19896        already_resolved: &ResolvedTypesMap,
19897    ) -> Result<(ResolvedSpecTypes, Vec<String>), Vec<Error>> {
19898        let data_defs = self
19899            .data_types
19900            .iter()
19901            .find(|(s, _)| discovery::same_loaded_spec(s, spec))
19902            .map(|(_, defs)| defs)
19903            .cloned()
19904            .unwrap_or_default();
19905
19906        let (scalar_names, list_names) = partition_list_type_names(&data_defs);
19907        let sorted_type_names = topo_sort_type_names(spec, &data_defs, scalar_names)?;
19908        let list_type_names = topo_sort_type_names(spec, &data_defs, list_names)?;
19909
19910        let mut resolved: HashMap<String, ResolvedTypeEntry> = HashMap::new();
19911
19912        for type_name in &sorted_type_names {
19913            let ftd = data_defs
19914                .get(type_name.as_str())
19915                .expect("BUG: topo-sorted type missing from registry");
19916            let entry =
19917                self.resolve_type_def(spec, type_name, ftd, at, already_resolved, &resolved)?;
19918            resolved.insert(type_name.clone(), entry);
19919        }
19920
19921        let mut unit_index = PlanningUnitIndex::new();
19922        let mut errors = Vec::new();
19923        for unit in Self::extract_units_from_type(&semantics::primitive_ratio_arc().specifications)
19924        {
19925            unit_index.insert_owner(unit, PlanningOwner::PrimitiveRatio);
19926        }
19927
19928        let locals: HashMap<String, Arc<LemmaType>> = resolved
19929            .iter()
19930            .map(|(name, (type_arc, _, _))| (name.clone(), Arc::clone(type_arc)))
19931            .collect();
19932
19933        for type_name in &sorted_type_names {
19934            let (type_arc, _, _) = resolved
19935                .get(type_name)
19936                .expect("BUG: type was resolved but missing from map");
19937            let data_type_def = data_defs
19938                .get(type_name.as_str())
19939                .expect("BUG: type was resolved but not in registry");
19940            let merge_result = if type_arc.is_measure() {
19941                if let ParentType::Qualified { spec_alias, inner } = &data_type_def.parent {
19942                    match Self::parent_units_for_qualified_extension(
19943                        self,
19944                        spec,
19945                        data_type_def,
19946                        already_resolved,
19947                        at,
19948                        spec_alias,
19949                        inner,
19950                    ) {
19951                        Ok(None) => Ok(()),
19952                        Ok(Some(parent_units)) => Self::merge_additive_measure_units(
19953                            spec,
19954                            &mut unit_index,
19955                            type_arc,
19956                            data_type_def,
19957                            &[],
19958                            &parent_units,
19959                            &locals,
19960                        ),
19961                        Err(error) => Err(error),
19962                    }
19963                } else {
19964                    Self::add_measure_units_to_index(
19965                        spec,
19966                        &mut unit_index,
19967                        type_arc,
19968                        data_type_def,
19969                        &[],
19970                        &locals,
19971                    )
19972                }
19973            } else if type_arc.is_ratio() {
19974                Self::add_ratio_units_to_index(
19975                    spec,
19976                    &mut unit_index,
19977                    type_arc,
19978                    data_type_def,
19979                    &[],
19980                    &locals,
19981                )
19982            } else {
19983                Ok(())
19984            };
19985            if let Err(error) = merge_result {
19986                errors.push(error);
19987            }
19988        }
19989
19990        for data_row in &spec.data {
19991            let import_alias = data_row.reference.name.clone();
19992            let mut merge_imported_units = |imported_resolved: &ResolvedSpecTypes| {
19993                for (bare, owner) in imported_resolved.unit_index.iter_owners() {
19994                    let planning_owner = match owner {
19995                        UnitOwner::PrimitiveRatio => PlanningOwner::PrimitiveRatio,
19996                        UnitOwner::Named {
19997                            import_path,
19998                            type_name,
19999                            lemma_type,
20000                        } => {
20001                            let mut path = vec![import_alias.clone()];
20002                            path.extend(import_path.iter().cloned());
20003                            PlanningOwner::Imported {
20004                                import_path: path,
20005                                type_name: type_name.clone(),
20006                                lemma_type: Arc::clone(lemma_type),
20007                            }
20008                        }
20009                    };
20010                    unit_index.insert_owner(bare.to_string(), planning_owner);
20011                }
20012            };
20013            match &data_row.value {
20014                ParsedDataValue::Import {
20015                    spec_ref,
20016                    effective_ref: Some(_),
20017                    ..
20018                } => {
20019                    // Dynamic pin: merge units from every candidate already type-resolved.
20020                    let resolved = match discovery::resolve_spec_ref_repository(
20021                        self.context,
20022                        spec_ref,
20023                        Some(&data_row.source_location),
20024                        spec.name.as_str(),
20025                        Some(spec),
20026                    ) {
20027                        Ok(resolved) => resolved,
20028                        Err(error) => {
20029                            errors.push(error);
20030                            continue;
20031                        }
20032                    };
20033                    let repository = match &resolved.repository {
20034                        Some(explicit) => Arc::clone(explicit),
20035                        None => Arc::clone(self.registered_repository(spec)),
20036                    };
20037                    let dep_name =
20038                        crate::parsing::ast::ascii_lowercase_logical_name(spec_ref.name.clone());
20039                    for (repo, imported, imported_resolved) in already_resolved {
20040                        if !Arc::ptr_eq(repo, &repository) {
20041                            continue;
20042                        }
20043                        if crate::parsing::ast::ascii_lowercase_logical_name(imported.name.clone())
20044                            != dep_name
20045                        {
20046                            continue;
20047                        }
20048                        if discovery::same_loaded_spec(imported, spec) {
20049                            continue;
20050                        }
20051                        merge_imported_units(imported_resolved);
20052                    }
20053                }
20054                ParsedDataValue::Import { .. } => {
20055                    let Some(spec_ref) = data_row.value.uses_spec_ref() else {
20056                        continue;
20057                    };
20058                    let (_, imported_spec) = match self.resolve_spec_for_import(
20059                        spec,
20060                        spec_ref,
20061                        &data_row.source_location,
20062                        at,
20063                    ) {
20064                        Ok(pair) => pair,
20065                        Err(error) => {
20066                            errors.push(error);
20067                            continue;
20068                        }
20069                    };
20070                    if discovery::same_loaded_spec(imported_spec, spec) {
20071                        continue;
20072                    }
20073                    let Some(imported_resolved) = already_resolved
20074                        .iter()
20075                        .find(|(_, imported, _)| {
20076                            discovery::same_loaded_spec(imported, imported_spec)
20077                        })
20078                        .map(|(_, _, resolved_spec_types)| resolved_spec_types)
20079                    else {
20080                        continue;
20081                    };
20082                    merge_imported_units(imported_resolved);
20083                }
20084                ParsedDataValue::Definition { .. } => {
20085                    let Some(spec_ref) = discovery::data_instance_list_spec_ref(spec, data_row)
20086                    else {
20087                        continue;
20088                    };
20089                    let ParsedDataValue::Definition {
20090                        base: Some(parent), ..
20091                    } = &data_row.value
20092                    else {
20093                        continue;
20094                    };
20095                    if Self::list_of_spec_name(spec, parent).is_none() {
20096                        continue;
20097                    }
20098                    let (_, imported_spec) = match self.resolve_list_spec_for_import(
20099                        spec,
20100                        &spec_ref,
20101                        &data_row.source_location,
20102                        at,
20103                    ) {
20104                        Ok(pair) => pair,
20105                        Err(error) => {
20106                            errors.push(error);
20107                            continue;
20108                        }
20109                    };
20110                    if discovery::same_loaded_spec(imported_spec, spec) {
20111                        continue;
20112                    }
20113                    let Some(imported_resolved) = already_resolved
20114                        .iter()
20115                        .find(|(_, imported, _)| {
20116                            discovery::same_loaded_spec(imported, imported_spec)
20117                        })
20118                        .map(|(_, _, resolved_spec_types)| resolved_spec_types)
20119                    else {
20120                        continue;
20121                    };
20122                    merge_imported_units(imported_resolved);
20123                }
20124                ParsedDataValue::ImportList { .. } => {
20125                    panic!(
20126                        "BUG: uses list is rejected at parse and must not reach type resolution"
20127                    );
20128                }
20129            }
20130        }
20131
20132        if !errors.is_empty() {
20133            return Err(errors);
20134        }
20135
20136        let mut raw_suggestions = Vec::new();
20137        let mut raw_values = Vec::new();
20138        let mut resolved_types = IndexMap::new();
20139        for type_name in &sorted_type_names {
20140            let (lemma_type, default, fill) = resolved
20141                .remove(type_name)
20142                .expect("BUG: every sorted type name must exist in resolved");
20143            if let Some(raw_suggestion) = default {
20144                raw_suggestions.push((type_name.clone(), raw_suggestion));
20145            }
20146            if let Some(raw_value) = fill {
20147                raw_values.push((type_name.clone(), raw_value));
20148            }
20149            resolved_types.insert(type_name.clone(), lemma_type);
20150        }
20151        if !resolved.is_empty() {
20152            panic!(
20153                "BUG: resolved types remain after draining sorted_type_names: {:?}",
20154                resolved.keys().collect::<Vec<_>>()
20155            );
20156        }
20157
20158        Ok((
20159            ResolvedSpecTypes {
20160                resolved: resolved_types,
20161                declared_suggestions: IndexMap::new(),
20162                declared_values: IndexMap::new(),
20163                raw_suggestions,
20164                raw_values,
20165                source_defaults: IndexMap::new(),
20166                source_value_defaults: IndexMap::new(),
20167                rule_list_types: Vec::new(),
20168                unit_index: UnitIndex::new(),
20169                planning_unit_index: unit_index,
20170                signature_index: crate::computation::arithmetic::SignatureIndex::new(),
20171                deferred_signature_collisions: Vec::new(),
20172            },
20173            list_type_names,
20174        ))
20175    }
20176
20177    fn resolve_spec_for_import(
20178        &self,
20179        spec: &LemmaSpec,
20180        from: &crate::parsing::ast::SpecRef,
20181        import_site: &crate::parsing::source::Source,
20182        at: &EffectiveDate,
20183    ) -> Result<(Arc<LemmaRepository>, &'a LemmaSpec), Error> {
20184        discovery::resolve_spec_ref(
20185            self.context,
20186            from,
20187            self.registered_repository(spec),
20188            spec,
20189            at,
20190            Some(import_site.clone()),
20191        )
20192    }
20193
20194    /// [`Self::resolve_spec_for_import`] for `data x: s list`, where `s` may be `spec` itself.
20195    fn resolve_list_spec_for_import(
20196        &self,
20197        spec: &LemmaSpec,
20198        from: &crate::parsing::ast::SpecRef,
20199        import_site: &crate::parsing::source::Source,
20200        at: &EffectiveDate,
20201    ) -> Result<(Arc<LemmaRepository>, &'a LemmaSpec), Error> {
20202        discovery::resolve_spec_ref_or_self(
20203            self.context,
20204            from,
20205            self.registered_repository(spec),
20206            spec,
20207            at,
20208            Some(import_site.clone()),
20209        )
20210    }
20211
20212    // =========================================================================
20213    // Static helpers (no &self)
20214    // =========================================================================
20215
20216    fn add_measure_units_to_index(
20217        spec: &LemmaSpec,
20218        unit_index: &mut PlanningUnitIndex,
20219        resolved_type: &Arc<LemmaType>,
20220        defined_by: &DataTypeDef,
20221        import_path: &[String],
20222        locals: &HashMap<String, Arc<LemmaType>>,
20223    ) -> Result<(), Error> {
20224        if matches!(defined_by.parent, ParentType::Qualified { .. }) {
20225            unreachable!(
20226                "BUG: Qualified parents must use parent_units_for_qualified_extension + merge_additive_measure_units"
20227            );
20228        }
20229        let measure_family = resolved_type
20230            .measure_family_name()
20231            .expect("BUG: add_measure_units_to_index requires measure type with family");
20232        for unit in Self::extract_units_from_type(&resolved_type.specifications) {
20233            unit_index
20234                .merge_measure_unit(
20235                    unit,
20236                    resolved_type,
20237                    &defined_by.name,
20238                    import_path,
20239                    measure_family,
20240                    locals,
20241                )
20242                .map_err(|conflict| {
20243                    Self::unit_merge_conflict_to_error(conflict, defined_by, spec)
20244                })?;
20245        }
20246        Ok(())
20247    }
20248
20249    /// Parent unit names for a Qualified extension; `None` if parent not resolved yet.
20250    fn parent_units_for_qualified_extension(
20251        resolver: &TypeResolver<'_>,
20252        spec: &LemmaSpec,
20253        defined_by: &DataTypeDef,
20254        already_resolved: &ResolvedTypesMap<'_>,
20255        at: &EffectiveDate,
20256        spec_alias: &str,
20257        inner: &ParentType,
20258    ) -> Result<Option<BTreeSet<String>>, Error> {
20259        let ParentType::Custom {
20260            name: parent_type_name,
20261            ..
20262        } = inner
20263        else {
20264            return Ok(None);
20265        };
20266        let Some(spec_ref) = spec.data.iter().find_map(|row| {
20267            if row.reference.segments.is_empty() && row.reference.name == spec_alias {
20268                row.value.uses_spec_ref()
20269            } else {
20270                None
20271            }
20272        }) else {
20273            return Err(Error::validation_with_context(
20274                format!("'{spec_alias}' is not a `uses` import alias in this spec"),
20275                Some(defined_by.source.clone()),
20276                Some(format!(
20277                    "Add `uses {spec_alias}: <target_spec>`, then write `{spec_alias}.<type>`."
20278                )),
20279                Some(spec),
20280                None,
20281            ));
20282        };
20283        let (_, target_spec) =
20284            resolver.resolve_spec_for_import(spec, spec_ref, &defined_by.source, at)?;
20285        let Some(imported_resolved) = already_resolved
20286            .iter()
20287            .find(|(_, imported, _)| discovery::same_loaded_spec(imported, target_spec))
20288            .map(|(_, _, types)| types)
20289        else {
20290            return Ok(None);
20291        };
20292        let Some(parent_type) = imported_resolved.resolved.get(parent_type_name.as_str()) else {
20293            return Ok(None);
20294        };
20295        Ok(Some(
20296            Self::extract_units_from_type(&parent_type.specifications)
20297                .into_iter()
20298                .collect(),
20299        ))
20300    }
20301
20302    /// Register only units the Qualified extension newly declares (not inherited from parent).
20303    fn merge_additive_measure_units(
20304        spec: &LemmaSpec,
20305        unit_index: &mut PlanningUnitIndex,
20306        resolved_type: &Arc<LemmaType>,
20307        defined_by: &DataTypeDef,
20308        import_path: &[String],
20309        parent_units: &BTreeSet<String>,
20310        locals: &HashMap<String, Arc<LemmaType>>,
20311    ) -> Result<(), Error> {
20312        let measure_family = resolved_type
20313            .measure_family_name()
20314            .expect("BUG: additive measure registration requires family");
20315        for unit in Self::extract_units_from_type(&resolved_type.specifications) {
20316            if parent_units.contains(&unit) {
20317                continue;
20318            }
20319            unit_index
20320                .merge_measure_unit(
20321                    unit,
20322                    resolved_type,
20323                    &defined_by.name,
20324                    import_path,
20325                    measure_family,
20326                    locals,
20327                )
20328                .map_err(|conflict| {
20329                    Self::unit_merge_conflict_to_error(conflict, defined_by, spec)
20330                })?;
20331        }
20332        Ok(())
20333    }
20334
20335    fn add_ratio_units_to_index(
20336        spec: &LemmaSpec,
20337        unit_index: &mut PlanningUnitIndex,
20338        resolved_type: &Arc<LemmaType>,
20339        defined_by: &DataTypeDef,
20340        import_path: &[String],
20341        locals: &HashMap<String, Arc<LemmaType>>,
20342    ) -> Result<(), Error> {
20343        for unit in Self::extract_units_from_type(&resolved_type.specifications) {
20344            unit_index
20345                .merge_ratio_unit(unit, resolved_type, &defined_by.name, import_path, locals)
20346                .map_err(|conflict| {
20347                    Self::unit_merge_conflict_to_error(conflict, defined_by, spec)
20348                })?;
20349        }
20350        Ok(())
20351    }
20352
20353    fn unit_merge_conflict_to_error(
20354        conflict: UnitMergeConflict,
20355        defined_by: &DataTypeDef,
20356        spec: &LemmaSpec,
20357    ) -> Error {
20358        let message = match conflict {
20359            UnitMergeConflict::ConflictingFactors { unit, family } => format!(
20360                "Unit '{unit}' has different values on related measure types in '{family}'. \
20361                 Use the same unit value on each type, or use different unit names."
20362            ),
20363        };
20364        Error::validation_with_context(
20365            message,
20366            Some(defined_by.source.clone()),
20367            None::<String>,
20368            Some(spec),
20369            None,
20370        )
20371    }
20372
20373    fn extract_units_from_type(specs: &TypeSpecification) -> Vec<String> {
20374        match specs {
20375            TypeSpecification::Measure { units, .. } => {
20376                units.iter().map(|unit| unit.name.clone()).collect()
20377            }
20378            TypeSpecification::Ratio { units, .. } => {
20379                units.iter().map(|unit| unit.name.clone()).collect()
20380            }
20381            _ => Vec::new(),
20382        }
20383    }
20384}
20385
20386// ============================================================================
20387// Validation (formerly validation.rs)
20388// ============================================================================
20389
20390fn instance_list_option_errors(
20391    lemma_type: &LemmaType,
20392    raw: &RawSuggestion,
20393    source: &Source,
20394    spec: &LemmaSpec,
20395) -> Vec<Error> {
20396    let owner = match &lemma_type.specifications {
20397        TypeSpecification::List { element, .. } => element.as_ref(),
20398        _ => lemma_type,
20399    };
20400    let mut values = Vec::new();
20401    let mut errors = Vec::new();
20402    if let Err(message) = collect_raw_values(raw, &mut values) {
20403        errors.push(Error::validation_with_context(
20404            message,
20405            Some(source.clone()),
20406            None::<String>,
20407            Some(spec),
20408            None,
20409        ));
20410        return errors;
20411    }
20412    for value in values {
20413        if let Err(message) = owner.reject_disallowed(&value) {
20414            errors.push(Error::validation_with_context(
20415                message,
20416                Some(source.clone()),
20417                None::<String>,
20418                Some(spec),
20419                None,
20420            ));
20421        }
20422    }
20423    errors
20424}
20425
20426fn collect_raw_values(raw: &RawSuggestion, out: &mut Vec<ValueKind>) -> Result<(), String> {
20427    let mut pending = vec![raw];
20428    while let Some(current) = pending.pop() {
20429        match current {
20430            RawSuggestion::Value(value) => out.push(value.clone()),
20431            RawSuggestion::List(rows) => {
20432                for row in rows.iter().rev() {
20433                    pending.push(row);
20434                }
20435            }
20436            RawSuggestion::Measure { .. } | RawSuggestion::UnitBound { .. } => {
20437                return Err(
20438                    "an instance-list option row is a value, not a measure bound still awaiting a unit"
20439                        .to_string(),
20440                );
20441            }
20442        }
20443    }
20444    Ok(())
20445}
20446
20447fn validate_type_options(
20448    lemma_type: &LemmaType,
20449    declared_default: Option<&BoundValueKind>,
20450    default_label: &str,
20451    type_name: &str,
20452    source: &Source,
20453    spec_context: Option<&LemmaSpec>,
20454    unit_index: &UnitIndex,
20455) -> Vec<Error> {
20456    let owner = match &lemma_type.specifications {
20457        TypeSpecification::List { element, .. } if !element.options.is_empty() => element.as_ref(),
20458        _ => lemma_type,
20459    };
20460    let mut errors = Vec::new();
20461    let mut bare = owner.clone();
20462    bare.options = TypeOptions::default();
20463    for option in &owner.options.values {
20464        let literal = LiteralValue {
20465            value: option.value.value.clone(),
20466        };
20467        if let Err(message) =
20468            execution_plan::validate_value_against_type(&bare, &literal, unit_index)
20469        {
20470            errors.push(Error::validation_with_context(
20471                format!(
20472                    "Type '{type_name}' option '{}': {message}",
20473                    owner.display_bound(&option.value)
20474                ),
20475                Some(source.clone()),
20476                None::<String>,
20477                spec_context,
20478                None,
20479            ));
20480        }
20481    }
20482    if let Some(default) = declared_default {
20483        if let Err(message) = execution_plan::validate_value_against_type(
20484            lemma_type,
20485            &default.to_literal(),
20486            unit_index,
20487        ) {
20488            errors.push(Error::validation_with_context(
20489                format!("Type '{type_name}' {default_label} value {message}"),
20490                Some(source.clone()),
20491                None::<String>,
20492                spec_context,
20493                None,
20494            ));
20495        }
20496    }
20497    errors
20498}
20499
20500/// Validate that TypeSpecification constraints are internally consistent.
20501///
20502/// Checks range, decimals, length, unit, and option constraints, and
20503/// validates the declared default (when present) against those constraints.
20504/// The default lives outside the type specification (on the data binding or
20505/// typedef entry); callers thread it in explicitly so this function can verify
20506/// consistency without owning the value. Pass `default_label` as `"suggestion"`
20507/// or `"with"` for error messages.
20508///
20509/// Returns a vector of errors (empty if valid).
20510pub fn validate_type_specifications(
20511    specs: &TypeSpecification,
20512    declared_default: Option<&BoundValueKind>,
20513    default_label: &str,
20514    type_name: &str,
20515    source: &Source,
20516    spec_context: Option<&LemmaSpec>,
20517    unit_index: &UnitIndex,
20518) -> Vec<Error> {
20519    let mut errors = Vec::new();
20520    let declared_default = declared_default.map(|bound| &bound.value);
20521
20522    match specs {
20523        TypeSpecification::Measure {
20524            minimum,
20525            maximum,
20526            units,
20527            ..
20528        } => {
20529            // Validate range consistency
20530            if let (Some(min), Some(max)) = (minimum, maximum) {
20531                match (
20532                    semantics::measure_declared_bound_to_canonical(
20533                        &min.0, &min.1, units, type_name, "minimum",
20534                    ),
20535                    semantics::measure_declared_bound_to_canonical(
20536                        &max.0, &max.1, units, type_name, "maximum",
20537                    ),
20538                ) {
20539                    (Ok(min_canonical), Ok(max_canonical)) => {
20540                        match min_canonical.try_cmp(&max_canonical) {
20541                            Ok(std::cmp::Ordering::Greater) => {
20542                                errors.push(Error::validation_with_context(
20543                                    format!(
20544                                        "Type '{}' has invalid range: minimum {} {} is greater than maximum {} {}",
20545                                        type_name,
20546                                        min.0,
20547                                        min.1,
20548                                        max.0,
20549                                        max.1
20550                                    ),
20551                                    Some(source.clone()),
20552                                    None::<String>,
20553                                    spec_context,
20554                                    None,
20555                                ));
20556                            }
20557                            Ok(std::cmp::Ordering::Less | std::cmp::Ordering::Equal) => {}
20558                            Err(failure) => {
20559                                errors.push(Error::validation_with_context(
20560                                    format!(
20561                                        "Type '{}' has invalid measure bound: range compare failed: {failure}",
20562                                        type_name
20563                                    ),
20564                                    Some(source.clone()),
20565                                    None::<String>,
20566                                    spec_context,
20567                                    None,
20568                                ));
20569                            }
20570                        }
20571                    }
20572                    (Err(message), _) | (_, Err(message)) => {
20573                        errors.push(Error::validation_with_context(
20574                            format!(
20575                                "Type '{}' has invalid measure bound: {}",
20576                                type_name, message
20577                            ),
20578                            Some(source.clone()),
20579                            None::<String>,
20580                            spec_context,
20581                            None,
20582                        ));
20583                    }
20584                }
20585            }
20586
20587            // Measure defaults are validated during suggestion lift against this type's units.
20588
20589            // Measure types must have at least one unit (required for parsing and conversion)
20590            if units.is_empty() {
20591                errors.push(Error::validation_with_context(
20592                    format!(
20593                        "Type '{}' is a measure type but has no units. Measure types must define at least one unit (e.g. -> unit eur: 1).",
20594                        type_name
20595                    ),
20596                    Some(source.clone()),
20597                    None::<String>,
20598                    spec_context,
20599                    None,
20600                ));
20601            }
20602
20603            // Validate units (if present)
20604            if !units.is_empty() {
20605                let mut seen_names: Vec<String> = Vec::new();
20606                for unit in units.iter() {
20607                    // Validate unit name is not empty
20608                    if unit.name.trim().is_empty() {
20609                        errors.push(Error::validation_with_context(
20610                            format!(
20611                                "Type '{}' has a unit with empty name. Unit names cannot be empty.",
20612                                type_name
20613                            ),
20614                            Some(source.clone()),
20615                            None::<String>,
20616                            spec_context,
20617                            None,
20618                        ));
20619                    }
20620
20621                    // Validate unit names are unique within the type
20622                    if seen_names.contains(&unit.name) {
20623                        errors.push(Error::validation_with_context(
20624                            format!("Type '{}' has duplicate unit name '{}'. Unit names must be unique within a type.", type_name, unit.name),
20625                            Some(source.clone()),
20626                            None::<String>,
20627                            spec_context,
20628                            None,
20629                        ));
20630                    } else {
20631                        seen_names.push(unit.name.clone());
20632                    }
20633
20634                    if !unit.is_positive_factor() {
20635                        let factor = unit.require_literal_factor();
20636                        errors.push(Error::validation_with_context(
20637                            format!("Type '{}' has unit '{}' with invalid value {}/{}. Unit values must be positive (conversion factor relative to type base).", type_name, unit.name, factor.numer_to_string(), factor.denom_to_string()),
20638                            Some(source.clone()),
20639                            None::<String>,
20640                            spec_context,
20641                            None,
20642                        ));
20643                    }
20644                }
20645            }
20646        }
20647        TypeSpecification::Number {
20648            minimum,
20649            maximum,
20650            decimals,
20651            ..
20652        } => {
20653            // Validate range consistency
20654            if let (Some(min), Some(max)) = (minimum, maximum) {
20655                match min.try_cmp(max) {
20656                    Ok(std::cmp::Ordering::Greater) => {
20657                        errors.push(Error::validation_with_context(
20658                            format!(
20659                                "Type '{}' has invalid range: minimum {} is greater than maximum {}",
20660                                type_name, min, max
20661                            ),
20662                            Some(source.clone()),
20663                            None::<String>,
20664                            spec_context,
20665                            None,
20666                        ));
20667                    }
20668                    Ok(std::cmp::Ordering::Less | std::cmp::Ordering::Equal) => {}
20669                    Err(failure) => {
20670                        errors.push(Error::validation_with_context(
20671                            format!(
20672                                "Type '{}' has invalid range: bound compare failed: {failure}",
20673                                type_name
20674                            ),
20675                            Some(source.clone()),
20676                            None::<String>,
20677                            spec_context,
20678                            None,
20679                        ));
20680                    }
20681                }
20682            }
20683
20684            // Validate decimals range (0-28 is rust_decimal limit)
20685            if let Some(d) = decimals {
20686                if *d > 28 {
20687                    errors.push(Error::validation_with_context(
20688                        format!(
20689                            "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
20690                            type_name, d
20691                        ),
20692                        Some(source.clone()),
20693                        None::<String>,
20694                        spec_context,
20695                        None,
20696                    ));
20697                }
20698            }
20699
20700            if let Some(ValueKind::Number(def)) = declared_default {
20701                if let Some(min) = minimum {
20702                    match def.try_cmp(min) {
20703                        Ok(std::cmp::Ordering::Less) => {
20704                            errors.push(Error::validation_with_context(
20705                                format!(
20706                                    "Type '{}' {default_label} value {} is less than minimum {}",
20707                                    type_name, def, min
20708                                ),
20709                                Some(source.clone()),
20710                                None::<String>,
20711                                spec_context,
20712                                None,
20713                            ));
20714                        }
20715                        Ok(std::cmp::Ordering::Equal | std::cmp::Ordering::Greater) => {}
20716                        Err(failure) => {
20717                            errors.push(Error::validation_with_context(
20718                                format!(
20719                                    "Type '{}' {default_label} bound compare failed: {failure}",
20720                                    type_name
20721                                ),
20722                                Some(source.clone()),
20723                                None::<String>,
20724                                spec_context,
20725                                None,
20726                            ));
20727                        }
20728                    }
20729                }
20730                if let Some(max) = maximum {
20731                    match def.try_cmp(max) {
20732                        Ok(std::cmp::Ordering::Greater) => {
20733                            errors.push(Error::validation_with_context(
20734                                format!(
20735                                    "Type '{}' {default_label} value {} is greater than maximum {}",
20736                                    type_name, def, max
20737                                ),
20738                                Some(source.clone()),
20739                                None::<String>,
20740                                spec_context,
20741                                None,
20742                            ));
20743                        }
20744                        Ok(std::cmp::Ordering::Less | std::cmp::Ordering::Equal) => {}
20745                        Err(failure) => {
20746                            errors.push(Error::validation_with_context(
20747                                format!(
20748                                    "Type '{}' {default_label} bound compare failed: {failure}",
20749                                    type_name
20750                                ),
20751                                Some(source.clone()),
20752                                None::<String>,
20753                                spec_context,
20754                                None,
20755                            ));
20756                        }
20757                    }
20758                }
20759            }
20760            // Note: Number types are dimensionless and cannot have units (validated in apply_constraint)
20761        }
20762
20763        TypeSpecification::Ratio {
20764            minimum,
20765            maximum,
20766            units,
20767            ..
20768        } => {
20769            // Validate range consistency
20770            if let (Some(min), Some(max)) = (minimum, maximum) {
20771                match min.try_cmp(max) {
20772                    Ok(std::cmp::Ordering::Greater) => {
20773                        errors.push(Error::validation_with_context(
20774                            format!(
20775                                "Type '{}' has invalid range: minimum {} is greater than maximum {}",
20776                                type_name, min, max
20777                            ),
20778                            Some(source.clone()),
20779                            None::<String>,
20780                            spec_context,
20781                            None,
20782                        ));
20783                    }
20784                    Ok(std::cmp::Ordering::Less | std::cmp::Ordering::Equal) => {}
20785                    Err(failure) => {
20786                        errors.push(Error::validation_with_context(
20787                            format!(
20788                                "Type '{}' has invalid range: bound compare failed: {failure}",
20789                                type_name
20790                            ),
20791                            Some(source.clone()),
20792                            None::<String>,
20793                            spec_context,
20794                            None,
20795                        ));
20796                    }
20797                }
20798            }
20799
20800            if let Some(ValueKind::Ratio(def)) = declared_default {
20801                if let Some(min) = minimum {
20802                    match def.try_cmp(min) {
20803                        Ok(std::cmp::Ordering::Less) => {
20804                            errors.push(Error::validation_with_context(
20805                                format!(
20806                                    "Type '{}' {default_label} value {} is less than minimum {}",
20807                                    type_name, def, min
20808                                ),
20809                                Some(source.clone()),
20810                                None::<String>,
20811                                spec_context,
20812                                None,
20813                            ));
20814                        }
20815                        Ok(std::cmp::Ordering::Equal | std::cmp::Ordering::Greater) => {}
20816                        Err(failure) => {
20817                            errors.push(Error::validation_with_context(
20818                                format!(
20819                                    "Type '{}' {default_label} bound compare failed: {failure}",
20820                                    type_name
20821                                ),
20822                                Some(source.clone()),
20823                                None::<String>,
20824                                spec_context,
20825                                None,
20826                            ));
20827                        }
20828                    }
20829                }
20830                if let Some(max) = maximum {
20831                    match def.try_cmp(max) {
20832                        Ok(std::cmp::Ordering::Greater) => {
20833                            errors.push(Error::validation_with_context(
20834                                format!(
20835                                    "Type '{}' {default_label} value {} is greater than maximum {}",
20836                                    type_name, def, max
20837                                ),
20838                                Some(source.clone()),
20839                                None::<String>,
20840                                spec_context,
20841                                None,
20842                            ));
20843                        }
20844                        Ok(std::cmp::Ordering::Less | std::cmp::Ordering::Equal) => {}
20845                        Err(failure) => {
20846                            errors.push(Error::validation_with_context(
20847                                format!(
20848                                    "Type '{}' {default_label} bound compare failed: {failure}",
20849                                    type_name
20850                                ),
20851                                Some(source.clone()),
20852                                None::<String>,
20853                                spec_context,
20854                                None,
20855                            ));
20856                        }
20857                    }
20858                }
20859            }
20860
20861            // Validate units (if present)
20862            // Types can have zero units (e.g., type ratio: number -> ratio) - this is valid
20863            // Only validate if units are defined
20864            if !units.is_empty() {
20865                let mut seen_names: Vec<String> = Vec::new();
20866                for unit in units.iter() {
20867                    // Validate unit name is not empty
20868                    if unit.name.trim().is_empty() {
20869                        errors.push(Error::validation_with_context(
20870                            format!(
20871                                "Type '{}' has a unit with empty name. Unit names cannot be empty.",
20872                                type_name
20873                            ),
20874                            Some(source.clone()),
20875                            None::<String>,
20876                            spec_context,
20877                            None,
20878                        ));
20879                    }
20880
20881                    // Validate unit names are unique within the type
20882                    if seen_names.contains(&unit.name) {
20883                        errors.push(Error::validation_with_context(
20884                            format!("Type '{}' has duplicate unit name '{}'. Unit names must be unique within a type.", type_name, unit.name),
20885                            Some(source.clone()),
20886                            None::<String>,
20887                            spec_context,
20888                            None,
20889                        ));
20890                    } else {
20891                        seen_names.push(unit.name.clone());
20892                    }
20893
20894                    if !unit.value.numer_is_positive() {
20895                        errors.push(Error::validation_with_context(
20896                            format!("Type '{}' has unit '{}' with invalid value {}/{}. Unit values must be positive (conversion factor relative to type base).", type_name, unit.name, unit.value.numer_to_string(), unit.value.denom_to_string()),
20897                            Some(source.clone()),
20898                            None::<String>,
20899                            spec_context,
20900                            None,
20901                        ));
20902                    }
20903                }
20904            }
20905        }
20906
20907        TypeSpecification::Text { length, .. } => {
20908            if let Some(ValueKind::Text(def)) = declared_default {
20909                let def_len = def.len();
20910
20911                if let Some(len) = length {
20912                    if def_len != *len {
20913                        errors.push(Error::validation_with_context(
20914                            format!("Type '{}' {default_label} value length {} does not match required length {}", type_name, def_len, len),
20915                            Some(source.clone()),
20916                            None::<String>,
20917                            spec_context,
20918                            None,
20919                        ));
20920                    }
20921                }
20922            }
20923        }
20924
20925        TypeSpecification::Date {
20926            minimum, maximum, ..
20927        } => {
20928            // Validate range consistency
20929            if let (Some(min), Some(max)) = (minimum, maximum) {
20930                let min_sem = semantics::date_time_to_semantic(min);
20931                let max_sem = semantics::date_time_to_semantic(max);
20932                if semantics::compare_semantic_dates(&min_sem, &max_sem) == Ordering::Greater {
20933                    errors.push(Error::validation_with_context(
20934                        format!(
20935                            "Type '{}' has invalid date range: minimum {} is after maximum {}",
20936                            type_name, min, max
20937                        ),
20938                        Some(source.clone()),
20939                        None::<String>,
20940                        spec_context,
20941                        None,
20942                    ));
20943                }
20944            }
20945
20946            if let Some(ValueKind::Date(def)) = declared_default {
20947                if let Some(min) = minimum {
20948                    let min_sem = semantics::date_time_to_semantic(min);
20949                    if semantics::compare_semantic_dates(def, &min_sem) == Ordering::Less {
20950                        errors.push(Error::validation_with_context(
20951                            format!(
20952                                "Type '{}' default date {} is before minimum {}",
20953                                type_name, def, min
20954                            ),
20955                            Some(source.clone()),
20956                            None::<String>,
20957                            spec_context,
20958                            None,
20959                        ));
20960                    }
20961                }
20962                if let Some(max) = maximum {
20963                    let max_sem = semantics::date_time_to_semantic(max);
20964                    if semantics::compare_semantic_dates(def, &max_sem) == Ordering::Greater {
20965                        errors.push(Error::validation_with_context(
20966                            format!(
20967                                "Type '{}' default date {} is after maximum {}",
20968                                type_name, def, max
20969                            ),
20970                            Some(source.clone()),
20971                            None::<String>,
20972                            spec_context,
20973                            None,
20974                        ));
20975                    }
20976                }
20977            }
20978        }
20979
20980        TypeSpecification::Time {
20981            minimum, maximum, ..
20982        } => {
20983            // Validate range consistency
20984            if let (Some(min), Some(max)) = (minimum, maximum) {
20985                let min_sem = semantics::time_to_semantic(min);
20986                let max_sem = semantics::time_to_semantic(max);
20987                if semantics::compare_semantic_times(&min_sem, &max_sem) == Ordering::Greater {
20988                    errors.push(Error::validation_with_context(
20989                        format!(
20990                            "Type '{}' has invalid time range: minimum {} is after maximum {}",
20991                            type_name, min, max
20992                        ),
20993                        Some(source.clone()),
20994                        None::<String>,
20995                        spec_context,
20996                        None,
20997                    ));
20998                }
20999            }
21000
21001            if let Some(ValueKind::Time(def)) = declared_default {
21002                if let Some(min) = minimum {
21003                    let min_sem = semantics::time_to_semantic(min);
21004                    if semantics::compare_semantic_times(def, &min_sem) == Ordering::Less {
21005                        errors.push(Error::validation_with_context(
21006                            format!(
21007                                "Type '{}' default time {} is before minimum {}",
21008                                type_name, def, min
21009                            ),
21010                            Some(source.clone()),
21011                            None::<String>,
21012                            spec_context,
21013                            None,
21014                        ));
21015                    }
21016                }
21017                if let Some(max) = maximum {
21018                    let max_sem = semantics::time_to_semantic(max);
21019                    if semantics::compare_semantic_times(def, &max_sem) == Ordering::Greater {
21020                        errors.push(Error::validation_with_context(
21021                            format!(
21022                                "Type '{}' default time {} is after maximum {}",
21023                                type_name, def, max
21024                            ),
21025                            Some(source.clone()),
21026                            None::<String>,
21027                            spec_context,
21028                            None,
21029                        ));
21030                    }
21031                }
21032            }
21033        }
21034
21035        TypeSpecification::NumberRange { .. }
21036        | TypeSpecification::DateRange { .. }
21037        | TypeSpecification::TimeRange { .. }
21038        | TypeSpecification::MeasureRange { .. }
21039        | TypeSpecification::RatioRange { .. } => {
21040            if let Err(message) = semantics::check_range_bound_consistency(specs, unit_index) {
21041                errors.push(Error::validation_with_context(
21042                    format!("Type '{}' has {message}", type_name),
21043                    Some(source.clone()),
21044                    None::<String>,
21045                    spec_context,
21046                    None,
21047                ));
21048            }
21049        }
21050        TypeSpecification::Boolean { .. } => {
21051            // No constraint validation needed for these types
21052        }
21053        TypeSpecification::Veto { .. } => {
21054            // Veto is not a user-declarable type, so validation should not be called on it
21055            // But if it is, there's nothing to validate
21056        }
21057        TypeSpecification::Undetermined => unreachable!(
21058            "BUG: validate_type_specification_constraints called with Undetermined sentinel type; this type exists only during type inference"
21059        ),
21060        TypeSpecification::Spec { .. } | TypeSpecification::List { .. } => {}
21061    }
21062
21063    errors
21064}