Skip to main content

lemma/planning/
execution_plan.rs

1//! Execution plan for evaluated specs
2//!
3//! Provides a complete self-contained execution plan ready for the evaluator.
4//! The plan holds an expanded shared normal-form graph in a dense table
5//! ([`ExecutionPlan::normal_forms`]) addressed by [`NormalFormId`], plus all data —
6//! no spec structure needed during evaluation.
7//!
8//! Imports are flattened at planning: every data and rule of an imported spec is an
9//! entry of the consumer plan under its `uses` hop, and a row written in a data list is
10//! an instance under the list path (`staff.1`). Evaluation has one context.
11//!
12//! Reliability model:
13//! - [`Show`] is the IO contract surface for consumers (data and rule outputs).
14//!   IO compatibility is the consumer-facing guarantee.
15
16use crate::computation::arithmetic::SignatureIndex;
17use crate::computation::rational::RationalInteger;
18use crate::computation::units::{conversion_target_declares_unit, owning_type_declares_unit_name};
19use crate::computation::{OperationResult, VetoType};
20use crate::engine::write_effective_window;
21use crate::limits::ResourceLimits;
22use crate::literals::Value;
23use crate::parsing::ast::{canonical_logical_name, DateTimeValue, EffectiveDate, LemmaSpec};
24use crate::parsing::source::{Source, SourceType};
25use crate::planning::graph::Graph;
26use crate::planning::graph::ResolvedSpecTypes;
27use crate::planning::instance_list::InstanceListSource;
28use crate::planning::normalize::{
29    build_normalized_rule, data_path_result_type, DispatchRegion, NormalForm, NormalFormId,
30    NormalFormInterner, NormalizeContext, NormalizedRule,
31};
32use crate::planning::semantics::{
33    lemma_type_with_display_binding, option_detail_line, primitive_number_arc, primitive_ratio_arc,
34    value_kind_matches_spec, BoundValueKind, ComparisonComputation, DataDefinition, DataPath,
35    LemmaType, LiteralValue, PathSegment, ReferenceEnd, ReferenceTarget, RowBinding, RowEnd,
36    RowInstance, RulePath, SpecIdentity, TypeSpecification, TypedLiteral, ValueKind,
37};
38use crate::planning::show_expression::{project_depends_on_rules, show_branches_from, ShowScope};
39use crate::planning::spec_set::LemmaSpecSet;
40use crate::planning::spec_value::{RowFieldRead, SpecCandidates};
41use crate::planning::typing::MeasureScope;
42use crate::planning::unit_family::FamilyUnitCatalog;
43use crate::planning::unit_index::UnitIndex;
44use crate::result_value::{
45    rule_result_value_failure_message, type_scoped_result_value_from_literal, RuleResultValue,
46};
47use crate::Error;
48use indexmap::IndexMap;
49use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
50use std::sync::Arc;
51
52pub use crate::planning::show_expression::{
53    ShowBranch, ShowConversionTarget, ShowExpression, ShowListCommand, ShowRule, SortDirection,
54};
55
56/// Rule positions that need one [`ExecutionPlan::data`] slot.
57///
58/// For an instance list, [`Self::fields`] is parallel to the row template's row fields
59/// ([`ExecutionPlan::row_field_count`]): which rules need each row field of
60/// evaluation-data rows through this list.
61/// Otherwise `fields` is empty.
62#[derive(Debug, Clone, Default)]
63pub(crate) struct NeededByRules {
64    pub rules: Vec<u32>,
65    pub fields: Vec<Vec<u32>>,
66}
67
68/// A complete execution plan ready for the evaluator
69///
70/// Contains drift-free normal-form equations in a table, named rule roots, and all data.
71/// Self-contained structure - no spec lookups required during evaluation.
72///
73/// A plan is immutable once stored. A committed plan is reused only when its own spec's
74/// bytes are still the spec set's `Arc` (kept while the version's bytes are unchanged),
75/// its version's boundaries (show-cache fields such as `effective_to`) and the
76/// slices its edges resolve to are unchanged, and every one of those slices is reused.
77#[derive(Debug, Clone)]
78pub struct ExecutionPlan {
79    /// Main spec name
80    pub spec_name: String,
81
82    /// Optional commentary from the `"""..."""` block in the spec source.
83    pub commentary: Option<String>,
84
85    /// This spec's own data slots in definition order, every path local. A `uses`
86    /// import is one [`DataDefinition::Import`] slot holding its row over the
87    /// template plan; spec-written list rows live in their [`RowStore`].
88    pub data: IndexMap<DataPath, DataDefinition>,
89
90    /// Dense table of interned NormalForm cells — the expanded shared graph.
91    /// Index = [`NormalFormId`].
92    pub normal_forms: Vec<NormalForm>,
93
94    /// This spec's own rules → root of that rule in [`Self::normal_forms`].
95    /// Insertion order is planning topo order (deps before consumers).
96    pub rules: IndexMap<RulePath, ExecutableRule>,
97
98    /// Data→data [`DataDefinition::Reference`] paths in dependency order for
99    /// evaluation prepop (chained reference → reference → data). Empty when the
100    /// plan has none. Rule-target references are not included.
101    pub data_reference_order: Vec<DataPath>,
102
103    /// Spec metadata, in declaration order.
104    pub meta: IndexMap<String, Value>,
105
106    /// Main-spec types from planning. [`ResolvedSpecTypes::unit_index`] is expression-scope
107    /// units (local types plus direct `uses` imports). Rule-result unit maps use
108    /// [`Self::family_units`]; Show data inputs stay declared-only.
109    pub resolved_types: ResolvedSpecTypes,
110
111    /// Precomputed measure/ratio family unit expansion for rule results and show rule schemas.
112    pub(crate) family_units: Arc<FamilyUnitCatalog>,
113
114    /// Reverse index: canonical-form unit signature `Vec<(unit_name, exponent)>` →
115    /// (unit_name, owning type). Built from expression-scope units during planning so
116    /// cross-type Multiply/Divide arithmetic can deterministically resolve a combined
117    /// signature back to a single named unit. Ambiguous signatures (the same key matched
118    /// by units in two distinct types) are rejected at planning time.
119    pub signature_index: SignatureIndex,
120
121    pub effective: EffectiveDate,
122
123    /// Declared temporal window `[effective_from, effective_to)` of the LemmaSpec
124    /// version this plan was built from. Filled by [`attach_show_cache`].
125    pub effective_from: Option<DateTimeValue>,
126    pub effective_to: Option<DateTimeValue>,
127
128    /// Source start line of the LemmaSpec version this plan was built from.
129    pub start_line: usize,
130
131    /// Source type of the LemmaSpec version this plan was built from.
132    pub source_type: Option<SourceType>,
133
134    /// Per [`Self::data`] position: [`Self::rules`] positions that need that slot
135    /// (through rule references). Non-promptable slots have empty `rules`.
136    /// Instance-list slots also carry `fields` parallel to the row template's `data`.
137    /// `len() == data.len()`. Built in [`build_execution_plan`].
138    pub(crate) needed_by_rules: Vec<NeededByRules>,
139
140    /// Prefill/suggestion [`BoundValueKind`]s for show, keyed by data path.
141    /// Built once at plan time; [`RuleResultValue`] is expanded at show.
142    pub(crate) data_display: IndexMap<DataPath, ShowDataCache>,
143
144    /// Public Show rule catalog: this spec's own rules only (empty path). Built in
145    /// [`build_execution_plan`]. Reachable import rules stay on Graph `show_edges` /
146    /// `show_imports` for planning and nested-data attribution.
147    pub(crate) show_rules: IndexMap<RulePath, ShowRule>,
148
149    /// Positions in [`Self::show_rules`] of the Show rules that need each data path, in
150    /// input-key order: every reader of data of `uses` imports by composed path (`i.x`),
151    /// and the readers of data of this plan that [`Self::needed_by_rules`] does not list
152    /// (local rules that need it only through an import).
153    pub(crate) show_needed_by: IndexMap<DataPath, Vec<u32>>,
154
155    /// Template identity of every import data path in [`Self::show_needed_by`].
156    pub(crate) show_import_data: IndexMap<DataPath, SpecIdentity>,
157
158    /// Every [`DataDefinition::Reference`] path → where its chain ends.
159    /// Copied from [`Graph::reference_ends`] in [`build_execution_plan`].
160    /// Missing key after planning is a bug.
161    pub(crate) reference_ends: IndexMap<DataPath, ReferenceEnd>,
162
163    /// Precomputed `input_key` → `DataPath` index for [`EvaluateData::resolve`].
164    /// One entry per [`Self::data`] slot. Keyed lookups index this map; they do not
165    /// walk the data tree.
166    pub input_key_index: IndexMap<String, DataPath>,
167
168    /// Precomputed `input_key` → `RulePath` for [`Self::show_rules`].
169    /// Built once so [`Self::get_rule`] is O(1) without allocating per candidate.
170    pub(crate) show_rule_index: IndexMap<String, RulePath>,
171
172    /// Every [`Self::data`] path → its [`LeafKind::DataPath`] cell in
173    /// [`Self::normal_forms`]. Built once so evaluation writes bindings into
174    /// the value table by [`NormalFormId`] instead of a parallel path map.
175    pub(crate) data_leaf: IndexMap<DataPath, NormalFormId>,
176
177    /// Every reference target through `uses` hops (`i.x`, `i.j.r`) that a binding
178    /// resolving in this plan's context names → its member cell: the read of the
179    /// target on the import row this plan holds.
180    pub(crate) member_targets: IndexMap<ReferenceTarget, NormalFormId>,
181
182    /// Template plans of instance-list row specs, by row spec. Rows supplied by
183    /// evaluation data (and option rows) are checked and evaluated against them.
184    pub(crate) row_templates: BTreeMap<SpecIdentity, Arc<ExecutionPlan>>,
185
186    /// This plan's own spec. Rows of its self-lists read this plan as their template.
187    pub(crate) identity: SpecIdentity,
188
189    /// The `Arc` of this plan's own spec version, shared with its [`LemmaSpecSet`].
190    pub(crate) own_spec: Arc<LemmaSpec>,
191
192    /// Slices this plan's dependency edges resolved to, in edge order, self-edges left
193    /// out.
194    pub(crate) edges: Vec<SpecIdentity>,
195
196    /// Instances each rule can yield (empty when not spec-valued). Copied from
197    /// [`Graph::rule_candidates`].
198    pub(crate) rule_candidates: BTreeMap<RulePath, SpecCandidates>,
199
200    /// Rules read on evaluation-data rows of this plan's self-lists, in [`Self::rules`]
201    /// order. A row tree fills them bottom-up, so a parent reads its children's values
202    /// without evaluating down the tree.
203    pub(crate) self_row_rules: Vec<RulePath>,
204
205    /// Source rows of every [`DataDefinition::InstanceList`] path.
206    pub(crate) instance_lists: IndexMap<DataPath, InstanceListSource>,
207
208    /// Per [`Self::normal_forms`] cell: bit `L` is set when the cell reads the current
209    /// list row at level `L` that encloses it (`RowField` or `RowValue`). Such cells
210    /// differ per row and are never cached in the value table.
211    pub(crate) row_mask: Vec<u32>,
212
213    /// Precomputed initial value table (length = `normal_forms.len()`): plan defaults
214    /// and pre-evaluated data-target reference chains in their data-leaf slots, `None` elsewhere.
215    pub(crate) initial_values: Vec<Option<OperationResult>>,
216
217    /// Data-leaf cell ids that must run a walk step before `ready` returns their
218    /// stored value: an options check, or a stored `UnscaledMeasure` to settle.
219    pub(crate) walk_settled_leaves: Vec<NormalFormId>,
220
221    /// Per [`Self::normal_forms`] cell: the [`RuleIndex`] of the rule a `rule_ref`
222    /// cell reads, `None` for every other cell.
223    pub(crate) rule_ref_indexes: Vec<Option<RuleIndex>>,
224
225    /// Per [`Self::data`] slot: its [`Self::data_leaf`] cell. Row bindings travel by
226    /// slot and land in the value table through this.
227    pub(crate) data_slot_leaves: Vec<NormalFormId>,
228
229    /// Scale targets of dynamic units on this plan's data-slot types (this plan's
230    /// coordinates). Row scopes force these before a leaf body.
231    pub(crate) data_slot_scale_targets: Vec<ReferenceTarget>,
232}
233
234/// Plan-time prefill/suggestion for one data path (show cache).
235///
236/// Stores domain [`BoundValueKind`]. [`RuleResultValue`] is built at
237/// show time (JSON / API boundary).
238#[derive(Debug, Clone)]
239pub(crate) struct ShowDataCache {
240    pub value: Option<BoundValueKind>,
241    pub suggestion: Option<BoundValueKind>,
242}
243
244/// A named rule's root in the normal-form table, plus declaration metadata.
245#[derive(Debug, Clone)]
246pub struct ExecutableRule {
247    /// Unique identifier for this rule
248    pub path: RulePath,
249
250    /// Root of this rule in the shared [`ExecutionPlan::normal_forms`] graph.
251    pub normal_form: NormalFormId,
252
253    /// Source location for error messages (always present for rules from parsed specs)
254    pub source: Source,
255
256    /// Type of the rule as written: the lowered source expression typed at
257    /// planning, equal to validation's inferred type. The root cell at
258    /// `normal_form` has the same value type but may carry a different name
259    /// after rewrites (`x + 0` → `x`), so the API type reads this field.
260    pub rule_type: Arc<LemmaType>,
261
262    /// Scale targets of every dynamic unit this rule's nodes hold or write, in this
263    /// plan's coordinates. Evaluated before the rule body; topological order puts
264    /// local scale rules first.
265    pub scale_dependencies: Vec<ReferenceTarget>,
266}
267
268impl ExecutableRule {
269    pub fn name(&self) -> &str {
270        &self.path.rule
271    }
272}
273
274/// Position of a rule in [`ExecutionPlan::rules`]; indexes the evaluation
275/// context's `rule_values` table. Plain `Copy` so the evaluation walk can
276/// unwind to a rule without cloning its [`RulePath`].
277#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
278pub(crate) struct RuleIndex(usize);
279
280impl RuleIndex {
281    pub(crate) fn index(self) -> usize {
282        self.0
283    }
284}
285
286/// Select the plan whose half-open `[effective, next.effective)` covers `instant`
287/// (greatest key `<= instant` in the map).
288pub(crate) fn plan_at<'a>(
289    plans: &'a BTreeMap<EffectiveDate, std::sync::Arc<ExecutionPlan>>,
290    instant: &EffectiveDate,
291) -> Option<&'a ExecutionPlan> {
292    plans
293        .range(..=instant.clone())
294        .next_back()
295        .map(|(_, plan)| plan.as_ref())
296}
297
298fn assert_list_elements_determined(root: &LemmaType) {
299    let mut stack = vec![root];
300    while let Some(lemma_type) = stack.pop() {
301        if let TypeSpecification::List { element, .. } = &lemma_type.specifications {
302            assert!(
303                !element.is_undetermined(),
304                "BUG: validated List type '{}' has an Undetermined element type",
305                lemma_type.name()
306            );
307            stack.push(element);
308        }
309    }
310}
311
312/// Builds an execution plan from a Graph for one temporal slice.
313///
314/// `interner` dedups cons cells while the plan is built. The plan's dense `normal_forms`
315/// table is extracted from it with [`NormalFormInterner::extract_reachable`], so the
316/// result does not depend on what else the interner holds.
317pub(crate) fn build_execution_plan(
318    graph: &Graph<'_>,
319    resolved_types: ResolvedSpecTypes,
320    effective: &EffectiveDate,
321    limits: &ResourceLimits,
322    interner: &mut NormalFormInterner,
323    own_spec: Arc<LemmaSpec>,
324    edges: Vec<SpecIdentity>,
325) -> Result<ExecutionPlan, Vec<Error>> {
326    let rule_order = &graph.rule_order;
327
328    let main_spec = graph.main_spec;
329    let data = graph.build_data(&resolved_types.resolved)?;
330
331    // Planning gate: every data-target reference and plain data declaration
332    // must carry a fully resolved type. Rule-target references are exempt:
333    // they deliberately ship `Undetermined` so runtime veto propagation
334    // surfaces the target rule's veto reason directly. A residual
335    // `Undetermined` anywhere else would violate the invariant evaluation
336    // and show consumers rely on — report it instead of shipping the plan.
337    let undetermined_errors: Vec<Error> = data
338        .iter()
339        .filter_map(|(path, definition)| {
340            let (resolved_type, source) = match definition {
341                DataDefinition::TypeDeclaration {
342                    resolved_type,
343                    source,
344                    ..
345                } => (resolved_type, source),
346                DataDefinition::Reference {
347                    target: ReferenceTarget::Data(_),
348                    resolved_type,
349                    source,
350                    ..
351                } => (resolved_type, source),
352                DataDefinition::Reference {
353                    target: ReferenceTarget::Rule(_),
354                    ..
355                }
356                | DataDefinition::Value { .. }
357                | DataDefinition::InstanceList { .. }
358                | DataDefinition::Import { .. } => return None,
359            };
360            if resolved_type.is_undetermined() {
361                Some(Error::validation(
362                    format!("could not determine the type of '{path}'"),
363                    Some(source.clone()),
364                    None::<String>,
365                ))
366            } else {
367                None
368            }
369        })
370        .collect();
371    if !undetermined_errors.is_empty() {
372        return Err(undetermined_errors);
373    }
374
375    for lemma_type in resolved_types.resolved.values() {
376        assert_list_elements_determined(lemma_type);
377    }
378
379    let signature_index = resolved_types.signature_index.clone();
380
381    let family_units = Arc::new(FamilyUnitCatalog::build(&resolved_types.unit_index));
382
383    let rule_types: HashMap<RulePath, Arc<LemmaType>> = graph
384        .rules
385        .iter()
386        .map(|(path, node)| (path.clone(), Arc::clone(&node.rule_type)))
387        .collect();
388    let mut rules: IndexMap<RulePath, ExecutableRule> = IndexMap::new();
389    let mut completed_rules: HashMap<RulePath, NormalFormId> = HashMap::new();
390    let mut local_show_rules: HashMap<RulePath, ShowRule> = HashMap::new();
391
392    let show_imports = &graph.show_imports;
393    let show_edges = &graph.show_edges;
394    let show_order = &graph.show_order;
395    let show_keys: HashSet<RulePath> = show_edges.keys().cloned().collect();
396    let catalog_keys: HashSet<RulePath> = rule_order.iter().cloned().collect();
397
398    for rule_path in rule_order {
399        let rule_node = graph.rules.get(rule_path).expect(
400            "bug: rule from topological sort not in graph - validation should have caught this",
401        );
402
403        let measure_scope = MeasureScope {
404            unit_index: &resolved_types.unit_index,
405            signature_index: &resolved_types.signature_index,
406            named_types: &resolved_types.resolved,
407        };
408        let normalize_ctx = NormalizeContext {
409            data: &data,
410            row_templates: &graph.instance_lists.templates,
411            self_identity: &graph.instance_lists.self_identity,
412            rule_order,
413            measure_scope,
414            max_normalized_expression_nodes: limits.max_normalized_expression_nodes,
415        };
416        let normalized = build_normalized_rule(
417            &normalize_ctx,
418            &completed_rules,
419            &rule_types,
420            &rule_node.expression,
421            Some(rule_node.source.clone()),
422            interner,
423        )
424        .map_err(|error| vec![error])?;
425        let NormalizedRule { body, source_type } = normalized;
426        assert_eq!(
427            &source_type, &rule_node.rule_type,
428            "BUG: rule '{}' typed differently by planning and validation",
429            rule_path.rule
430        );
431        assert!(
432            interner
433                .result_type(body)
434                .same_value_type(&rule_node.rule_type),
435            "BUG: rewrites changed the value type of rule '{}': {:?} from {:?}",
436            rule_path.rule,
437            interner.result_type(body),
438            rule_node.rule_type
439        );
440        completed_rules.insert(rule_path.clone(), body);
441
442        local_show_rules.insert(
443            rule_path.clone(),
444            ShowRule {
445                lemma_type: family_units.rule_type_for_show(rule_node.rule_type.as_ref()),
446                path: rule_path.segments.clone(),
447                branches: show_branches_from(
448                    &rule_node.expression,
449                    &rule_node.branches,
450                    &ShowScope {
451                        show_keys: &show_keys,
452                        data: &data,
453                        templates: &graph.instance_lists.templates,
454                    },
455                ),
456                depends_on_rules: project_depends_on_rules(&show_edges[rule_path], &catalog_keys),
457            },
458        );
459
460        rules.insert(
461            rule_path.clone(),
462            ExecutableRule {
463                path: rule_path.clone(),
464                normal_form: body,
465                source: rule_node.source.clone(),
466                rule_type: source_type,
467                scale_dependencies: Vec::new(),
468            },
469        );
470    }
471
472    for (rule_path, rule) in rules.iter_mut() {
473        rule.scale_dependencies = graph
474            .rules
475            .get(rule_path)
476            .expect("BUG: executable rule missing from its graph")
477            .scale_targets
478            .clone();
479    }
480    let mut member_target_paths: Vec<&ReferenceTarget> = Vec::new();
481    crate::planning::graph::for_each_owned_reference(
482        &data,
483        &graph.instance_lists,
484        |_, _, target| {
485            if !reference_target_segments(target).is_empty()
486                && !member_target_paths.contains(&target)
487            {
488                member_target_paths.push(target);
489            }
490        },
491    );
492    let mut owned_scale_member_targets: Vec<ReferenceTarget> = Vec::new();
493    let mut push_uses_scale_member = |target: &ReferenceTarget| {
494        if reference_target_segments(target).is_empty() {
495            return;
496        }
497        if !first_hop_is_uses_import(&data, target) {
498            return;
499        }
500        if member_target_paths.contains(&target) || owned_scale_member_targets.contains(target) {
501            return;
502        }
503        owned_scale_member_targets.push(target.clone());
504    };
505    for rule in rules.values() {
506        for target in &rule.scale_dependencies {
507            push_uses_scale_member(target);
508        }
509    }
510    for definition in data.values() {
511        let Some(lemma_type) = definition.schema_type() else {
512            continue;
513        };
514        for target in lemma_type.dynamic_scale_targets() {
515            push_uses_scale_member(&target);
516        }
517    }
518    for target in &owned_scale_member_targets {
519        member_target_paths.push(target);
520    }
521    let mut member_target_roots: Vec<NormalFormId> = Vec::new();
522    for target in &member_target_paths {
523        let normalize_ctx = NormalizeContext {
524            data: &data,
525            row_templates: &graph.instance_lists.templates,
526            self_identity: &graph.instance_lists.self_identity,
527            rule_order,
528            measure_scope: MeasureScope {
529                unit_index: &resolved_types.unit_index,
530                signature_index: &resolved_types.signature_index,
531                named_types: &resolved_types.resolved,
532            },
533            max_normalized_expression_nodes: limits.max_normalized_expression_nodes,
534        };
535        let normalized = build_normalized_rule(
536            &normalize_ctx,
537            &completed_rules,
538            &rule_types,
539            &member_target_expression(target),
540            None,
541            interner,
542        )
543        .map_err(|error| vec![error])?;
544        member_target_roots.push(normalized.body);
545    }
546
547    let root_ids: Vec<NormalFormId> = rules
548        .values()
549        .map(|rule| rule.normal_form)
550        .chain(member_target_roots)
551        .collect();
552    let (normal_forms, mut remapped_roots) = interner.extract_reachable(&root_ids);
553    let remapped_members = remapped_roots.split_off(rules.len());
554    for (rule, remapped) in rules.values_mut().zip(remapped_roots) {
555        rule.normal_form = remapped;
556    }
557    let member_targets: IndexMap<ReferenceTarget, NormalFormId> = member_target_paths
558        .into_iter()
559        .cloned()
560        .zip(remapped_members)
561        .collect();
562
563    let mut show_rules: IndexMap<RulePath, ShowRule> =
564        IndexMap::with_capacity(local_show_rules.len());
565    for path in show_order {
566        if let Some(local) = local_show_rules.remove(path) {
567            show_rules.insert(path.clone(), local);
568        }
569    }
570    assert!(
571        local_show_rules.is_empty(),
572        "BUG: local rules missing from the Show order"
573    );
574
575    let mut plan = ExecutionPlan {
576        spec_name: main_spec.name.clone(),
577        commentary: main_spec.commentary.clone(),
578        data,
579        normal_forms,
580        rules,
581        data_reference_order: graph.data_reference_order.to_vec(),
582        meta: main_spec
583            .meta_fields
584            .iter()
585            .map(|f| (f.key.clone(), f.value.clone()))
586            .collect(),
587        resolved_types,
588        family_units,
589        signature_index,
590        effective: effective.clone(),
591        // Filled by attach_show_cache after this returns.
592        effective_from: None,
593        effective_to: None,
594        start_line: 1,
595        source_type: None,
596        needed_by_rules: Vec::new(),
597        data_display: IndexMap::new(),
598        show_rules,
599        // Filled below after validation succeeds.
600        show_needed_by: IndexMap::new(),
601        show_import_data: show_imports
602            .data
603            .iter()
604            .map(|(path, identity)| (path.clone(), identity.clone()))
605            .collect(),
606        reference_ends: IndexMap::new(),
607        input_key_index: IndexMap::new(),
608        show_rule_index: IndexMap::new(),
609        data_leaf: IndexMap::new(),
610        member_targets,
611        row_templates: graph.instance_lists.templates.clone(),
612        identity: graph.instance_lists.self_identity.clone(),
613        own_spec,
614        edges,
615        rule_candidates: graph
616            .rule_candidates
617            .iter()
618            .map(|(rule, candidates)| (rule.clone(), candidates.clone()))
619            .collect(),
620        self_row_rules: Vec::new(),
621        instance_lists: graph.instance_lists.sources.clone(),
622        row_mask: Vec::new(),
623        initial_values: Vec::new(),
624        walk_settled_leaves: Vec::new(),
625        rule_ref_indexes: Vec::new(),
626        data_slot_leaves: Vec::new(),
627        data_slot_scale_targets: Vec::new(),
628    };
629    plan.data_slot_scale_targets = data_slot_scale_targets(&plan.data);
630
631    let mut plan_errors = validate_literal_data_against_types(&plan);
632    if let Err(error) = validate_unit_conversion_targets(&plan) {
633        plan_errors.push(error);
634    }
635    if !plan_errors.is_empty() {
636        return Err(plan_errors);
637    }
638
639    plan.reference_ends = graph.reference_ends.clone();
640    let input_keys: Vec<DataPath> = plan.data.keys().cloned().collect();
641    plan.data_leaf = ensure_data_leaves(&mut plan.normal_forms, &input_keys, &plan.data);
642    plan.input_key_index = input_keys
643        .iter()
644        .map(|path| (path.input_key(), path.clone()))
645        .collect();
646    plan.show_rule_index = plan
647        .show_rules
648        .keys()
649        .map(|path| (path.input_key(), path.clone()))
650        .collect();
651    for (path, &id) in &plan.data_leaf {
652        debug_assert_eq!(
653            plan.result_type(id).as_ref(),
654            data_path_result_type(&plan.data, path).as_ref(),
655            "BUG: DataPath leaf result_type drift for {path}"
656        );
657    }
658    let bits_by_rule = plan.build_needed_by_rules();
659    plan.show_needed_by =
660        plan.build_show_needed_by(show_order, show_edges, show_imports, &bits_by_rule);
661    plan.fill_row_field_needs(&bits_by_rule);
662    plan.row_mask = build_row_mask(&plan.normal_forms).map_err(|error| vec![error])?;
663    plan.self_row_rules = self_row_rules(&plan);
664    plan.populate_evaluation_tables();
665    validate_option_rules(&plan)?;
666    Ok(plan)
667}
668
669/// The `uses` hops a reference target is read through; empty for a target of the plan itself.
670pub(crate) fn reference_target_segments(target: &ReferenceTarget) -> &[PathSegment] {
671    match target {
672        ReferenceTarget::Data(path) => &path.segments,
673        ReferenceTarget::Rule(rule) => &rule.segments,
674    }
675}
676
677/// Whether `target`'s first hop is a `uses` Import of this plan (not a list name).
678fn first_hop_is_uses_import(
679    data: &IndexMap<DataPath, DataDefinition>,
680    target: &ReferenceTarget,
681) -> bool {
682    let Some(first) = reference_target_segments(target).first() else {
683        return false;
684    };
685    matches!(
686        data.get(&DataPath::local(first.uses.to_string())),
687        Some(DataDefinition::Import { .. })
688    )
689}
690
691/// Dynamic scale targets declared on this plan's data-slot types, in slot order.
692fn data_slot_scale_targets(data: &IndexMap<DataPath, DataDefinition>) -> Vec<ReferenceTarget> {
693    let mut seen = BTreeSet::new();
694    let mut out = Vec::new();
695    for definition in data.values() {
696        let Some(lemma_type) = definition.schema_type() else {
697            continue;
698        };
699        for target in lemma_type.dynamic_scale_targets() {
700            if seen.insert(target.clone()) {
701                out.push(target);
702            }
703        }
704    }
705    out
706}
707
708/// `i.j.x` as a member read on the plan's import slot `i`: the expression a member
709/// target cell lowers.
710fn member_target_expression(target: &ReferenceTarget) -> crate::planning::semantics::Expression {
711    let (first, rest) = reference_target_segments(target)
712        .split_first()
713        .expect("BUG: a member target has a `uses` hop");
714    let name = match target {
715        ReferenceTarget::Data(path) => path.data.clone(),
716        ReferenceTarget::Rule(rule) => rule.rule.clone(),
717    };
718    let mut table = crate::parsing::arena::ExprTable::default();
719    let base = table.push(
720        crate::planning::semantics::ExpressionKind::DataPath(DataPath::local(
721            first.uses.to_string(),
722        )),
723        None,
724    );
725    let root = table.push(
726        crate::planning::semantics::ExpressionKind::SpecMember {
727            base,
728            path: rest
729                .iter()
730                .map(|segment| segment.uses.to_string())
731                .collect(),
732            name,
733        },
734        None,
735    );
736    crate::planning::semantics::Expression::new(table, root)
737}
738
739/// Rule the options of data `path` come from: the rule named by the slot's type, in
740/// the spec that declared the slot (the hops of `path`). `None` when the type has no
741/// options rule.
742pub(crate) fn options_rule_path(data_path: &DataPath, lemma_type: &LemmaType) -> Option<RulePath> {
743    let name = lemma_type.options_rule()?;
744    Some(RulePath {
745        segments: data_path.segments.clone(),
746        rule: name.to_string(),
747    })
748}
749
750fn validate_option_rules(plan: &ExecutionPlan) -> Result<(), Vec<Error>> {
751    let mut errors = Vec::new();
752    for (path, definition) in &plan.data {
753        let Some(lemma_type) = definition.schema_type() else {
754            continue;
755        };
756        let owners = option_owners(lemma_type);
757        for owner in owners {
758            // The owner's own rule: a list's element is checked as its own owner.
759            let Some(name) = &owner.options.rule else {
760                continue;
761            };
762            let rule_path = RulePath {
763                segments: path.segments.clone(),
764                rule: name.clone(),
765            };
766            let Some(rule) = plan.rules.get(&rule_path) else {
767                errors.push(Error::validation(
768                    format!("options rule '{name}' does not exist"),
769                    None,
770                    None::<String>,
771                ));
772                continue;
773            };
774            let TypeSpecification::List { element, .. } = &rule.rule_type.specifications else {
775                errors.push(Error::validation(
776                    format!("options rule '{name}' must return a list"),
777                    None,
778                    None::<String>,
779                ));
780                continue;
781            };
782            if !option_element_matches(owner, element) {
783                errors.push(Error::validation(
784                    format!(
785                        "options rule '{name}' returns {}, not a list of {}",
786                        rule.rule_type.name(),
787                        owner.name()
788                    ),
789                    None,
790                    None::<String>,
791                ));
792            }
793        }
794    }
795    if errors.is_empty() {
796        Ok(())
797    } else {
798        Err(errors)
799    }
800}
801
802fn option_owners(lemma_type: &LemmaType) -> Vec<&LemmaType> {
803    let mut owners = vec![lemma_type];
804    if let TypeSpecification::List { element, .. } = &lemma_type.specifications {
805        owners.push(element.as_ref());
806    }
807    owners
808}
809
810fn option_element_matches(owner: &LemmaType, element: &LemmaType) -> bool {
811    match (&owner.specifications, &element.specifications) {
812        (TypeSpecification::Spec { spec: left }, TypeSpecification::Spec { spec: right }) => {
813            left == right
814        }
815        _ => {
816            owner.has_same_base_type(element)
817                && !matches!(owner.specifications, TypeSpecification::Spec { .. })
818        }
819    }
820}
821
822/// Bitset over the row fields ([`ExecutionPlan::row_field_position`]) of rows of `identity`.
823fn row_bits_entry<'a>(
824    plan: &ExecutionPlan,
825    row_bits: &'a mut BTreeMap<SpecIdentity, Vec<u64>>,
826    identity: &SpecIdentity,
827) -> &'a mut Vec<u64> {
828    let words = plan.row_template(identity).row_field_count().div_ceil(64);
829    row_bits
830        .entry(identity.clone())
831        .or_insert_with(|| vec![0u64; words])
832}
833
834/// Set the bit of the slot an evaluation-data row binds when a rule reads `path` on it.
835fn mark_template_data_path(template: &ExecutionPlan, path: &DataPath, bits: &mut [u64]) {
836    if let Some(target) = template.bindable_row_field(path) {
837        let data_pos = template
838            .data
839            .get_index_of(target)
840            .unwrap_or_else(|| panic!("BUG: bindable row field '{target}' absent from plan.data"));
841        bits[data_pos / 64] |= 1u64 << (data_pos % 64);
842    }
843}
844
845/// Row fields a finished template's rule needs: its data (from `needed_by_rules`) and
846/// the data of its `uses` imports (from `show_needed_by`).
847fn mark_template_rule_data(template: &ExecutionPlan, rule: &RulePath, bits: &mut [u64]) {
848    let rule_pos = u32::try_from(template.rules.get_index_of(rule).unwrap_or_else(|| {
849        panic!(
850            "BUG: row rule '{rule}' missing from template plan '{}'",
851            template.spec_name
852        )
853    }))
854    .expect("BUG: rule count exceeds u32");
855    for (data_pos, consumers) in template.needed_by_rules.iter().enumerate() {
856        if consumers.rules.contains(&rule_pos) {
857            bits[data_pos / 64] |= 1u64 << (data_pos % 64);
858        }
859    }
860    mark_template_import_data(template, rule, bits);
861}
862
863/// Row fields of `uses` imports of `template` that its Show rule `rule` (a path composed
864/// from import hops) needs.
865fn mark_template_import_data(template: &ExecutionPlan, rule: &RulePath, bits: &mut [u64]) {
866    let Some(rule_pos) = template.show_rule_position(rule) else {
867        return;
868    };
869    for (index, path) in template.show_import_data.keys().enumerate() {
870        let needed = template
871            .show_needed_by
872            .get(path)
873            .expect("BUG: Show import data has readers");
874        if needed.contains(&rule_pos) {
875            let field_pos = template.data.len() + index;
876            bits[field_pos / 64] |= 1u64 << (field_pos % 64);
877        }
878    }
879}
880
881/// One `uses` level below a row spec in [`mark_row_end_needs`]: the instance the slot
882/// holds there (a consumer-written override or the template's own) and its template.
883#[derive(Clone)]
884struct MemberLevel<'p> {
885    segment: PathSegment,
886    row: &'p RowInstance,
887    template: &'p ExecutionPlan,
888}
889
890/// A read on rows of a row spec, at a chain of `uses` levels below it.
891enum MemberNeed<'p> {
892    Data(Vec<MemberLevel<'p>>, DataPath),
893    Rule(Vec<MemberLevel<'p>>, RulePath),
894}
895
896/// `chain` extended by the `uses` slots `aliases`, starting at the template of the
897/// chain's last level (or `holder` for an empty chain).
898fn member_levels<'p, 'a>(
899    plan: &'p ExecutionPlan,
900    holder: &'p ExecutionPlan,
901    mut chain: Vec<MemberLevel<'p>>,
902    aliases: impl IntoIterator<Item = &'a str>,
903) -> Vec<MemberLevel<'p>> {
904    for alias in aliases {
905        let template = chain.last().map_or(holder, |level| level.template);
906        let slot = DataPath::local(alias.to_string());
907        let Some(DataDefinition::Import { row, .. }) = template.data.get(&slot) else {
908            panic!("BUG: member hop '{slot}' is not a `uses` slot");
909        };
910        let row: &RowInstance = match chain
911            .last()
912            .and_then(|level| level.row.get_binding(template, &slot))
913        {
914            Some(RowBinding::Instance(written)) => written,
915            Some(other) => panic!("BUG: `uses` slot '{slot}' bound to {other:?}"),
916            None => row,
917        };
918        chain.push(MemberLevel {
919            segment: PathSegment::import(&slot.data, &row.identity),
920            row,
921            template: plan.row_template(&row.identity),
922        });
923    }
924    chain
925}
926
927/// Record what reading the member chain `ends` on evaluation-data rows of `identity`
928/// needs. Every end but the last reads a `uses` slot.
929fn mark_row_end_needs(
930    plan: &ExecutionPlan,
931    identity: &SpecIdentity,
932    ends: &[RowEnd],
933    bits_by_rule: &[Vec<u64>],
934    row_bits: &mut BTreeMap<SpecIdentity, Vec<u64>>,
935    consumer_rule: &RulePath,
936) {
937    let bits = row_bits_entry(plan, row_bits, identity);
938    let holder = plan.row_template(identity);
939    let (end, hops) = ends.split_last().expect("BUG: a row read has a last end");
940    let chain = member_levels(
941        plan,
942        holder,
943        Vec::new(),
944        hops.iter().map(|hop| match hop {
945            RowEnd::Data(slot) => slot.data.as_str(),
946            RowEnd::Rule(rule) => panic!("BUG: a row read hop is a `uses` slot, got rule '{rule}'"),
947        }),
948    );
949    let mut pending = vec![match end {
950        RowEnd::Data(path) => MemberNeed::Data(chain, path.clone()),
951        RowEnd::Rule(rule) => MemberNeed::Rule(chain, rule.clone()),
952    }];
953    while let Some(need) = pending.pop() {
954        match need {
955            MemberNeed::Data(chain, path) if !path.segments.is_empty() => {
956                let chain = member_levels(
957                    plan,
958                    holder,
959                    chain,
960                    path.segments.iter().map(|segment| &*segment.uses),
961                );
962                pending.push(MemberNeed::Data(chain, DataPath::local(path.data)));
963            }
964            MemberNeed::Rule(chain, rule) if !rule.segments.is_empty() => {
965                let chain = member_levels(
966                    plan,
967                    holder,
968                    chain,
969                    rule.segments.iter().map(|segment| &*segment.uses),
970                );
971                pending.push(MemberNeed::Rule(
972                    chain,
973                    RulePath::new(Vec::new(), rule.rule),
974                ));
975            }
976            MemberNeed::Data(chain, path) if chain.is_empty() => {
977                mark_holder_data(plan, identity, &path, bits_by_rule, bits, consumer_rule);
978            }
979            MemberNeed::Rule(chain, rule) if chain.is_empty() => {
980                mark_holder_rule(plan, identity, &rule, bits_by_rule, bits, consumer_rule);
981            }
982            MemberNeed::Data(mut chain, path) => {
983                let level = chain.last().expect("BUG: non-empty member chain");
984                match level.row.get_binding(level.template, &path) {
985                    None => match level.template.data.get(&path) {
986                        Some(DataDefinition::Reference { .. }) => {
987                            match level.template.reference_ends.get(&path) {
988                                Some(ReferenceEnd::Promptable(target)) => {
989                                    pending.push(MemberNeed::Data(chain, target.clone()));
990                                }
991                                Some(ReferenceEnd::Rule(rule)) => {
992                                    pending.push(MemberNeed::Rule(chain, rule.clone()));
993                                }
994                                None => panic!(
995                                    "BUG: reference '{path}' missing from reference_ends after planning"
996                                ),
997                            }
998                        }
999                        Some(_) => {
1000                            let composed = DataPath {
1001                                segments: chain.iter().map(|level| level.segment.clone()).collect(),
1002                                data: path.data,
1003                            };
1004                            if let Some(index) = holder.show_import_data.get_index_of(&composed) {
1005                                let field_pos = holder.data.len() + index;
1006                                bits[field_pos / 64] |= 1u64 << (field_pos % 64);
1007                            }
1008                        }
1009                        None => panic!("BUG: member data '{path}' absent from its template"),
1010                    },
1011                    Some(RowBinding::Deferred { target, hops }) => {
1012                        let frame = chain
1013                            .len()
1014                            .checked_sub(1 + *hops as usize)
1015                            .expect("BUG: a `uses` binding hops above the row spec");
1016                        let target = target.clone();
1017                        chain.truncate(frame);
1018                        pending.push(match target {
1019                            ReferenceTarget::Data(path) => MemberNeed::Data(chain, path),
1020                            ReferenceTarget::Rule(rule) => MemberNeed::Rule(chain, rule),
1021                        });
1022                    }
1023                    Some(
1024                        RowBinding::Bound(_) | RowBinding::Rows { .. } | RowBinding::Instance(_),
1025                    ) => {}
1026                }
1027            }
1028            MemberNeed::Rule(chain, rule) => {
1029                let template = chain.last().expect("BUG: non-empty member chain").template;
1030                let rule_pos =
1031                    u32::try_from(template.rules.get_index_of(&rule).unwrap_or_else(|| {
1032                        panic!("BUG: member rule '{rule}' missing from its template")
1033                    }))
1034                    .expect("BUG: rule count exceeds u32");
1035                let show_pos = template
1036                    .show_rule_position(&rule)
1037                    .expect("BUG: a local rule is a Show rule of its plan");
1038                for ((path, _), needed) in template.data.iter().zip(&template.needed_by_rules) {
1039                    if needed.rules.contains(&rule_pos) {
1040                        pending.push(MemberNeed::Data(chain.clone(), path.clone()));
1041                    }
1042                }
1043                for (path, needed) in &template.show_needed_by {
1044                    if !path.segments.is_empty() && needed.contains(&show_pos) {
1045                        pending.push(MemberNeed::Data(chain.clone(), path.clone()));
1046                    }
1047                }
1048            }
1049        }
1050    }
1051}
1052
1053/// What reading data `path` on rows of the row spec `identity` itself needs.
1054fn mark_holder_data(
1055    plan: &ExecutionPlan,
1056    identity: &SpecIdentity,
1057    path: &DataPath,
1058    bits_by_rule: &[Vec<u64>],
1059    bits: &mut [u64],
1060    consumer_rule: &RulePath,
1061) {
1062    let template = plan.row_template(identity);
1063    match template.reference_ends.get(path) {
1064        Some(ReferenceEnd::Rule(rule)) => {
1065            mark_holder_rule(plan, identity, rule, bits_by_rule, bits, consumer_rule);
1066        }
1067        Some(ReferenceEnd::Promptable(_)) | None => mark_template_data_path(template, path, bits),
1068    }
1069}
1070
1071/// What reading rule `rule` on rows of the row spec `identity` itself needs.
1072fn mark_holder_rule(
1073    plan: &ExecutionPlan,
1074    identity: &SpecIdentity,
1075    rule: &RulePath,
1076    bits_by_rule: &[Vec<u64>],
1077    bits: &mut [u64],
1078    consumer_rule: &RulePath,
1079) {
1080    let template = plan.row_template(identity);
1081    if *identity == plan.identity {
1082        let rule_pos = plan.rules.get_index_of(rule).unwrap_or_else(|| {
1083            panic!("BUG: row rule '{rule}' missing from plan.rules (rule '{consumer_rule}')")
1084        });
1085        for (word, dep) in bits.iter_mut().zip(bits_by_rule[rule_pos].iter()) {
1086            *word |= *dep;
1087        }
1088        mark_template_import_data(template, rule, bits);
1089    } else {
1090        mark_template_rule_data(template, rule, bits);
1091    }
1092}
1093
1094fn mark_list_key_fields(
1095    plan: &ExecutionPlan,
1096    list: NormalFormId,
1097    row_bits: &mut BTreeMap<SpecIdentity, Vec<u64>>,
1098    consumer_rule: &RulePath,
1099) {
1100    use crate::planning::semantics::ListKey;
1101
1102    let list_type = plan.result_type(list);
1103    let TypeSpecification::List { element, key, .. } = &list_type.specifications else {
1104        panic!(
1105            "BUG: Find/WithRow list cell is not a list type (rule '{consumer_rule}'): {list_type:?}"
1106        );
1107    };
1108    let TypeSpecification::Spec { spec: identity } = &element.specifications else {
1109        return;
1110    };
1111    let ListKey::Fields(fields) = key else {
1112        return;
1113    };
1114    let template = plan.row_template(identity);
1115    let bits = row_bits_entry(plan, row_bits, identity);
1116    for key_field in fields {
1117        mark_template_data_path(template, &key_field.path, bits);
1118    }
1119}
1120
1121/// [`ExecutionPlan::self_row_rules`]: every rule a row field or row member read of a
1122/// self-list names, directly or through a data reference to a rule.
1123fn self_row_rules(plan: &ExecutionPlan) -> Vec<RulePath> {
1124    use crate::planning::normalize::NormalFormKind;
1125    use crate::planning::semantics::RowEnd;
1126
1127    let mut rules_read: HashSet<&RulePath> = HashSet::new();
1128    for (index, cell) in plan.normal_forms.iter().enumerate() {
1129        let end = match &cell.kind {
1130            NormalFormKind::RowField { identity, end, .. } if *identity == plan.identity => end,
1131            NormalFormKind::SpecMember { base, row, .. } => match &plan
1132                .result_type(*base)
1133                .specifications
1134            {
1135                TypeSpecification::Spec { spec } if *spec == plan.identity => {
1136                    row.ends.first().expect("BUG: a row read has a first end")
1137                }
1138                TypeSpecification::Spec { .. } => continue,
1139                other => panic!("BUG: row member read on cell {index} of non-spec type {other:?}"),
1140            },
1141            _ => continue,
1142        };
1143        match end {
1144            RowEnd::Rule(rule) => {
1145                rules_read.insert(rule);
1146            }
1147            RowEnd::Data(path) => {
1148                if let Some(ReferenceEnd::Rule(rule)) = plan.reference_ends.get(path) {
1149                    rules_read.insert(rule);
1150                }
1151            }
1152        }
1153    }
1154    plan.rules
1155        .keys()
1156        .filter(|rule| rules_read.contains(rule))
1157        .cloned()
1158        .collect()
1159}
1160
1161impl ExecutionPlan {
1162    /// Template plan rows of `identity` read: this plan for its own self-lists.
1163    pub(crate) fn row_template(&self, identity: &SpecIdentity) -> &ExecutionPlan {
1164        if *identity == self.identity {
1165            return self;
1166        }
1167        self.row_templates
1168            .get(identity)
1169            .unwrap_or_else(|| panic!("BUG: rows of '{identity}' without a template plan"))
1170    }
1171
1172    /// Row of this plan's `uses` slot `slot` as read on `row` of this plan (`None` for
1173    /// the plan's own context): the nested row the holder bound over it, else the
1174    /// plan's own import row.
1175    ///
1176    /// `dynamic_rows` supplies the body chosen for a `uses … -> effective` slot of this
1177    /// plan (keyed by the slot path). Nested `RowBinding::Instance` hops ignore it.
1178    /// Pass `None` when no dynamic imports are in play (or only nested instance hops).
1179    pub(crate) fn import_row<'a>(
1180        &'a self,
1181        row: Option<&'a RowInstance>,
1182        slot: &DataPath,
1183        dynamic_rows: Option<&'a HashMap<DataPath, Arc<RowInstance>>>,
1184    ) -> &'a Arc<RowInstance> {
1185        let (index, _, definition) = self
1186            .data
1187            .get_full(slot)
1188            .unwrap_or_else(|| panic!("BUG: `uses` slot '{slot}' is not data of its plan"));
1189        if let Some(RowBinding::Instance(nested)) = row.and_then(|row| row.bindings[index].as_ref())
1190        {
1191            return nested;
1192        }
1193        let DataDefinition::Import { row, pin, .. } = definition else {
1194            panic!("BUG: data '{slot}' read as a `uses` slot is not an import");
1195        };
1196        if pin.is_some() {
1197            return dynamic_rows
1198                .and_then(|rows| rows.get(slot))
1199                .unwrap_or_else(|| {
1200                    panic!("BUG: dynamic import '{slot}' hop without a selected body")
1201                });
1202        }
1203        row
1204    }
1205
1206    /// Select the candidate body of a dynamic import active at `at`.
1207    pub(crate) fn select_dynamic_import_row(
1208        &self,
1209        slot: &DataPath,
1210        at: &crate::parsing::ast::EffectiveDate,
1211    ) -> Option<&Arc<RowInstance>> {
1212        let DataDefinition::Import {
1213            pin: Some(_),
1214            candidates,
1215            ..
1216        } = self.data.get(slot)?
1217        else {
1218            return None;
1219        };
1220        candidates
1221            .iter()
1222            .filter(|(identity, _)| identity.slice <= *at)
1223            .max_by(|a, b| a.0.slice.cmp(&b.0.slice))
1224            .map(|(_, row)| row)
1225    }
1226
1227    /// Every [`Self::data`] slot that is not an Import marker: the members show and
1228    /// the row field sets expose.
1229    pub(crate) fn input_slots(&self) -> impl Iterator<Item = (&DataPath, &DataDefinition)> {
1230        self.data
1231            .iter()
1232            .filter(|(_, definition)| !matches!(definition, DataDefinition::Import { .. }))
1233    }
1234
1235    /// Row-suppliable slots a read of `end` on a row of this plan touches: the slot
1236    /// `end` is, or every slot the rule `end` names (or the data reference `end`
1237    /// resolves to) reads. `unbound` marks slots the spec gives no value: a row
1238    /// appended inline must write each of them.
1239    pub(crate) fn row_end_reads(&self, end: &RowEnd) -> Vec<RowFieldRead> {
1240        let rule_reads = |rule: &RulePath| -> Vec<&DataPath> {
1241            let rule_pos = u32::try_from(self.rules.get_index_of(rule).unwrap_or_else(|| {
1242                panic!(
1243                    "BUG: row rule '{rule}' missing from plan '{}'",
1244                    self.spec_name
1245                )
1246            }))
1247            .expect("BUG: rule count exceeds u32");
1248            self.needed_by_rules
1249                .iter()
1250                .enumerate()
1251                .filter(|(_, consumers)| consumers.rules.contains(&rule_pos))
1252                .map(|(data_pos, _)| {
1253                    self.data
1254                        .get_index(data_pos)
1255                        .expect("BUG: needed_by_rules position outside plan.data")
1256                        .0
1257                })
1258                .collect()
1259        };
1260        let slots: Vec<&DataPath> = match end {
1261            RowEnd::Data(path) => match self.reference_ends.get(path) {
1262                Some(ReferenceEnd::Rule(rule)) => rule_reads(rule),
1263                Some(ReferenceEnd::Promptable(_)) | None => {
1264                    self.bindable_row_field(path).into_iter().collect()
1265                }
1266            },
1267            RowEnd::Rule(rule) => rule_reads(rule),
1268        };
1269        slots
1270            .into_iter()
1271            .map(|slot| RowFieldRead {
1272                slot: slot.clone(),
1273                unbound: self.data[slot].value().is_none(),
1274            })
1275            .collect()
1276    }
1277
1278    /// Slot an evaluation-data row of this plan binds when a field read lands on
1279    /// `path`: the slot itself, or the end of its reference chain. `None` when the slot
1280    /// is bound by the spec, computed by a rule, or an instance marker.
1281    fn bindable_row_field<'a>(&'a self, path: &'a DataPath) -> Option<&'a DataPath> {
1282        match self
1283            .data
1284            .get(path)
1285            .unwrap_or_else(|| panic!("BUG: row field '{path}' absent from plan.data"))
1286        {
1287            DataDefinition::Value { .. }
1288            | DataDefinition::TypeDeclaration { .. }
1289            | DataDefinition::InstanceList { .. } => Some(path),
1290            DataDefinition::Reference { .. } => match self.reference_ends.get(path) {
1291                Some(ReferenceEnd::Promptable(target)) => Some(target),
1292                Some(ReferenceEnd::Rule(_)) => None,
1293                None => {
1294                    panic!("BUG: reference '{path}' missing from reference_ends after planning")
1295                }
1296            },
1297            DataDefinition::Import { .. } => None,
1298        }
1299        .filter(|target| !self.data[*target].is_bound())
1300    }
1301}
1302
1303/// Nesting of list commands a plan supports: one bit per level in [`ExecutionPlan::row_mask`].
1304const MAX_ROW_LEVELS: u32 = u32::BITS;
1305
1306/// [`ExecutionPlan::row_mask`]: a list-row leaf (`RowField` or `RowValue`) sets its
1307/// level, a list command clears its own level, every other cell takes the union of
1308/// its children.
1309fn build_row_mask(normal_forms: &[NormalForm]) -> Result<Vec<u32>, Error> {
1310    use crate::planning::normalize::{LeafKind, NormalFormKind};
1311
1312    let mut mask: Vec<Option<u32>> = vec![None; normal_forms.len()];
1313    let mut stack: Vec<(usize, bool)> = Vec::new();
1314    for root in 0..normal_forms.len() {
1315        if mask[root].is_some() {
1316            continue;
1317        }
1318        stack.push((root, false));
1319        while let Some((index, children_done)) = stack.pop() {
1320            if mask[index].is_some() {
1321                continue;
1322            }
1323            let cell = &normal_forms[index];
1324            if !children_done {
1325                stack.push((index, true));
1326                cell.kind.for_each_child(|child| {
1327                    if mask[child.index()].is_none() {
1328                        stack.push((child.index(), false));
1329                    }
1330                });
1331                continue;
1332            }
1333            let mut union = 0u32;
1334            cell.kind.for_each_child(|child| {
1335                union |= mask[child.index()]
1336                    .expect("BUG: child row mask missing after post-order visit");
1337            });
1338            let level = match &cell.kind {
1339                NormalFormKind::RowField { level, .. }
1340                | NormalFormKind::Leaf(LeafKind::RowValue { level }) => Some(*level),
1341                _ => None,
1342            };
1343            let bits = match (level, &cell.kind) {
1344                (Some(level), _) => {
1345                    if level >= MAX_ROW_LEVELS {
1346                        return Err(Error::validation(
1347                            format!(
1348                                "list commands nest {} levels deep; at most {MAX_ROW_LEVELS} are supported",
1349                                level + 1
1350                            ),
1351                            cell.source.clone(),
1352                            None::<String>,
1353                        ));
1354                    }
1355                    union | (1u32 << level)
1356                }
1357                (
1358                    None,
1359                    NormalFormKind::ListCommand {
1360                        row_level: Some(level),
1361                        ..
1362                    },
1363                ) if *level < MAX_ROW_LEVELS => union & !(1u32 << level),
1364                (None, _) => union,
1365            };
1366            mask[index] = Some(bits);
1367        }
1368    }
1369    Ok(mask
1370        .into_iter()
1371        .map(|bits| bits.expect("BUG: row mask missing for a cell"))
1372        .collect())
1373}
1374
1375/// What evaluating one input cell needs: a data slot or a rule.
1376enum InputNeed {
1377    Data(DataPath),
1378    Rule(RulePath),
1379}
1380
1381/// Pass-1 accumulator of [`ExecutionPlan::build_needed_by_rules`] for one rule.
1382struct NeedsMarker<'a> {
1383    plan: &'a ExecutionPlan,
1384    rule_pos: usize,
1385    bits: &'a mut [u64],
1386    bits_by_rule: &'a [Vec<u64>],
1387}
1388
1389impl NeedsMarker<'_> {
1390    fn mark(&mut self, need: InputNeed) {
1391        match need {
1392            InputNeed::Rule(rule) => {
1393                let ref_pos =
1394                    self.plan.rules.get_index_of(&rule).unwrap_or_else(|| {
1395                        panic!("BUG: needed rule '{rule}' missing from plan.rules")
1396                    });
1397                if ref_pos >= self.rule_pos {
1398                    panic!(
1399                        "BUG: needed rule '{rule}' must precede '{}' in topo order",
1400                        self.plan.rules[self.rule_pos].path
1401                    );
1402                }
1403                for (word, dep_word) in self.bits.iter_mut().zip(&self.bits_by_rule[ref_pos]) {
1404                    *word |= *dep_word;
1405                }
1406            }
1407            InputNeed::Data(path) => {
1408                let data_pos =
1409                    self.plan.data.get_index_of(&path).unwrap_or_else(|| {
1410                        panic!("BUG: needed data '{path}' missing from plan.data")
1411                    });
1412                self.bits[data_pos / 64] |= 1u64 << (data_pos % 64);
1413            }
1414        }
1415    }
1416
1417    fn mark_scale_target(&mut self, stack: &mut Vec<NormalFormId>, target: &ReferenceTarget) {
1418        if let Some(&cell) = self.plan.member_targets.get(target) {
1419            stack.push(cell);
1420            return;
1421        }
1422        let segments = reference_target_segments(target);
1423        if segments.is_empty() {
1424            match target {
1425                ReferenceTarget::Rule(rule) => self.mark(InputNeed::Rule(rule.clone())),
1426                ReferenceTarget::Data(path) => {
1427                    if let Some(need) = self.plan.data_read_needs(path) {
1428                        self.mark(need);
1429                    }
1430                }
1431            }
1432            return;
1433        }
1434        // Uses-qualified target that is a `uses` Import of this plan must have a
1435        // member cell. A first hop that is not this plan's import (list name, nested
1436        // element's uses) settles in that row template — not as a SpecMember here.
1437        if first_hop_is_uses_import(&self.plan.data, target) {
1438            panic!(
1439                "BUG: uses-qualified scale target '{target}' missing from member_targets of plan '{}'",
1440                self.plan.spec_name
1441            );
1442        }
1443    }
1444}
1445
1446/// Ensure every [`ExecutionPlan::data`] path has a [`LeafKind::DataPath`] cell in `normal_forms`
1447/// and return the path → id index. Expression lowering already creates leaves for
1448/// referenced paths; unused paths get orphan leaves appended so evaluation can
1449/// still store bindings/defaults/reference copies in the value table.
1450///
1451/// Rule reference cells share a body's Kind, so a `Leaf(DataPath)` cell may carry
1452/// `rule_ref`. Those cells hold the referenced rule's value in the value table and
1453/// must never be the binding slot for the path.
1454///
1455/// Fold survivors also share a body's Kind and carry `origin` (identity elim of
1456/// `x * 1` → DataPath `x` with the Product as pre-image). Those cells are the
1457/// rule body's explanation root, not the binding slot: intake must land on the
1458/// origin-free leaf so an explain walk still visits the pre-image.
1459fn ensure_data_leaves(
1460    normal_forms: &mut Vec<NormalForm>,
1461    input_keys: &[DataPath],
1462    data: &IndexMap<DataPath, DataDefinition>,
1463) -> IndexMap<DataPath, NormalFormId> {
1464    use crate::planning::normalize::LeafKind;
1465    use crate::planning::normalize::NormalFormKind;
1466
1467    let mut data_leaf: IndexMap<DataPath, NormalFormId> = IndexMap::new();
1468    for (index, cell) in normal_forms.iter().enumerate() {
1469        if cell.rule_ref.is_some() || cell.origin.is_some() {
1470            continue;
1471        }
1472        if let NormalFormKind::Leaf(LeafKind::DataPath(path)) = &cell.kind {
1473            data_leaf.insert(path.clone(), NormalFormId::from_index(index));
1474        }
1475    }
1476    for path in input_keys {
1477        if data_leaf.contains_key(path) {
1478            continue;
1479        }
1480        let id = NormalFormId::from_index(normal_forms.len());
1481        normal_forms.push(NormalForm {
1482            kind: NormalFormKind::Leaf(LeafKind::DataPath(path.clone())),
1483            result_type: data_path_result_type(data, path),
1484            fold_types: Vec::new(),
1485            source: None,
1486            origin: None,
1487            rule_ref: None,
1488        });
1489        data_leaf.insert(path.clone(), id);
1490    }
1491    data_leaf
1492}
1493
1494/// Fill show/response cache fields that are pure functions of the plan and its
1495/// owning LemmaSpec / LemmaSpecSet. Called once after [`build_execution_plan`]
1496/// succeeds so [`Engine::show`] / [`Engine::evaluate`] only look up the plan.
1497pub(crate) fn attach_show_cache(
1498    plan: &mut ExecutionPlan,
1499    lemma_spec_set: &LemmaSpecSet,
1500    spec: &LemmaSpec,
1501) {
1502    let (effective_from, effective_to) = lemma_spec_set.effective_range(spec);
1503    plan.effective_from = effective_from;
1504    plan.effective_to = effective_to;
1505    plan.start_line = spec.start_line;
1506    plan.source_type = spec.source_type.clone();
1507    plan.data_display = build_data_display(plan);
1508}
1509
1510fn build_data_display(plan: &ExecutionPlan) -> IndexMap<DataPath, ShowDataCache> {
1511    let mut out = IndexMap::new();
1512    for (path, data) in &plan.data {
1513        insert_data_display_entry(&mut out, path, data);
1514    }
1515    out
1516}
1517
1518fn insert_data_display_entry(
1519    out: &mut IndexMap<DataPath, ShowDataCache>,
1520    path: &DataPath,
1521    data: &DataDefinition,
1522) {
1523    if data.schema_type().is_none() || matches!(data, DataDefinition::Reference { .. }) {
1524        return;
1525    }
1526    let value = match data {
1527        DataDefinition::Value {
1528            value,
1529            resolved_type,
1530            ..
1531        } => Some(BoundValueKind {
1532            value: value.value.clone(),
1533            unit: resolved_type.unit().map(Arc::from),
1534            instance_factors: None,
1535        }),
1536        _ => data.bound_value().cloned(),
1537    };
1538    let suggestion = data.bound_suggestion().cloned();
1539    if value.is_some() || suggestion.is_some() {
1540        out.insert(path.clone(), ShowDataCache { value, suggestion });
1541    }
1542}
1543
1544/// Whether a show bound value is the fill or the suggestion for a data path.
1545#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1546pub(crate) enum ShowBoundRole {
1547    Value,
1548    Suggestion,
1549}
1550
1551pub(crate) fn show_bound_result_value(
1552    bound: &BoundValueKind,
1553    schema_type: &LemmaType,
1554    input_key: &str,
1555    role: ShowBoundRole,
1556) -> RuleResultValue {
1557    let (fill, suggestion) = match role {
1558        ShowBoundRole::Value => (Some(bound), None),
1559        ShowBoundRole::Suggestion => (None, Some(bound)),
1560    };
1561    let display_type = lemma_type_with_display_binding(schema_type, None, fill, suggestion);
1562    type_scoped_result_value_from_literal(&bound.to_literal(), &display_type)
1563        .unwrap_or_else(|failure| {
1564            let role_name = match role {
1565                ShowBoundRole::Value => "value",
1566                ShowBoundRole::Suggestion => "suggestion",
1567            };
1568            panic!(
1569                "BUG: show {role_name} value for '{input_key}' failed type_scoped_result_value_from_literal: {}",
1570                rule_result_value_failure_message(failure)
1571            )
1572        })
1573}
1574
1575/// Show value of instance list `path`: its spec-written rows in order, each with the
1576/// fields the row binds and no `result` (a row has no value of its own to display).
1577/// Literal fields carry their value; referenced fields carry
1578/// [`RuleResultValue::reference`]. `None` when the list has no spec-written rows.
1579pub(crate) fn show_instance_list_value(
1580    plan: &ExecutionPlan,
1581    path: &DataPath,
1582) -> Option<RuleResultValue> {
1583    let source = plan
1584        .instance_lists
1585        .get(path)
1586        .expect("BUG: instance list data without a source");
1587    if source.store.rows.is_empty() {
1588        return None;
1589    }
1590    let template = plan
1591        .row_templates
1592        .get(&source.identity)
1593        .expect("BUG: list with spec-written rows has a row template");
1594    let mut items = Vec::with_capacity(source.store.rows.len());
1595    for row in &source.store.rows {
1596        items.push(RuleResultValue {
1597            fields: Some(show_written_row_fields(plan, row, template)),
1598            ..Default::default()
1599        });
1600    }
1601    Some(RuleResultValue {
1602        list: Some(items),
1603        ..Default::default()
1604    })
1605}
1606
1607/// Fields a spec-written row binds, keyed by their path under the row. A nested `uses`
1608/// instance the row writes through (`-> with item.price: 3 eur`) shows the fields it
1609/// writes there, not those the row spec's own `uses` already binds.
1610fn show_written_row_fields(
1611    plan: &ExecutionPlan,
1612    row: &RowInstance,
1613    template: &ExecutionPlan,
1614) -> IndexMap<String, RuleResultValue> {
1615    struct Level<'r> {
1616        row: &'r RowInstance,
1617        base: Option<&'r RowInstance>,
1618        template: &'r ExecutionPlan,
1619        prefix: String,
1620        next: usize,
1621    }
1622    let mut fields = IndexMap::new();
1623    let mut levels: Vec<Level<'_>> = vec![Level {
1624        row,
1625        base: None,
1626        template,
1627        prefix: String::new(),
1628        next: 0,
1629    }];
1630    while let Some(level) = levels.last_mut() {
1631        let slot = level.next;
1632        let Some((field_path, definition)) = level.template.data.get_index(slot) else {
1633            levels.pop();
1634            continue;
1635        };
1636        level.next += 1;
1637        let binding = level.row.bindings[slot].as_ref();
1638        let base_binding = level.base.and_then(|base| base.bindings[slot].as_ref());
1639        let copied = match (binding, base_binding) {
1640            (Some(binding), Some(base)) => binding.is_copy_of(base),
1641            (None, _) => true,
1642            (Some(_), None) => false,
1643        };
1644        if copied {
1645            continue;
1646        }
1647        let key = format!("{}{}", level.prefix, field_path.input_key());
1648        let value = match binding.expect("BUG: unbound slots are skipped above") {
1649            RowBinding::Rows { .. } => continue,
1650            RowBinding::Bound(OperationResult::Value(bound)) => {
1651                let schema = definition
1652                    .resolved_type_arc()
1653                    .expect("BUG: stored row bound field has a resolved type");
1654                show_bound_result_value(bound, schema, &key, ShowBoundRole::Value)
1655            }
1656            RowBinding::Bound(OperationResult::Veto(_)) => {
1657                unreachable!("BUG: stored row literal binding is never a Veto")
1658            }
1659            RowBinding::Deferred { target, .. } => RuleResultValue {
1660                reference: Some(match target {
1661                    ReferenceTarget::Data(target) => target.input_key(),
1662                    ReferenceTarget::Rule(target) => target.input_key(),
1663                }),
1664                ..Default::default()
1665            },
1666            RowBinding::Instance(nested) => {
1667                let DataDefinition::Import { row: base, .. } = definition else {
1668                    panic!("BUG: nested instance binding on non-`uses` slot '{field_path}'");
1669                };
1670                let nested_template = plan.row_template(&nested.identity);
1671                levels.push(Level {
1672                    row: nested,
1673                    base: Some(base),
1674                    template: nested_template,
1675                    prefix: format!("{key}."),
1676                    next: 0,
1677                });
1678                continue;
1679            }
1680        };
1681        fields.insert(key, value);
1682    }
1683    fields
1684}
1685
1686/// One data entry in a [`Show`].
1687///
1688/// A named struct instead of a tuple so JSON-native consumers (TypeScript, Python, ...)
1689/// get stable field names. `fill` is a spec literal or literal `with` binding;
1690/// `suggestion` is a `-> suggest ...` hint only.
1691/// Empty `needed_by_rules` means the slot is offered for reuse (`data x: alias.slot`),
1692/// not needed by this spec's remaining rules after normalize.
1693#[derive(Debug, Clone, PartialEq)]
1694pub struct ShowData {
1695    pub lemma_type: LemmaType,
1696    /// Import hops to this slot (`[]` = this spec). Clone of [`DataPath::segments`].
1697    pub path: Vec<PathSegment>,
1698    pub value: Option<RuleResultValue>,
1699    pub suggestion: Option<RuleResultValue>,
1700    /// Show.rules keys that transitively need this data after normalize.
1701    /// Show.data only includes slots where this is non-empty.
1702    pub needed_by_rules: Vec<String>,
1703}
1704
1705/// Consumer [`Engine::show`] result: local offered types, inputs rules need,
1706/// this spec's own rules (empty path), and resolved temporal window.
1707/// Source: [`Engine::source`]. Branch Rule leaves may name reachable imports that
1708/// are not keys of [`Self::rules`]; follow those via `show` on the imported spec.
1709#[derive(Debug, Clone, PartialEq)]
1710pub struct Show {
1711    /// Interned repository name (`specs()` string). `None` = unnamed workspace.
1712    pub repository: Option<String>,
1713    pub spec: String,
1714    pub commentary: Option<String>,
1715    pub effective_from: Option<DateTimeValue>,
1716    pub effective_to: Option<DateTimeValue>,
1717    /// Planning instant of the execution-plan slice that produced this Show.
1718    /// Distinguishes temporal dependency slices that share the same declared window.
1719    pub plan_effective: EffectiveDate,
1720    pub start_line: usize,
1721    pub source_type: Option<SourceType>,
1722    /// Local `data` declarations (empty path; type or value), declaration order.
1723    pub types: indexmap::IndexMap<String, LemmaType>,
1724    /// Inputs rules need (`needed_by_rules` non-empty), local or imported.
1725    pub data: indexmap::IndexMap<String, ShowData>,
1726    pub rules: indexmap::IndexMap<String, ShowRule>,
1727    /// Spec metadata, in declaration order.
1728    pub meta: IndexMap<String, Value>,
1729}
1730
1731impl std::fmt::Display for Show {
1732    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1733        match &self.repository {
1734            Some(repository) => write!(f, "Spec: {} {}", repository, self.spec)?,
1735            None => write!(f, "Spec: {}", self.spec)?,
1736        }
1737        write_effective_window(f, self.effective_from.as_ref(), self.effective_to.as_ref())?;
1738
1739        if let Some(commentary) = &self.commentary {
1740            for line in commentary.lines() {
1741                if line.is_empty() {
1742                    writeln!(f)?;
1743                } else {
1744                    write!(f, "\n  {line}")?;
1745                }
1746            }
1747        }
1748
1749        if !self.meta.is_empty() {
1750            write!(f, "\n\nMeta:")?;
1751            let mut entries: Vec<(&String, &Value)> = self.meta.iter().collect();
1752            entries.sort_by_key(|(k, _)| *k);
1753            for (key, value) in entries {
1754                write!(f, "\n  {}: {}", key, value)?;
1755            }
1756        }
1757
1758        if !self.types.is_empty() {
1759            write!(f, "\n\nTypes:")?;
1760            for (name, lemma_type) in &self.types {
1761                write!(f, "\n  {} ({})", name, lemma_type.specifications)?;
1762                for line in type_detail_lines(&lemma_type.specifications) {
1763                    write!(f, "\n    {}", line)?;
1764                }
1765                let help = lemma_type.specifications.help();
1766                if !help.is_empty() {
1767                    write!(f, "\n    help: {}", help)?;
1768                }
1769            }
1770        }
1771
1772        if !self.data.is_empty() {
1773            write!(f, "\n\nData:")?;
1774            for (name, entry) in &self.data {
1775                write!(f, "\n  {} ({})", name, entry.lemma_type.specifications)?;
1776                for line in type_detail_lines(&entry.lemma_type.specifications) {
1777                    write!(f, "\n    {}", line)?;
1778                }
1779                if let Some(line) = option_detail_line(&entry.lemma_type) {
1780                    write!(f, "\n    {}", line)?;
1781                }
1782                let help = entry.lemma_type.specifications.help();
1783                if !help.is_empty() {
1784                    write!(f, "\n    help: {}", help)?;
1785                }
1786                match &entry.value {
1787                    Some(RuleResultValue {
1788                        list: Some(rows), ..
1789                    }) if rows.iter().all(|row| row.fields.is_some()) => {
1790                        write!(f, "\n    value:")?;
1791                        for (index, row) in rows.iter().enumerate() {
1792                            let fields = row
1793                                .fields
1794                                .as_ref()
1795                                .expect("BUG: guard checked every row has fields")
1796                                .iter()
1797                                .map(|(name, field)| format!("{name} {field}"))
1798                                .collect::<Vec<_>>()
1799                                .join(", ");
1800                            write!(f, "\n      {}: {fields}", index + 1)?;
1801                        }
1802                    }
1803                    Some(val) => write!(f, "\n    value: {}", val)?,
1804                    None => {}
1805                }
1806                if let Some(val) = &entry.suggestion {
1807                    write!(f, "\n    suggestion: {}", val)?;
1808                }
1809                if !entry.needed_by_rules.is_empty() {
1810                    write!(
1811                        f,
1812                        "\n    needed_by_rules: {}",
1813                        entry.needed_by_rules.join(", ")
1814                    )?;
1815                }
1816            }
1817        }
1818
1819        if !self.rules.is_empty() {
1820            write!(f, "\n\nRules:")?;
1821            for (name, rule) in &self.rules {
1822                write!(f, "\n  {} ({})", name, rule.lemma_type.specifications)?;
1823                if !rule.depends_on_rules.is_empty() {
1824                    write!(
1825                        f,
1826                        "\n    depends_on_rules: {}",
1827                        rule.depends_on_rules
1828                            .iter()
1829                            .map(|path| path.input_key())
1830                            .collect::<Vec<_>>()
1831                            .join(", ")
1832                    )?;
1833                }
1834            }
1835        }
1836
1837        if self.types.is_empty() && self.data.is_empty() && self.rules.is_empty() {
1838            write!(f, "\n  (no types, data, or rules)")?;
1839        }
1840
1841        Ok(())
1842    }
1843}
1844
1845/// Produce a human-readable summary of type constraints, or `None` when there
1846/// are no constraints worth showing (e.g. bare `boolean`).
1847/// Returns one formatted string per constraint or property of the type specification.
1848/// Uses `display_str` for all rational bounds so they render as decimals,
1849/// not as raw fractions.
1850pub fn type_detail_lines(spec: &TypeSpecification) -> Vec<String> {
1851    let mut lines = Vec::new();
1852    match spec {
1853        TypeSpecification::Measure {
1854            minimum,
1855            maximum,
1856            units,
1857            ..
1858        } => {
1859            let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
1860            if !unit_names.is_empty() {
1861                lines.push(format!("units: {}", unit_names.join(", ")));
1862            }
1863            if let Some((magnitude, unit_name)) = minimum {
1864                lines.push(format!(
1865                    "minimum: {} {}",
1866                    magnitude.display_str(),
1867                    unit_name
1868                ));
1869            }
1870            if let Some((magnitude, unit_name)) = maximum {
1871                lines.push(format!(
1872                    "maximum: {} {}",
1873                    magnitude.display_str(),
1874                    unit_name
1875                ));
1876            }
1877        }
1878        TypeSpecification::Number {
1879            minimum,
1880            maximum,
1881            decimals,
1882            ..
1883        } => {
1884            if let Some(d) = decimals {
1885                lines.push(format!("decimals: {}", d));
1886            }
1887            if let Some(v) = minimum {
1888                lines.push(format!("minimum: {}", v.display_str()));
1889            }
1890            if let Some(v) = maximum {
1891                lines.push(format!("maximum: {}", v.display_str()));
1892            }
1893        }
1894        TypeSpecification::Ratio {
1895            minimum,
1896            maximum,
1897            units,
1898            ..
1899        } => {
1900            let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
1901            if !unit_names.is_empty() {
1902                lines.push(format!("units: {}", unit_names.join(", ")));
1903            }
1904            if let Some(v) = minimum {
1905                lines.push(format!("minimum: {}", v.display_str()));
1906            }
1907            if let Some(v) = maximum {
1908                lines.push(format!("maximum: {}", v.display_str()));
1909            }
1910        }
1911        TypeSpecification::Text { length, .. } => {
1912            if let Some(l) = length {
1913                lines.push(format!("length: {}", l));
1914            }
1915        }
1916        TypeSpecification::Date {
1917            minimum, maximum, ..
1918        } => {
1919            if let Some(v) = minimum {
1920                lines.push(format!("minimum: {}", v));
1921            }
1922            if let Some(v) = maximum {
1923                lines.push(format!("maximum: {}", v));
1924            }
1925        }
1926        TypeSpecification::Time {
1927            minimum, maximum, ..
1928        } => {
1929            if let Some(v) = minimum {
1930                lines.push(format!("minimum: {}", v));
1931            }
1932            if let Some(v) = maximum {
1933                lines.push(format!("maximum: {}", v));
1934            }
1935        }
1936        TypeSpecification::MeasureRange {
1937            lower,
1938            upper,
1939            minimum,
1940            maximum,
1941            units,
1942            ..
1943        } => {
1944            let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
1945            if !unit_names.is_empty() {
1946                lines.push(format!("units: {}", unit_names.join(", ")));
1947            }
1948            if let Some((magnitude, unit_name)) = lower {
1949                lines.push(format!("lower: {} {}", magnitude.display_str(), unit_name));
1950            }
1951            if let Some((magnitude, unit_name)) = upper {
1952                lines.push(format!("upper: {} {}", magnitude.display_str(), unit_name));
1953            }
1954            if let Some((magnitude, unit_name)) = minimum {
1955                lines.push(format!(
1956                    "minimum: {} {}",
1957                    magnitude.display_str(),
1958                    unit_name
1959                ));
1960            }
1961            if let Some((magnitude, unit_name)) = maximum {
1962                lines.push(format!(
1963                    "maximum: {} {}",
1964                    magnitude.display_str(),
1965                    unit_name
1966                ));
1967            }
1968        }
1969        TypeSpecification::RatioRange {
1970            lower,
1971            upper,
1972            minimum,
1973            maximum,
1974            units,
1975            ..
1976        } => {
1977            let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
1978            if !unit_names.is_empty() {
1979                lines.push(format!("units: {}", unit_names.join(", ")));
1980            }
1981            if let Some(v) = lower {
1982                lines.push(format!("lower: {}", v.display_str()));
1983            }
1984            if let Some(v) = upper {
1985                lines.push(format!("upper: {}", v.display_str()));
1986            }
1987            if let Some(v) = minimum {
1988                lines.push(format!("minimum: {}", v.display_str()));
1989            }
1990            if let Some(v) = maximum {
1991                lines.push(format!("maximum: {}", v.display_str()));
1992            }
1993        }
1994        TypeSpecification::NumberRange {
1995            lower,
1996            upper,
1997            minimum,
1998            maximum,
1999            ..
2000        } => {
2001            if let Some(v) = lower {
2002                lines.push(format!("lower: {}", v.display_str()));
2003            }
2004            if let Some(v) = upper {
2005                lines.push(format!("upper: {}", v.display_str()));
2006            }
2007            if let Some(v) = minimum {
2008                lines.push(format!("minimum: {}", v.display_str()));
2009            }
2010            if let Some(v) = maximum {
2011                lines.push(format!("maximum: {}", v.display_str()));
2012            }
2013        }
2014        TypeSpecification::DateRange {
2015            lower,
2016            upper,
2017            minimum,
2018            maximum,
2019            ..
2020        } => {
2021            if let Some(v) = lower {
2022                lines.push(format!("lower: {}", v));
2023            }
2024            if let Some(v) = upper {
2025                lines.push(format!("upper: {}", v));
2026            }
2027            if let Some((magnitude, unit_name)) = minimum {
2028                lines.push(format!(
2029                    "minimum: {} {}",
2030                    magnitude.display_str(),
2031                    unit_name
2032                ));
2033            }
2034            if let Some((magnitude, unit_name)) = maximum {
2035                lines.push(format!(
2036                    "maximum: {} {}",
2037                    magnitude.display_str(),
2038                    unit_name
2039                ));
2040            }
2041        }
2042        TypeSpecification::TimeRange {
2043            lower,
2044            upper,
2045            minimum,
2046            maximum,
2047            ..
2048        } => {
2049            if let Some(v) = lower {
2050                lines.push(format!("lower: {}", v));
2051            }
2052            if let Some(v) = upper {
2053                lines.push(format!("upper: {}", v));
2054            }
2055            if let Some((magnitude, unit_name)) = minimum {
2056                lines.push(format!(
2057                    "minimum: {} {}",
2058                    magnitude.display_str(),
2059                    unit_name
2060                ));
2061            }
2062            if let Some((magnitude, unit_name)) = maximum {
2063                lines.push(format!(
2064                    "maximum: {} {}",
2065                    magnitude.display_str(),
2066                    unit_name
2067                ));
2068            }
2069        }
2070        TypeSpecification::Boolean { .. }
2071        | TypeSpecification::Veto { .. }
2072        | TypeSpecification::Undetermined
2073        | TypeSpecification::Spec { .. } => {}
2074        TypeSpecification::List { element, key, help } => {
2075            lines.push(format!("element: {element}"));
2076            if !help.is_empty() {
2077                lines.push(format!("help: {help}"));
2078            }
2079            match key {
2080                crate::planning::semantics::ListKey::Numbered => {
2081                    lines.push("key: numbered".to_string());
2082                }
2083                crate::planning::semantics::ListKey::Fields(fields) => {
2084                    lines.push(format!(
2085                        "key: {}",
2086                        fields
2087                            .iter()
2088                            .map(|field| field.name.as_str())
2089                            .collect::<Vec<_>>()
2090                            .join(", ")
2091                    ));
2092                }
2093            }
2094        }
2095    }
2096    lines
2097}
2098
2099impl ExecutionPlan {
2100    /// Expression-scope unit index (local types plus direct `uses` imports).
2101    pub(crate) fn expression_unit_index(&self) -> &UnitIndex {
2102        &self.resolved_types.unit_index
2103    }
2104
2105    /// [`Self::show_needed_by`], each list in input-key order: a catalog rule needs the
2106    /// data it reads that no binding fills, the data `bits_by_rule` gives a local rule,
2107    /// and the needs of every rule it reads (including reachable imports walked in
2108    /// `show_order`). A local rule is not listed for the data `bits_by_rule` already
2109    /// gives it. Reader positions are catalog ([`Self::show_rules`]) indices only.
2110    fn build_show_needed_by(
2111        &self,
2112        show_order: &[RulePath],
2113        show_edges: &BTreeMap<RulePath, BTreeSet<RulePath>>,
2114        show_imports: &crate::planning::graph::ShowImports,
2115        bits_by_rule: &[Vec<u64>],
2116    ) -> IndexMap<DataPath, Vec<u32>> {
2117        let mut paths: indexmap::IndexSet<&DataPath> = self.data.keys().collect();
2118        for reads in show_imports.data_reads.values() {
2119            paths.extend(reads);
2120        }
2121        let words = paths.len().div_ceil(64);
2122        let mut needs: HashMap<&RulePath, Vec<u64>> = HashMap::with_capacity(show_order.len());
2123        for path in show_order {
2124            let mut bits = match self.rules.get_index_of(path) {
2125                Some(rule_pos) => {
2126                    let mut bits = bits_by_rule[rule_pos].clone();
2127                    bits.resize(words, 0);
2128                    bits
2129                }
2130                None => vec![0u64; words],
2131            };
2132            for read in show_imports.data_reads.get(path).into_iter().flatten() {
2133                let position = paths
2134                    .get_index_of(read)
2135                    .expect("BUG: Show data read indexed above");
2136                bits[position / 64] |= 1u64 << (position % 64);
2137            }
2138            let reads = match show_imports.need_edges.get(path) {
2139                Some(edges) => edges,
2140                None => &show_edges[path],
2141            };
2142            for dependency in reads {
2143                let dependency_bits = needs
2144                    .get(dependency)
2145                    .expect("BUG: Show dependency needs computed before its reader");
2146                for (word, dependency_word) in bits.iter_mut().zip(dependency_bits) {
2147                    *word |= *dependency_word;
2148                }
2149            }
2150            needs.insert(path, bits);
2151        }
2152        let mut readers: Vec<(usize, &RulePath)> = self.show_rules.keys().enumerate().collect();
2153        readers.sort_by_cached_key(|(_, path)| path.input_key());
2154        let mut lists: Vec<Vec<u32>> = vec![Vec::new(); paths.len()];
2155        for (rule_pos, path) in readers {
2156            let rule_pos = u32::try_from(rule_pos).expect("BUG: Show rule count exceeds u32");
2157            // `bits_by_rule` covers this plan's data only; import data is never listed.
2158            let listed: &[u64] = match self.rules.get_index_of(path) {
2159                Some(local_pos) => &bits_by_rule[local_pos],
2160                None => &[],
2161            };
2162            for (word_idx, &word) in needs[path].iter().enumerate() {
2163                let mut remaining = word & !listed.get(word_idx).copied().unwrap_or(0);
2164                while remaining != 0 {
2165                    let bit = remaining.trailing_zeros() as usize;
2166                    lists[word_idx * 64 + bit].push(rule_pos);
2167                    remaining &= remaining - 1;
2168                }
2169            }
2170        }
2171        paths
2172            .into_iter()
2173            .zip(lists)
2174            .filter(|(_, list)| !list.is_empty())
2175            .map(|(path, list)| (path.clone(), list))
2176            .collect()
2177    }
2178
2179    /// Per [`Self::data`] position: [`Self::rules`] positions that transitively
2180    /// need that slot (through rule references). Row-field columns are sized and left
2181    /// empty for [`Self::fill_row_field_needs`]. Returns the per-rule data-slot bitsets.
2182    ///
2183    /// Built in plan topological order: at a rule-ref cell, OR the target rule's
2184    /// already-computed bitsets instead of descending Kind (rule references are
2185    /// evaluation boundaries). Walks the normalized body so constant-dead unless
2186    /// arms removed by normalize stay out of the index. Fields read on
2187    /// evaluation-data rows land on the instance-list slot's [`NeededByRules::fields`]
2188    /// column; fields read on spec-written rows are ordinary slots of this plan.
2189    fn build_needed_by_rules(&mut self) -> Vec<Vec<u64>> {
2190        use crate::planning::normalize::{LeafKind, NormalFormKind};
2191
2192        let data_len = self.data.len();
2193        let words = data_len.div_ceil(64);
2194        let mut bits_by_rule: Vec<Vec<u64>> = Vec::with_capacity(self.rules.len());
2195
2196        let mut visited = HashSet::new();
2197        let mut stack = Vec::new();
2198
2199        // Pass 1: data-slot bitsets (topo / rule_ref OR). Evaluation-data row reads ignored here.
2200        for (rule_pos, (_, rule)) in self.rules.iter().enumerate() {
2201            let mut bits = vec![0u64; words];
2202            visited.clear();
2203            stack.push(rule.normal_form);
2204            {
2205                let mut needs = NeedsMarker {
2206                    plan: self,
2207                    rule_pos,
2208                    bits: &mut bits,
2209                    bits_by_rule: &bits_by_rule,
2210                };
2211                for target in &rule.scale_dependencies {
2212                    needs.mark_scale_target(&mut stack, target);
2213                }
2214            }
2215            while let Some(id) = stack.pop() {
2216                if !visited.insert(id) {
2217                    continue;
2218                }
2219                let nf = self.normal_form(id);
2220                let mut needs = NeedsMarker {
2221                    plan: self,
2222                    rule_pos,
2223                    bits: &mut bits,
2224                    bits_by_rule: &bits_by_rule,
2225                };
2226                if let Some(ref_path) = &nf.rule_ref {
2227                    needs.mark(InputNeed::Rule(ref_path.clone()));
2228                    continue;
2229                }
2230                match &nf.kind {
2231                    NormalFormKind::Leaf(LeafKind::DataPath(path)) => {
2232                        // Local slots only. Uses-qualified paths are SpecMember /
2233                        // member-target cells; `data_read_needs` keys `plan.data` by
2234                        // local name and must not see nested import FX scales.
2235                        if path.segments.is_empty() {
2236                            if let Some(need) = self.data_read_needs(path) {
2237                                needs.mark(need);
2238                            }
2239                            if let Some(rule) = self.options_rule_of(path) {
2240                                needs.mark(InputNeed::Rule(rule));
2241                            }
2242                        }
2243                    }
2244                    NormalFormKind::OrderedDispatch { .. } => {
2245                        let origin = nf.origin.unwrap_or_else(|| {
2246                            panic!(
2247                                "BUG: non-rule_ref OrderedDispatch must carry origin (rule '{}')",
2248                                rule.path
2249                            )
2250                        });
2251                        stack.push(origin);
2252                    }
2253                    _ => nf.kind.for_each_child(|child| stack.push(child)),
2254                }
2255            }
2256            bits_by_rule.push(bits);
2257        }
2258
2259        let mut consumers: Vec<usize> = (0..self.rules.len()).collect();
2260        consumers.sort_by(|&a, &b| {
2261            self.rules[a]
2262                .path
2263                .input_key()
2264                .cmp(&self.rules[b].path.input_key())
2265        });
2266
2267        let mut needed_by_rules: Vec<NeededByRules> = self
2268            .data
2269            .iter()
2270            .map(|(path, definition)| {
2271                let fields = match definition {
2272                    DataDefinition::InstanceList { .. } => {
2273                        let identity = &self
2274                            .instance_lists
2275                            .get(path)
2276                            .unwrap_or_else(|| {
2277                                panic!("BUG: InstanceList '{path}' missing from instance_lists")
2278                            })
2279                            .identity;
2280                        vec![Vec::new(); self.row_template(identity).row_field_count()]
2281                    }
2282                    _ => Vec::new(),
2283                };
2284                NeededByRules {
2285                    rules: Vec::new(),
2286                    fields,
2287                }
2288            })
2289            .collect();
2290
2291        for &rule_pos in &consumers {
2292            let rule_id = u32::try_from(rule_pos).expect("BUG: rule count exceeds u32");
2293            let bits = &bits_by_rule[rule_pos];
2294            for (word_idx, &word) in bits.iter().enumerate() {
2295                let mut remaining = word;
2296                while remaining != 0 {
2297                    let bit = remaining.trailing_zeros() as usize;
2298                    let data_pos = word_idx * 64 + bit;
2299                    needed_by_rules[data_pos].rules.push(rule_id);
2300                    remaining &= remaining - 1;
2301                }
2302            }
2303        }
2304        self.needed_by_rules = needed_by_rules;
2305        bits_by_rule
2306    }
2307
2308    /// Fills the row-field columns of [`Self::needed_by_rules`]: which rules need each row
2309    /// field of evaluation-data rows. Runs after [`Self::show_needed_by`] so reads of a
2310    /// self-recursive row see the needs of its `uses` imports.
2311    fn fill_row_field_needs(&mut self, bits_by_rule: &[Vec<u64>]) {
2312        use crate::planning::normalize::{LeafKind, NormalFormKind, NormalFormListOp};
2313
2314        let mut visited = HashSet::new();
2315        let mut stack = Vec::new();
2316        // Evaluation-data row-field bitsets per row spec. Self RowEnd::Rule
2317        // expands via completed bits_by_rule (row rule reads are not topo edges).
2318        let mut row_bits_by_rule: Vec<BTreeMap<SpecIdentity, Vec<u64>>> =
2319            Vec::with_capacity(self.rules.len());
2320        for (rule_pos, (_, rule)) in self.rules.iter().enumerate() {
2321            let mut row_bits: BTreeMap<SpecIdentity, Vec<u64>> = BTreeMap::new();
2322            visited.clear();
2323            stack.push(rule.normal_form);
2324            while let Some(id) = stack.pop() {
2325                if !visited.insert(id) {
2326                    continue;
2327                }
2328                let nf = self.normal_form(id);
2329                if let Some(ref_path) = &nf.rule_ref {
2330                    let ref_pos = self.rules.get_index_of(ref_path).unwrap_or_else(|| {
2331                        panic!(
2332                            "BUG: rule_ref target '{ref_path}' missing from plan.rules (rule '{}')",
2333                            rule.path
2334                        )
2335                    });
2336                    if ref_pos >= rule_pos {
2337                        panic!(
2338                            "BUG: rule_ref target '{ref_path}' must precede '{}' in topo order",
2339                            rule.path
2340                        );
2341                    }
2342                    for (identity, dep_row) in &row_bits_by_rule[ref_pos] {
2343                        let entry = row_bits_entry(self, &mut row_bits, identity);
2344                        for (word, dep_word) in entry.iter_mut().zip(dep_row.iter()) {
2345                            *word |= *dep_word;
2346                        }
2347                    }
2348                    continue;
2349                }
2350                match &nf.kind {
2351                    NormalFormKind::RowField { identity, end, .. } => {
2352                        if self.has_row_template(identity) {
2353                            mark_row_end_needs(
2354                                self,
2355                                identity,
2356                                std::slice::from_ref(end),
2357                                bits_by_rule,
2358                                &mut row_bits,
2359                                &rule.path,
2360                            );
2361                        }
2362                        nf.kind.for_each_child(|child| stack.push(child));
2363                    }
2364                    NormalFormKind::SpecMember { base, row, .. } => {
2365                        let on_import_slot = match &self.normal_form(*base).kind {
2366                            NormalFormKind::Leaf(LeafKind::DataPath(path)) => {
2367                                matches!(self.data.get(path), Some(DataDefinition::Import { .. }))
2368                            }
2369                            _ => false,
2370                        };
2371                        if !on_import_slot {
2372                            let identity = match &self.result_type(*base).specifications {
2373                                TypeSpecification::Spec { spec } => spec,
2374                                other => panic!(
2375                                    "BUG: SpecMember row read on non-spec type {other:?} (rule '{}')",
2376                                    rule.path
2377                                ),
2378                            };
2379                            mark_row_end_needs(
2380                                self,
2381                                identity,
2382                                &row.ends,
2383                                bits_by_rule,
2384                                &mut row_bits,
2385                                &rule.path,
2386                            );
2387                        }
2388                        nf.kind.for_each_child(|child| stack.push(child));
2389                    }
2390                    NormalFormKind::ListCommand {
2391                        list,
2392                        op: NormalFormListOp::Find | NormalFormListOp::WithRow { .. },
2393                        row_level: Some(_),
2394                        ..
2395                    } => {
2396                        mark_list_key_fields(self, *list, &mut row_bits, &rule.path);
2397                        nf.kind.for_each_child(|child| stack.push(child));
2398                    }
2399                    NormalFormKind::OrderedDispatch { .. } => {
2400                        let origin = nf.origin.unwrap_or_else(|| {
2401                            panic!(
2402                                "BUG: non-rule_ref OrderedDispatch must carry origin (rule '{}')",
2403                                rule.path
2404                            )
2405                        });
2406                        stack.push(origin);
2407                    }
2408                    _ => nf.kind.for_each_child(|child| stack.push(child)),
2409                }
2410            }
2411            row_bits_by_rule.push(row_bits);
2412        }
2413
2414        let mut consumers: Vec<usize> = (0..self.rules.len()).collect();
2415        consumers.sort_by(|&a, &b| {
2416            self.rules[a]
2417                .path
2418                .input_key()
2419                .cmp(&self.rules[b].path.input_key())
2420        });
2421        let mut row_fields: Vec<(usize, usize, u32)> = Vec::new();
2422        for &rule_pos in &consumers {
2423            let rule_id = u32::try_from(rule_pos).expect("BUG: rule count exceeds u32");
2424            for (identity, row_bits) in &row_bits_by_rule[rule_pos] {
2425                let list_positions = self.instance_list_data_positions(identity);
2426                let template_len = self.row_template(identity).row_field_count();
2427                for (word_idx, &word) in row_bits.iter().enumerate() {
2428                    let mut remaining = word;
2429                    while remaining != 0 {
2430                        let bit = remaining.trailing_zeros() as usize;
2431                        let field_pos = word_idx * 64 + bit;
2432                        assert!(
2433                            field_pos < template_len,
2434                            "BUG: row field bit {field_pos} past the row fields of '{identity}'"
2435                        );
2436                        for &list_pos in &list_positions {
2437                            row_fields.push((list_pos, field_pos, rule_id));
2438                        }
2439                        remaining &= remaining - 1;
2440                    }
2441                }
2442            }
2443        }
2444        for (list_pos, field_pos, rule_id) in row_fields {
2445            self.needed_by_rules[list_pos].fields[field_pos].push(rule_id);
2446        }
2447    }
2448
2449    /// Position of `rule` in [`Self::show_rules`].
2450    fn show_rule_position(&self, rule: &RulePath) -> Option<u32> {
2451        self.show_rules
2452            .get_index_of(rule)
2453            .map(|position| u32::try_from(position).expect("BUG: Show rule count exceeds u32"))
2454    }
2455
2456    /// Row fields of rows of this spec: its data, then the data of its `uses` imports
2457    /// Show reads ([`Self::show_import_data`]).
2458    pub(crate) fn row_field_count(&self) -> usize {
2459        self.data.len() + self.show_import_data.len()
2460    }
2461
2462    /// Position of row field `input_key` among [`Self::row_field_count`].
2463    fn row_field_position(&self, input_key: &str) -> Option<usize> {
2464        if let Some(path) = self.input_key_index.get(input_key) {
2465            return self.data.get_index_of(path);
2466        }
2467        self.show_import_data
2468            .keys()
2469            .position(|path| path.input_key() == input_key)
2470            .map(|index| self.data.len() + index)
2471    }
2472
2473    /// Whether rows of `identity` can come from evaluation data on this plan: it is this
2474    /// spec or a row spec of one of its data lists.
2475    fn has_row_template(&self, identity: &SpecIdentity) -> bool {
2476        *identity == self.identity || self.row_templates.contains_key(identity)
2477    }
2478
2479    /// [`Self::data`] positions of instance lists whose element identity is `identity`.
2480    fn instance_list_data_positions(&self, identity: &SpecIdentity) -> Vec<usize> {
2481        self.instance_lists
2482            .iter()
2483            .filter(|(_, source)| source.identity == *identity)
2484            .map(|(path, _)| {
2485                self.data
2486                    .get_index_of(path)
2487                    .unwrap_or_else(|| panic!("BUG: instance list '{path}' absent from plan.data"))
2488            })
2489            .collect()
2490    }
2491
2492    /// Union of [`NeededByRules::fields`] across instance lists of `identity`
2493    /// (alphabetical rule-name order already on each list column).
2494    pub(crate) fn row_field_needed_by_rules(
2495        &self,
2496        identity: &SpecIdentity,
2497        field_input_key: &str,
2498    ) -> Vec<u32> {
2499        let field_pos = self
2500            .row_template(identity)
2501            .row_field_position(field_input_key)
2502            .unwrap_or_else(|| {
2503                panic!("BUG: row field '{field_input_key}' is not a row field of '{identity}'")
2504            });
2505        let mut seen = HashSet::new();
2506        let mut union = Vec::new();
2507        for list_pos in self.instance_list_data_positions(identity) {
2508            let fields = &self.needed_by_rules[list_pos].fields;
2509            let consumers = fields.get(field_pos).unwrap_or_else(|| {
2510                panic!(
2511                    "BUG: needed_by_rules.fields len {} < field {field_pos} for list data pos {list_pos}",
2512                    fields.len()
2513                )
2514            });
2515            for &rule_id in consumers {
2516                if seen.insert(rule_id) {
2517                    union.push(rule_id);
2518                }
2519            }
2520        }
2521        union.sort_by(|&a, &b| {
2522            self.rules
2523                .get_index(a as usize)
2524                .expect("BUG: needed_by_rules rule id out of range")
2525                .1
2526                .path
2527                .input_key()
2528                .cmp(
2529                    &self
2530                        .rules
2531                        .get_index(b as usize)
2532                        .expect("BUG: needed_by_rules rule id out of range")
2533                        .1
2534                        .path
2535                        .input_key(),
2536                )
2537        });
2538        union
2539    }
2540
2541    /// Promptable [`DataPath`] for a normal-form data leaf, following reference
2542    /// chain ends for references. `None` when the leaf is a rule-target, import
2543    /// marker, instance list, or a slot bound by the spec (not caller-promptable).
2544    ///
2545    /// Reference paths must appear in [`Self::reference_ends`].
2546    pub(crate) fn promptable_data_path<'a>(&'a self, path: &'a DataPath) -> Option<&'a DataPath> {
2547        let target = match self.data.get(path).unwrap_or_else(|| {
2548            panic!("BUG: normal-form DataPath leaf absent from plan.data: {path}")
2549        }) {
2550            DataDefinition::Value { .. } | DataDefinition::TypeDeclaration { .. } => path,
2551            DataDefinition::Reference { .. } => match self.reference_ends.get(path) {
2552                Some(ReferenceEnd::Promptable(target)) => target,
2553                Some(ReferenceEnd::Rule(_)) => return None,
2554                None => {
2555                    panic!("BUG: reference '{path}' missing from reference_ends after planning")
2556                }
2557            },
2558            DataDefinition::Import { .. } | DataDefinition::InstanceList { .. } => return None,
2559        };
2560        match self
2561            .data
2562            .get(target)
2563            .unwrap_or_else(|| panic!("BUG: reference end '{target}' absent from plan.data"))
2564        {
2565            DataDefinition::Value { .. } | DataDefinition::TypeDeclaration { .. }
2566                if !self.data[target].is_bound() =>
2567            {
2568                Some(target)
2569            }
2570            DataDefinition::Value { .. } | DataDefinition::TypeDeclaration { .. } => None,
2571            DataDefinition::InstanceList { .. } => None,
2572            DataDefinition::Reference { .. } | DataDefinition::Import { .. } => {
2573                panic!("BUG: reference end '{target}' is not a slot")
2574            }
2575        }
2576    }
2577
2578    /// What reading `path` needs: the slot itself, the end of its reference chain, or
2579    /// the rule a reference chain ends at. An import marker needs nothing (its value
2580    /// is the instance).
2581    fn data_read_needs(&self, path: &DataPath) -> Option<InputNeed> {
2582        match self
2583            .data
2584            .get(path)
2585            .unwrap_or_else(|| panic!("BUG: data '{path}' missing from plan '{}'", self.spec_name))
2586        {
2587            DataDefinition::Value { .. }
2588            | DataDefinition::TypeDeclaration { .. }
2589            | DataDefinition::InstanceList { .. } => Some(InputNeed::Data(path.clone())),
2590            DataDefinition::Import { .. } => None,
2591            DataDefinition::Reference { .. } => {
2592                match self.reference_ends.get(path).unwrap_or_else(|| {
2593                    panic!("BUG: reference '{path}' missing from reference_ends")
2594                }) {
2595                    ReferenceEnd::Promptable(target) => Some(InputNeed::Data(target.clone())),
2596                    ReferenceEnd::Rule(rule) => Some(InputNeed::Rule(rule.clone())),
2597                }
2598            }
2599        }
2600    }
2601
2602    /// Options rule of data `path`, when its type names one ([`options_rule_path`]).
2603    pub(crate) fn options_rule_of(&self, path: &DataPath) -> Option<RulePath> {
2604        let lemma_type = self.data.get(path)?.schema_type()?;
2605        let rule = options_rule_path(path, lemma_type)?;
2606        assert!(
2607            self.rules.contains_key(&rule),
2608            "BUG: options rule '{rule}' missing after planning validated it"
2609        );
2610        Some(rule)
2611    }
2612
2613    /// Validate caller-requested rule names and return resolved [`RulePath`]s in
2614    /// [`Self::show_rules`] order, each once.
2615    ///
2616    /// `None` means all local rules (this spec's outputs). `Some(&[])` is an error.
2617    /// Unknown names in `Some` slice error. Explicit names are this spec's Show.rules
2618    /// keys only (empty path).
2619    pub fn validated_response_rule_paths(
2620        &self,
2621        rules: Option<&[String]>,
2622    ) -> Result<Vec<&RulePath>, Error> {
2623        let show_position = |path: &RulePath| {
2624            self.show_rules
2625                .get_index_of(path)
2626                .unwrap_or_else(|| panic!("BUG: response rule '{path}' missing from show_rules"))
2627        };
2628        let mut positions: Vec<usize> = match rules {
2629            None => self.rules.keys().map(show_position).collect(),
2630            Some([]) => {
2631                return Err(Error::request(
2632                    "at least one rule required".to_string(),
2633                    None::<String>,
2634                ));
2635            }
2636            Some(rules) => {
2637                let mut positions = Vec::with_capacity(rules.len());
2638                for rule_name in rules {
2639                    let path = self
2640                        .show_rule_index
2641                        .get(canonical_logical_name(rule_name).as_ref())
2642                        .ok_or_else(|| {
2643                            Error::request(
2644                                format!(
2645                                    "Rule '{rule_name}' not found in spec '{}'",
2646                                    self.spec_name
2647                                ),
2648                                None::<String>,
2649                            )
2650                        })?;
2651                    positions.push(show_position(path));
2652                }
2653                positions
2654            }
2655        };
2656        positions.sort_unstable();
2657        positions.dedup();
2658        Ok(positions
2659            .into_iter()
2660            .map(|position| {
2661                self.show_rules
2662                    .get_index(position)
2663                    .expect("BUG: show_rules position taken from show_rules")
2664                    .0
2665            })
2666            .collect())
2667    }
2668
2669    /// Look up a Show.rules key (`RulePath::input_key`) among this spec's local rules.
2670    pub fn get_rule(&self, name: &str) -> Option<&ExecutableRule> {
2671        let canonical = canonical_logical_name(name);
2672        let path = self.show_rule_index.get(canonical.as_ref())?;
2673        self.rules.get(path)
2674    }
2675
2676    /// Look up a normal-form cell by id.
2677    pub fn normal_form(&self, id: NormalFormId) -> &NormalForm {
2678        self.normal_forms.get(id.index()).unwrap_or_else(|| {
2679            panic!(
2680                "BUG: NormalFormId {} out of range (table len {})",
2681                id.index(),
2682                self.normal_forms.len()
2683            )
2684        })
2685    }
2686
2687    /// Stamped result type of the cell at `id`.
2688    pub(crate) fn result_type(&self, id: NormalFormId) -> &Arc<LemmaType> {
2689        &self.normal_form(id).result_type
2690    }
2691
2692    /// Position of `path` in [`Self::rules`]. Every rule path a plan cell
2693    /// references was planned, so absence is a bug.
2694    pub(crate) fn rule_index(&self, path: &RulePath) -> RuleIndex {
2695        RuleIndex(
2696            self.rules
2697                .get_index_of(path)
2698                .unwrap_or_else(|| panic!("BUG: rule '{}' missing from execution plan", path.rule)),
2699        )
2700    }
2701
2702    pub(crate) fn rule_at(&self, index: RuleIndex) -> &ExecutableRule {
2703        self.rules
2704            .get_index(index.0)
2705            .map(|(_, rule)| rule)
2706            .unwrap_or_else(|| {
2707                panic!(
2708                    "BUG: rule index {} out of plan.rules range ({} rules)",
2709                    index.0,
2710                    self.rules.len()
2711                )
2712            })
2713    }
2714
2715    fn compute_initial_values(&self) -> Vec<Option<OperationResult>> {
2716        let mut values: Vec<Option<OperationResult>> = vec![None; self.normal_forms.len()];
2717        for (path, definition) in &self.data {
2718            let leaf = *self
2719                .data_leaf
2720                .get(path)
2721                .expect("BUG: every plan.data path must have a data_leaf entry");
2722            if let Some(fill) = definition.bound_value() {
2723                values[leaf.index()] = Some(OperationResult::from_bound(fill.clone()));
2724            } else if let Some(literal) = definition.value() {
2725                let unit = definition
2726                    .schema_type()
2727                    .and_then(|schema| schema.unit().map(Arc::from));
2728                values[leaf.index()] = Some(OperationResult::from_bound(BoundValueKind {
2729                    value: literal.value,
2730                    unit,
2731                    instance_factors: None,
2732                }));
2733            }
2734        }
2735        self.copy_reference_targets(&mut values, |_, _| false);
2736        values
2737    }
2738
2739    /// Copy each data-target reference's target value into its leaf, in
2740    /// `data_reference_order`, checked against the reference's declared type. A
2741    /// reference bound by the caller (`is_bound` of its data slot and path) or
2742    /// holding its own value keeps it.
2743    pub(crate) fn copy_reference_targets(
2744        &self,
2745        values: &mut [Option<OperationResult>],
2746        is_bound: impl Fn(usize, &DataPath) -> bool,
2747    ) {
2748        for reference_path in &self.data_reference_order {
2749            let Some((
2750                slot,
2751                _,
2752                DataDefinition::Reference {
2753                    target: ReferenceTarget::Data(target_path),
2754                    resolved_type,
2755                    local_value,
2756                    ..
2757                },
2758            )) = self.data.get_full(reference_path)
2759            else {
2760                unreachable!("BUG: data_reference_order holds only data-target references");
2761            };
2762            if local_value.is_some() || is_bound(slot, reference_path) {
2763                continue;
2764            }
2765            let leaf = self.data_slot_leaves[slot];
2766            let target_leaf = *self
2767                .data_leaf
2768                .get(target_path)
2769                .expect("BUG: reference target missing from data_leaf");
2770            values[leaf.index()] =
2771                match &values[target_leaf.index()] {
2772                    None => None,
2773                    Some(OperationResult::Veto(veto)) => Some(OperationResult::Veto(veto.clone())),
2774                    Some(OperationResult::Value(value)) if value.value.has_unscaled_measure() => {
2775                        Some(OperationResult::from_bound(value.clone()))
2776                    }
2777                    Some(OperationResult::Value(value)) => Some(match validate_value_against_type(
2778                        resolved_type.as_ref(),
2779                        &value.to_literal(),
2780                        &self.resolved_types.unit_index,
2781                        None,
2782                    ) {
2783                        Ok(()) => OperationResult::from_bound(value.clone()),
2784                        Err(msg) => OperationResult::Veto(VetoType::computation(format!(
2785                            "Reference '{reference_path}' violates its declared constraint: {msg}"
2786                        ))),
2787                    }),
2788                };
2789        }
2790    }
2791
2792    pub(crate) fn populate_evaluation_tables(&mut self) {
2793        self.data_slot_leaves = self
2794            .data
2795            .keys()
2796            .map(|path| {
2797                *self
2798                    .data_leaf
2799                    .get(path)
2800                    .expect("BUG: every plan.data path must have a data_leaf entry")
2801            })
2802            .collect();
2803        self.initial_values = self.compute_initial_values();
2804        self.rule_ref_indexes = self
2805            .normal_forms
2806            .iter()
2807            .map(|cell| cell.rule_ref.as_ref().map(|path| self.rule_index(path)))
2808            .collect();
2809        self.walk_settled_leaves = self
2810            .data
2811            .iter()
2812            .filter_map(|(path, definition)| {
2813                let options = definition
2814                    .schema_type()
2815                    .is_some_and(|schema| schema.needs_runtime_option_check());
2816                let unscaled = definition
2817                    .value()
2818                    .is_some_and(|literal| literal.value.has_unscaled_measure())
2819                    || definition
2820                        .bound_value()
2821                        .is_some_and(|bound| bound.value.has_unscaled_measure())
2822                    || definition
2823                        .schema_type()
2824                        .is_some_and(LemmaType::has_dynamic_units);
2825                if options || unscaled {
2826                    Some(
2827                        *self
2828                            .data_leaf
2829                            .get(path)
2830                            .expect("BUG: every plan.data path must have a data_leaf entry"),
2831                    )
2832                } else {
2833                    None
2834                }
2835            })
2836            .collect();
2837    }
2838}
2839
2840/// Follow an [`NormalFormKind::OrderedDispatch`] region to the origin arm body it selects,
2841/// using filled slots: a nested dispatch by its scrutinee slot, a conjunctive region by
2842/// its residual arms. Stops at rule-reference cells: those borrow another rule's Kind and
2843/// must compare as the origin arm body, not the body that rule would itself select.
2844/// `None` when a slot the walk reads is unset or vetoed (not decided yet).
2845pub(crate) fn resolve_nested_dispatch_body(
2846    plan: &ExecutionPlan,
2847    values: &[Option<OperationResult>],
2848    mut region: DispatchRegion,
2849) -> Option<NormalFormId> {
2850    use crate::computation::OperationResult;
2851    use crate::planning::normalize::NormalFormKind;
2852    use crate::planning::ordered_dispatch::region_of_scrutinee;
2853
2854    for _ in 0..=plan.normal_forms.len() {
2855        let body = match region {
2856            DispatchRegion::Body(body) => body,
2857            DispatchRegion::Conjunctive(group) => {
2858                region = DispatchRegion::Body(conjunctive_winner_body(plan, values, group)?);
2859                continue;
2860            }
2861        };
2862        let cell = plan.normal_form(body);
2863        if cell.rule_ref.is_some() {
2864            return Some(body);
2865        }
2866        let (scrutinee, boundaries, regions) = match &cell.kind {
2867            NormalFormKind::OrderedDispatch {
2868                scrutinee,
2869                boundaries,
2870                regions,
2871            } => (scrutinee, boundaries, regions),
2872            NormalFormKind::Piecewise(_) => {
2873                panic!("BUG: single-scrutinee region body is a Piecewise, not a conjunctive group")
2874            }
2875            NormalFormKind::Leaf(_)
2876            | NormalFormKind::RowField { .. }
2877            | NormalFormKind::Sum(_)
2878            | NormalFormKind::Product(_)
2879            | NormalFormKind::Subtract(_, _)
2880            | NormalFormKind::Divide(_, _)
2881            | NormalFormKind::Power(_, _)
2882            | NormalFormKind::Modulo(_, _)
2883            | NormalFormKind::Negate(_)
2884            | NormalFormKind::Reciprocal(_)
2885            | NormalFormKind::Comparison(_, _, _)
2886            | NormalFormKind::Logical(_, _)
2887            | NormalFormKind::Not(_)
2888            | NormalFormKind::MathOp(_, _)
2889            | NormalFormKind::StepRounding(_, _, _)
2890            | NormalFormKind::UnitConversion(_, _)
2891            | NormalFormKind::Veto(_)
2892            | NormalFormKind::DateRelative(_, _)
2893            | NormalFormKind::DateCalendar(_, _, _)
2894            | NormalFormKind::RangeLiteral(_, _)
2895            | NormalFormKind::PastFutureRange(_, _)
2896            | NormalFormKind::RangeContainment(_, _)
2897            | NormalFormKind::RangeBound(_, _)
2898            | NormalFormKind::ResultIsVeto(_)
2899            | NormalFormKind::Now
2900            | NormalFormKind::SpecMember { .. }
2901            | NormalFormKind::ListCommand { .. } => return Some(body),
2902        };
2903        let literal = match values.get(scrutinee.index()).and_then(|slot| slot.as_ref()) {
2904            Some(OperationResult::Value(literal)) => literal,
2905            Some(OperationResult::Veto(_)) | None => return None,
2906        };
2907        let index = region_of_scrutinee(boundaries, &literal.value).ok()?;
2908        region = regions[index];
2909    }
2910    panic!("BUG: nested OrderedDispatch chain longer than the normal-form table");
2911}
2912
2913/// Origin arm body a conjunctive region's residual arms select, scanned like the origin
2914/// Piecewise. `None` while a residual condition is unset or vetoed. A group that
2915/// normalization rewrote (into a dispatch or a collapsed value) is itself the body.
2916fn conjunctive_winner_body(
2917    plan: &ExecutionPlan,
2918    values: &[Option<OperationResult>],
2919    group: NormalFormId,
2920) -> Option<NormalFormId> {
2921    use crate::evaluation::branch_semantics::{
2922        condition_outcome, piecewise_decision, PiecewiseDecision,
2923    };
2924    use crate::planning::normalize::NormalFormKind;
2925
2926    let NormalFormKind::Piecewise(arms) = &plan.normal_form(group).kind else {
2927        return Some(group);
2928    };
2929    let outcome = |arm: usize| {
2930        values
2931            .get(arms[arm].0.index())
2932            .and_then(|slot| slot.as_ref())
2933            .map(condition_outcome)
2934    };
2935    match piecewise_decision(arms.len(), outcome) {
2936        PiecewiseDecision::Taken { arm } => Some(arms[arm].1),
2937        PiecewiseDecision::Default => Some(arms[0].1),
2938        PiecewiseDecision::Undecided { .. } => None,
2939    }
2940}
2941
2942/// DataPath leaves reachable from `root` in evaluator decision-tree preorder.
2943///
2944/// Control liveness is derived from `values` (the evaluation value table), matching
2945/// the evaluator's own decisions:
2946/// - [`NormalFormKind::Logical`]: an absorbing operand decides the connective and is
2947///   the only live operand. Otherwise every operand not settled at the identity is live.
2948/// - [`NormalFormKind::Piecewise`]: arms decided from filled condition slots, high to
2949///   low. An unfilled or vetoed condition before a winner is found means no decision:
2950///   every arm stays live.
2951/// - [`NormalFormKind::OrderedDispatch`]: only the region for the scrutinee slot. An
2952///   unfilled scrutinee means every origin arm stays live.
2953///
2954/// Every cell the evaluator visits owns its slot, including rule reference cells
2955/// (they hold the referenced rule's value).
2956///
2957/// Order matches the evaluator walk (`tree.rs`): first-seen [`DataPath`] wins
2958/// (rule references may share a leaf under distinct cell ids). Iterative DFS with
2959/// pop-time visited; children pushed in reverse evaluation order. Instance-list
2960/// commands and row member reads appear as [`ReachableInput::RowReads`]: their
2961/// missing row fields are recorded during evaluation, not in the plan.
2962pub(crate) fn reachable_data_paths(
2963    plan: &ExecutionPlan,
2964    root: NormalFormId,
2965    values: &[Option<OperationResult>],
2966) -> Vec<ReachableInput> {
2967    use crate::computation::OperationResult;
2968    use crate::evaluation::branch_semantics::{
2969        condition_outcome, piecewise_decision, BranchOutcome, PiecewiseDecision,
2970    };
2971    use crate::planning::normalize::LeafKind;
2972    use crate::planning::normalize::NormalFormKind;
2973    use crate::planning::ordered_dispatch::region_of_scrutinee;
2974
2975    fn slot_outcome(values: &[Option<OperationResult>], id: NormalFormId) -> Option<BranchOutcome> {
2976        values
2977            .get(id.index())
2978            .and_then(|slot| slot.as_ref())
2979            .map(condition_outcome)
2980    }
2981
2982    /// Piecewise pre-image of a dispatch cell.
2983    ///
2984    /// A cell that borrows a rule's Kind (`rule_ref`) records no `origin` of its
2985    /// own, and the rule it names can itself be a bare reference to a further
2986    /// rule (`rule alias: country`), so the fold can sit any number of
2987    /// references away. Every hop moves to a rule earlier in topological order,
2988    /// so the chain is shorter than the rule table.
2989    fn dispatch_origin(plan: &ExecutionPlan, cell: &NormalForm) -> NormalFormId {
2990        let mut current = cell;
2991        for _ in 0..=plan.rules.len() {
2992            if let Some(origin) = current.origin {
2993                return origin;
2994            }
2995            let ref_path = current.rule_ref.as_ref().unwrap_or_else(|| {
2996                panic!("BUG: OrderedDispatch without origin must carry rule_ref")
2997            });
2998            let body = plan
2999                .rules
3000                .get(ref_path)
3001                .unwrap_or_else(|| {
3002                    panic!("BUG: rule reference '{ref_path}' missing from plan.rules")
3003                })
3004                .normal_form;
3005            current = plan.normal_form(body);
3006        }
3007        panic!("BUG: rule reference chain over a dispatch cell does not terminate");
3008    }
3009
3010    /// Piecewise children the evaluator visits under `decision`, in evaluation
3011    /// order: `cond_n, body_n, …, cond_1, body_1, default`.
3012    fn live_piecewise_children(
3013        arms: &[(NormalFormId, NormalFormId)],
3014        decision: PiecewiseDecision,
3015    ) -> Vec<NormalFormId> {
3016        let mut live = Vec::new();
3017        match decision {
3018            PiecewiseDecision::Taken { arm } => {
3019                for (condition, _) in arms.iter().skip(arm + 1).rev() {
3020                    live.push(*condition);
3021                }
3022                live.push(arms[arm].0);
3023                live.push(arms[arm].1);
3024            }
3025            PiecewiseDecision::Default => {
3026                for (condition, _) in arms.iter().skip(1).rev() {
3027                    live.push(*condition);
3028                }
3029                live.push(arms[0].1);
3030            }
3031            PiecewiseDecision::Undecided { arm } => {
3032                for (condition, _) in arms.iter().skip(arm + 1).rev() {
3033                    live.push(*condition);
3034                }
3035                for (condition, body) in arms.iter().take(arm + 1).skip(1).rev() {
3036                    live.push(*condition);
3037                    live.push(*body);
3038                }
3039                live.push(arms[0].1);
3040            }
3041        }
3042        live
3043    }
3044
3045    let mut out = Vec::new();
3046    let mut seen_paths = HashSet::new();
3047    let mut visited = HashSet::new();
3048    let mut stack = vec![root];
3049
3050    while let Some(id) = stack.pop() {
3051        if !visited.insert(id) {
3052            continue;
3053        }
3054        let nf = plan.normal_form(id);
3055        let live: Vec<NormalFormId> = match &nf.kind {
3056            // A slot bound to a rule has that rule's inputs.
3057            NormalFormKind::Leaf(LeafKind::DataPath(path)) => match plan.reference_ends.get(path) {
3058                Some(ReferenceEnd::Rule(rule)) => vec![
3059                    plan.rules
3060                        .get(rule)
3061                        .unwrap_or_else(|| {
3062                            panic!("BUG: rule-bound slot '{path}' names missing rule '{rule}'")
3063                        })
3064                        .normal_form,
3065                ],
3066                Some(ReferenceEnd::Promptable(_)) | None => {
3067                    if seen_paths.insert(path.clone()) {
3068                        out.push(ReachableInput::Data(path.clone()));
3069                    }
3070                    Vec::new()
3071                }
3072            },
3073            NormalFormKind::RowField { .. } => Vec::new(),
3074            NormalFormKind::Leaf(LeafKind::RowValue { .. }) => Vec::new(),
3075            NormalFormKind::ListCommand { row_level, .. } => {
3076                if row_level.is_some() {
3077                    out.push(ReachableInput::RowReads(id));
3078                }
3079                nf.kind.children()
3080            }
3081            NormalFormKind::Logical(connective, children) => {
3082                let absorbing = connective.absorbing_operand();
3083                match children.iter().copied().find(|&child| {
3084                    matches!(
3085                        (absorbing, slot_outcome(values, child)),
3086                        (true, Some(BranchOutcome::Taken)) | (false, Some(BranchOutcome::NotTaken))
3087                    )
3088                }) {
3089                    Some(decider) => vec![decider],
3090                    None => children
3091                        .iter()
3092                        .copied()
3093                        .filter(|&child| {
3094                            !matches!(
3095                                (absorbing, slot_outcome(values, child)),
3096                                (true, Some(BranchOutcome::NotTaken))
3097                                    | (false, Some(BranchOutcome::Taken))
3098                            )
3099                        })
3100                        .collect(),
3101                }
3102            }
3103            NormalFormKind::Piecewise(arms) => {
3104                let decision =
3105                    piecewise_decision(arms.len(), |arm| slot_outcome(values, arms[arm].0));
3106                live_piecewise_children(arms, decision)
3107            }
3108            NormalFormKind::OrderedDispatch {
3109                scrutinee,
3110                boundaries,
3111                regions,
3112            } => {
3113                let origin_id = dispatch_origin(plan, nf);
3114                let NormalFormKind::Piecewise(arms) = &plan.normal_form(origin_id).kind else {
3115                    panic!("BUG: OrderedDispatch origin must be Piecewise");
3116                };
3117                visited.insert(origin_id);
3118
3119                match values.get(scrutinee.index()).and_then(|slot| slot.as_ref()) {
3120                    Some(OperationResult::Value(literal)) => {
3121                        match region_of_scrutinee(boundaries, &literal.value) {
3122                            Ok(region) => vec![*scrutinee, regions[region].cell()],
3123                            Err(_) => vec![*scrutinee],
3124                        }
3125                    }
3126                    // Definitive veto settles the rule; gated bodies cannot un-veto.
3127                    Some(result) if !result.is_missing_data() => vec![*scrutinee],
3128                    // Open scrutinee (unbound DataPath leave-None, or expression
3129                    // MissingData stored on the cell): keep every origin arm live.
3130                    None | Some(_) => {
3131                        let mut live = vec![*scrutinee];
3132                        live.extend(live_piecewise_children(
3133                            arms,
3134                            PiecewiseDecision::Undecided {
3135                                arm: arms.len() - 1,
3136                            },
3137                        ));
3138                        live
3139                    }
3140                }
3141            }
3142            NormalFormKind::SpecMember { base, .. } => {
3143                out.push(ReachableInput::RowReads(id));
3144                vec![*base]
3145            }
3146            NormalFormKind::Leaf(LeafKind::Literal(_))
3147            | NormalFormKind::Now
3148            | NormalFormKind::Veto(_)
3149            | NormalFormKind::Sum(_)
3150            | NormalFormKind::Product(_)
3151            | NormalFormKind::Subtract(_, _)
3152            | NormalFormKind::Divide(_, _)
3153            | NormalFormKind::Power(_, _)
3154            | NormalFormKind::Modulo(_, _)
3155            | NormalFormKind::Comparison(_, _, _)
3156            | NormalFormKind::RangeLiteral(_, _)
3157            | NormalFormKind::RangeContainment(_, _)
3158            | NormalFormKind::StepRounding(_, _, _)
3159            | NormalFormKind::Negate(_)
3160            | NormalFormKind::Reciprocal(_)
3161            | NormalFormKind::Not(_)
3162            | NormalFormKind::MathOp(_, _)
3163            | NormalFormKind::UnitConversion(_, _)
3164            | NormalFormKind::DateRelative(_, _)
3165            | NormalFormKind::DateCalendar(_, _, _)
3166            | NormalFormKind::PastFutureRange(_, _)
3167            | NormalFormKind::RangeBound(_, _)
3168            | NormalFormKind::ResultIsVeto(_) => nf.kind.children(),
3169        };
3170        stack.extend(live.into_iter().rev());
3171    }
3172    out
3173}
3174
3175/// One input [`reachable_data_paths`] found, in evaluation order.
3176pub(crate) enum ReachableInput {
3177    Data(DataPath),
3178    /// A cell that reads list rows; its missing row fields are in the request state.
3179    RowReads(NormalFormId),
3180}
3181
3182pub(crate) fn validate_value_against_type(
3183    expected_type: &LemmaType,
3184    value: &LiteralValue,
3185    unit_index: &UnitIndex,
3186    runtime_scales: Option<
3187        &std::collections::HashMap<String, crate::computation::rational::RationalInteger>,
3188    >,
3189) -> Result<(), String> {
3190    use std::borrow::Cow;
3191
3192    let mut pending = vec![(expected_type, Cow::Borrowed(value))];
3193    while let Some((expected, value)) = pending.pop() {
3194        match (&expected.specifications, &value.value) {
3195            (TypeSpecification::List { element, .. }, ValueKind::List(rows)) => {
3196                for row in rows.iter().rev() {
3197                    pending.push((element.as_ref(), Cow::Owned(row.to_literal())));
3198                }
3199            }
3200            (TypeSpecification::Measure { .. }, ValueKind::UnscaledMeasure { .. }) => {}
3201            (TypeSpecification::MeasureRange { .. }, ValueKind::Range(left, right))
3202                if matches!(
3203                    (&left.value, &right.value),
3204                    (
3205                        ValueKind::UnscaledMeasure { .. },
3206                        ValueKind::UnscaledMeasure { .. }
3207                    ) | (ValueKind::UnscaledMeasure { .. }, ValueKind::Measure(_))
3208                        | (ValueKind::Measure(_), ValueKind::UnscaledMeasure { .. })
3209                ) => {}
3210            (
3211                range_spec @ (TypeSpecification::NumberRange { .. }
3212                | TypeSpecification::DateRange { .. }
3213                | TypeSpecification::TimeRange { .. }
3214                | TypeSpecification::MeasureRange { .. }
3215                | TypeSpecification::RatioRange { .. }),
3216                ValueKind::Range(left, right),
3217            ) => validate_range_literal(
3218                expected,
3219                range_spec,
3220                left.as_ref(),
3221                right.as_ref(),
3222                unit_index,
3223            )?,
3224            _ => {
3225                validate_scalar_against_type(expected, &value, runtime_scales)?;
3226                expected.reject_disallowed(&value.value, runtime_scales)?;
3227            }
3228        }
3229    }
3230    Ok(())
3231}
3232
3233fn validate_scalar_against_type(
3234    expected_type: &LemmaType,
3235    value: &LiteralValue,
3236    runtime_scales: Option<
3237        &std::collections::HashMap<String, crate::computation::rational::RationalInteger>,
3238    >,
3239) -> Result<(), String> {
3240    use crate::computation::bigint::BigInt;
3241    use crate::computation::rational::{
3242        checked_mul, rational_new, try_pow_integer, RationalInteger,
3243    };
3244
3245    fn exceeds_decimal_places(magnitude: &RationalInteger, max_decimals: u8) -> bool {
3246        let scale = match try_pow_integer(
3247            &rational_new(10, 1),
3248            &BigInt::from_i64(i64::from(max_decimals)),
3249        ) {
3250            Ok(value) => value,
3251            Err(_) => return true,
3252        };
3253        let scaled = match checked_mul(magnitude, &scale) {
3254            Ok(value) => value,
3255            Err(_) => return true,
3256        };
3257        match RationalInteger::try_reduce_ref(&scaled) {
3258            Ok(reduced) => !reduced.is_integer(),
3259            Err(_) => true,
3260        }
3261    }
3262
3263    fn format_rational_for_validation_message(
3264        expected_type: &LemmaType,
3265        magnitude: &RationalInteger,
3266    ) -> String {
3267        expected_type
3268            .try_rational_as_decimal(magnitude)
3269            .map(|decimal| decimal.to_string())
3270            .unwrap_or_else(|_| magnitude.display_str())
3271    }
3272
3273    match (&expected_type.specifications, &value.value) {
3274        (
3275            TypeSpecification::Number {
3276                minimum,
3277                maximum,
3278                decimals,
3279                ..
3280            },
3281            ValueKind::Number(n),
3282        ) => {
3283            use std::cmp::Ordering;
3284            if let Some(d) = decimals {
3285                if exceeds_decimal_places(n, *d) {
3286                    return Err(format!(
3287                        "{} exceeds the decimals constraint of {d}.",
3288                        n.display_str()
3289                    ));
3290                }
3291            }
3292            if let Some(min) = minimum {
3293                if n.try_cmp(min).map_err(|failure| {
3294                    format!("Comparing against the minimum bound failed: {failure}.")
3295                })? == Ordering::Less
3296                {
3297                    return Err(format!(
3298                        "{} is below the minimum of {}.",
3299                        format_rational_for_validation_message(expected_type, n),
3300                        format_rational_for_validation_message(expected_type, min)
3301                    ));
3302                }
3303            }
3304            if let Some(max) = maximum {
3305                if n.try_cmp(max).map_err(|failure| {
3306                    format!("Comparing against the maximum bound failed: {failure}.")
3307                })? == Ordering::Greater
3308                {
3309                    return Err(format!(
3310                        "{} is above the maximum of {}.",
3311                        format_rational_for_validation_message(expected_type, n),
3312                        format_rational_for_validation_message(expected_type, max)
3313                    ));
3314                }
3315            }
3316            Ok(())
3317        }
3318        (
3319            TypeSpecification::Measure {
3320                minimum,
3321                maximum,
3322                units,
3323                ..
3324            },
3325            ValueKind::Measure(magnitude),
3326        ) => {
3327            use crate::computation::rational::{checked_div, checked_mul};
3328            use std::cmp::Ordering;
3329            for (bound, label, violation, relation) in [
3330                (minimum, "minimum", Ordering::Less, "below"),
3331                (maximum, "maximum", Ordering::Greater, "above"),
3332            ] {
3333                let Some((bound_magnitude, bound_unit)) = bound else {
3334                    continue;
3335                };
3336                let bound_unit_def = units.get(bound_unit.as_str())?;
3337                let canonical_bound = if let Some(factor) = bound_unit_def.literal_factor() {
3338                    checked_mul(bound_magnitude, factor).map_err(|failure| {
3339                        format!("{label}: unit conversion overflow: {failure}")
3340                    })?
3341                } else {
3342                    let factor = runtime_scales
3343                        .and_then(|scales| scales.get(bound_unit.as_str()))
3344                        .ok_or_else(|| {
3345                            format!(
3346                                "{label} cannot use unit '{bound_unit}' on '{}': its conversion factor is not settled.",
3347                                expected_type.name()
3348                            )
3349                        })?;
3350                    checked_mul(bound_magnitude, factor).map_err(|failure| {
3351                        format!("{label}: unit conversion overflow: {failure}")
3352                    })?
3353                };
3354                if magnitude.try_cmp(&canonical_bound).map_err(|failure| {
3355                    format!("Comparing against the {label} bound failed: {failure}.")
3356                })? != violation
3357                {
3358                    continue;
3359                }
3360                let display_unit = match expected_type.unit() {
3361                    Some(written) => units.get(written)?,
3362                    None => units.get(bound_unit)?,
3363                };
3364                let factor = if let Some(factor) = display_unit.literal_factor() {
3365                    factor.clone()
3366                } else {
3367                    runtime_scales
3368                        .and_then(|scales| scales.get(display_unit.name.as_str()))
3369                        .cloned()
3370                        .ok_or_else(|| {
3371                            format!(
3372                                "Cannot de-canonicalize the measure for validation: unit '{}' scale is not settled.",
3373                                display_unit.name
3374                            )
3375                        })?
3376                };
3377                let value_in_unit = checked_div(magnitude, &factor).map_err(|failure| {
3378                    format!("Cannot de-canonicalize the measure for validation: {failure}.")
3379                })?;
3380                let bound_in_unit = checked_div(&canonical_bound, &factor).map_err(|failure| {
3381                    format!("Cannot de-canonicalize the {label} for validation: {failure}.")
3382                })?;
3383                return Err(format!(
3384                    "{} {unit} is {relation} the {label} of {} {unit}.",
3385                    format_rational_for_validation_message(expected_type, &value_in_unit),
3386                    format_rational_for_validation_message(expected_type, &bound_in_unit),
3387                    unit = display_unit.name
3388                ));
3389            }
3390            Ok(())
3391        }
3392        (TypeSpecification::Text { length, .. }, ValueKind::Text(s)) => {
3393            let len = s.chars().count();
3394            if let Some(exact) = length {
3395                if len != *exact {
3396                    return Err(format!(
3397                        "'{}' has length {}, but the required length is {}.",
3398                        s, len, exact
3399                    ));
3400                }
3401            }
3402            Ok(())
3403        }
3404        (
3405            TypeSpecification::Ratio {
3406                minimum,
3407                maximum,
3408                units,
3409                ..
3410            },
3411            ValueKind::Ratio(r),
3412        ) => {
3413            use crate::computation::rational::checked_mul;
3414            use std::cmp::Ordering;
3415
3416            let written_unit = expected_type.unit();
3417
3418            if let Some(type_minimum) = minimum {
3419                if r.try_cmp(type_minimum).map_err(|failure| {
3420                    format!("Comparing against the minimum bound failed: {failure}.")
3421                })? == Ordering::Less
3422                {
3423                    let message = match written_unit {
3424                        Some(unit) => {
3425                            let ratio_unit = units.get(unit)?;
3426                            let value_per_unit = checked_mul(r, &ratio_unit.value)
3427                                .map_err(|failure| failure.to_string())?;
3428                            let bound_per_unit = ratio_unit.minimum.clone().expect(
3429                                "BUG: RatioUnit.minimum missing after type minimum set by sync_ratio_units_from_canonical",
3430                            );
3431                            format!(
3432                                "{} {unit} is below the minimum of {} {unit}.",
3433                                format_rational_for_validation_message(
3434                                    expected_type,
3435                                    &value_per_unit
3436                                ),
3437                                format_rational_for_validation_message(
3438                                    expected_type,
3439                                    &bound_per_unit.clone()
3440                                ),
3441                            )
3442                        }
3443                        None => format!(
3444                            "{} is below the minimum of {}.",
3445                            format_rational_for_validation_message(expected_type, r),
3446                            format_rational_for_validation_message(expected_type, type_minimum),
3447                        ),
3448                    };
3449                    return Err(message);
3450                }
3451            }
3452            if let Some(type_maximum) = maximum {
3453                if r.try_cmp(type_maximum).map_err(|failure| {
3454                    format!("Comparing against the maximum bound failed: {failure}.")
3455                })? == Ordering::Greater
3456                {
3457                    let message = match written_unit {
3458                        Some(unit) => {
3459                            let ratio_unit = units.get(unit)?;
3460                            let value_per_unit = checked_mul(r, &ratio_unit.value)
3461                                .map_err(|failure| failure.to_string())?;
3462                            let bound_per_unit = ratio_unit.maximum.clone().expect(
3463                                "BUG: RatioUnit.maximum missing after type maximum set by sync_ratio_units_from_canonical",
3464                            );
3465                            format!(
3466                                "{} {unit} is above the maximum of {} {unit}.",
3467                                format_rational_for_validation_message(
3468                                    expected_type,
3469                                    &value_per_unit
3470                                ),
3471                                format_rational_for_validation_message(
3472                                    expected_type,
3473                                    &bound_per_unit.clone()
3474                                ),
3475                            )
3476                        }
3477                        None => format!(
3478                            "{} is above the maximum of {}.",
3479                            format_rational_for_validation_message(expected_type, r),
3480                            format_rational_for_validation_message(expected_type, type_maximum),
3481                        ),
3482                    };
3483                    return Err(message);
3484                }
3485            }
3486            Ok(())
3487        }
3488        (
3489            TypeSpecification::Date {
3490                minimum, maximum, ..
3491            },
3492            ValueKind::Date(dt),
3493        ) => {
3494            use crate::planning::semantics::{compare_semantic_dates, date_time_to_semantic};
3495            use std::cmp::Ordering;
3496            if let Some(min) = minimum {
3497                let min_sem = date_time_to_semantic(min);
3498                if compare_semantic_dates(dt, &min_sem) == Ordering::Less {
3499                    return Err(format!("{} is below the minimum of {}.", dt, min));
3500                }
3501            }
3502            if let Some(max) = maximum {
3503                let max_sem = date_time_to_semantic(max);
3504                if compare_semantic_dates(dt, &max_sem) == Ordering::Greater {
3505                    return Err(format!("{} is above the maximum of {}.", dt, max));
3506                }
3507            }
3508            Ok(())
3509        }
3510        (
3511            TypeSpecification::Time {
3512                minimum, maximum, ..
3513            },
3514            ValueKind::Time(t),
3515        ) => {
3516            use crate::planning::semantics::{compare_semantic_times, time_to_semantic};
3517            use std::cmp::Ordering;
3518            if let Some(min) = minimum {
3519                let min_sem = time_to_semantic(min);
3520                if compare_semantic_times(t, &min_sem) == Ordering::Less {
3521                    return Err(format!("{} is below the minimum of {}.", t, min));
3522                }
3523            }
3524            if let Some(max) = maximum {
3525                let max_sem = time_to_semantic(max);
3526                if compare_semantic_times(t, &max_sem) == Ordering::Greater {
3527                    return Err(format!("{} is above the maximum of {}.", t, max));
3528                }
3529            }
3530            Ok(())
3531        }
3532        (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_)) => Ok(()),
3533        (TypeSpecification::List { .. }, ValueKind::List(_))
3534        | (
3535            TypeSpecification::NumberRange { .. }
3536            | TypeSpecification::DateRange { .. }
3537            | TypeSpecification::TimeRange { .. }
3538            | TypeSpecification::MeasureRange { .. }
3539            | TypeSpecification::RatioRange { .. },
3540            ValueKind::Range(_, _),
3541        ) => unreachable!("BUG: list and range values are walked by validate_value_against_type"),
3542        (TypeSpecification::Veto { .. }, _) | (TypeSpecification::Undetermined, _) => Ok(()),
3543        (TypeSpecification::Spec { .. }, ValueKind::Spec(_)) => Ok(()),
3544        (spec, value_kind) if !value_kind_matches_spec(value_kind, spec) => unreachable!(
3545            "BUG: validate_value_against_type called with mismatched type/value: \
3546             spec={:?}, value={:?} — typing must be enforced before validation",
3547            spec, value_kind
3548        ),
3549        (spec, value_kind) => unreachable!(
3550            "BUG: validate_value_against_type missed a value_kind_matches_spec pair: \
3551             spec={:?}, value={:?}",
3552            spec, value_kind
3553        ),
3554    }
3555}
3556
3557fn validate_range_literal(
3558    expected_type: &LemmaType,
3559    range_spec: &TypeSpecification,
3560    left: &TypedLiteral,
3561    right: &TypedLiteral,
3562    unit_index: &UnitIndex,
3563) -> Result<(), String> {
3564    use crate::computation::{comparison_operation, OperationResult};
3565    use crate::planning::semantics::{
3566        compare_semantic_dates, compare_semantic_times, measure_declared_bound_to_canonical,
3567        ValueKind,
3568    };
3569    use std::cmp::Ordering;
3570    use std::sync::Arc;
3571
3572    let element_spec = range_spec
3573        .element_from_range()
3574        .expect("BUG: element_from_range missing arm for validated range");
3575    if let TypeSpecification::Measure {
3576        units,
3577        decomposition,
3578        ..
3579    } = &element_spec
3580    {
3581        assert!(
3582            decomposition.is_some() || units.is_empty(),
3583            "BUG: measure range '{}' with units must carry a decomposition after planning",
3584            expected_type.name()
3585        );
3586    }
3587    let element_type = Arc::new(LemmaType::primitive(element_spec));
3588    validate_scalar_against_type(element_type.as_ref(), &left.to_literal(), None)?;
3589    validate_scalar_against_type(element_type.as_ref(), &right.to_literal(), None)?;
3590
3591    let ordering = match (&left.value, &right.value) {
3592        (ValueKind::Number(l), ValueKind::Number(r)) => l
3593            .try_cmp(r)
3594            .map_err(|failure| format!("Comparing range endpoints failed: {failure}."))?,
3595        (ValueKind::Date(l), ValueKind::Date(r)) => compare_semantic_dates(l, r),
3596        (ValueKind::Time(l), ValueKind::Time(r)) => compare_semantic_times(l, r),
3597        (ValueKind::Ratio(l), ValueKind::Ratio(r)) => l
3598            .try_cmp(r)
3599            .map_err(|failure| format!("Comparing range endpoints failed: {failure}."))?,
3600        (ValueKind::Measure(l), ValueKind::Measure(r)) => l
3601            .try_cmp(r)
3602            .map_err(|failure| format!("Comparing range endpoints failed: {failure}."))?,
3603        (left_kind, right_kind) => unreachable!(
3604            "BUG: range endpoints have mismatched value kinds after typing: {left_kind:?} vs {right_kind:?}"
3605        ),
3606    };
3607    if ordering == Ordering::Greater {
3608        return Err(format!(
3609            "The range left endpoint {} is above the right endpoint {}.",
3610            left.display_value(),
3611            right.display_value()
3612        ));
3613    }
3614
3615    let range_lit = LiteralValue {
3616        value: ValueKind::Range(Box::new(left.clone()), Box::new(right.clone())),
3617    };
3618    let range_type = Arc::new(expected_type.clone());
3619
3620    let compare_width = |bound: &LiteralValue,
3621                         bound_type: &Arc<LemmaType>,
3622                         op: ComparisonComputation,
3623                         fail_msg: String|
3624     -> Result<(), String> {
3625        match comparison_operation(&range_lit.value, &range_type, &op, &bound.value, bound_type) {
3626            OperationResult::Value(result) => match &result.value {
3627                ValueKind::Boolean(true) => Ok(()),
3628                ValueKind::Boolean(false) => Err(fail_msg),
3629                other => unreachable!("BUG: width comparison must return boolean, got {other:?}"),
3630            },
3631            OperationResult::Veto(veto) => Err(veto.to_string()),
3632        }
3633    };
3634
3635    match range_spec {
3636        TypeSpecification::NumberRange {
3637            minimum, maximum, ..
3638        } => {
3639            if let Some(min_w) = minimum {
3640                compare_width(
3641                    &LiteralValue::number(min_w.clone()),
3642                    primitive_number_arc(),
3643                    ComparisonComputation::GreaterThanOrEqual,
3644                    format!(
3645                        "The span is below the minimum width of {}.",
3646                        min_w.display_str()
3647                    ),
3648                )?;
3649            }
3650            if let Some(max_w) = maximum {
3651                compare_width(
3652                    &LiteralValue::number(max_w.clone()),
3653                    primitive_number_arc(),
3654                    ComparisonComputation::LessThanOrEqual,
3655                    format!(
3656                        "The span is above the maximum width of {}.",
3657                        max_w.display_str()
3658                    ),
3659                )?;
3660            }
3661        }
3662        TypeSpecification::RatioRange {
3663            minimum, maximum, ..
3664        } => {
3665            if let Some(min_w) = minimum {
3666                compare_width(
3667                    &LiteralValue::ratio(min_w.clone()),
3668                    primitive_ratio_arc(),
3669                    ComparisonComputation::GreaterThanOrEqual,
3670                    format!(
3671                        "The span is below the minimum width of {}.",
3672                        min_w.display_str()
3673                    ),
3674                )?;
3675            }
3676            if let Some(max_w) = maximum {
3677                compare_width(
3678                    &LiteralValue::ratio(max_w.clone()),
3679                    primitive_ratio_arc(),
3680                    ComparisonComputation::LessThanOrEqual,
3681                    format!(
3682                        "The span is above the maximum width of {}.",
3683                        max_w.display_str()
3684                    ),
3685                )?;
3686            }
3687        }
3688        TypeSpecification::MeasureRange {
3689            minimum,
3690            maximum,
3691            units,
3692            ..
3693        } => {
3694            if let Some(min_w) = minimum {
3695                let canonical = measure_declared_bound_to_canonical(
3696                    &min_w.0, &min_w.1, units, "range", "minimum",
3697                )?;
3698                let bound = LiteralValue::measure(canonical);
3699                compare_width(
3700                    &bound,
3701                    &element_type,
3702                    ComparisonComputation::GreaterThanOrEqual,
3703                    format!(
3704                        "The span is below the minimum width of {} {}.",
3705                        min_w.0.display_str(),
3706                        min_w.1
3707                    ),
3708                )?;
3709            }
3710            if let Some(max_w) = maximum {
3711                let canonical = measure_declared_bound_to_canonical(
3712                    &max_w.0, &max_w.1, units, "range", "maximum",
3713                )?;
3714                let bound = LiteralValue::measure(canonical);
3715                compare_width(
3716                    &bound,
3717                    &element_type,
3718                    ComparisonComputation::LessThanOrEqual,
3719                    format!(
3720                        "The span is above the maximum width of {} {}.",
3721                        max_w.0.display_str(),
3722                        max_w.1
3723                    ),
3724                )?;
3725            }
3726        }
3727        TypeSpecification::DateRange {
3728            minimum, maximum, ..
3729        }
3730        | TypeSpecification::TimeRange {
3731            minimum, maximum, ..
3732        } => {
3733            let allow_calendar = matches!(range_spec, TypeSpecification::DateRange { .. });
3734            let resolve_bound = |bound: &(RationalInteger, String),
3735                                 command: &str|
3736             -> Result<(LiteralValue, Arc<LemmaType>), String> {
3737                let (bare, owner) = unit_index
3738                    .resolve(bound.1.as_str())
3739                    .map_err(|err| format!("{command} width unit '{}': {err}", bound.1))?;
3740                if allow_calendar {
3741                    if !owner.is_duration_like() && !owner.is_calendar_like() {
3742                        return Err(format!(
3743                                "The {command} width unit '{bare}' must be a duration or calendar unit.",
3744                            ));
3745                    }
3746                } else if !owner.is_duration_like() {
3747                    return Err(format!(
3748                        "The {command} width unit '{bare}' must be a duration unit.",
3749                    ));
3750                }
3751                let TypeSpecification::Measure { units, .. } = &owner.specifications else {
3752                    return Err(format!(
3753                        "The {command} width unit '{bare}' must resolve to a measure type.",
3754                    ));
3755                };
3756                let type_name = owner.name();
3757                let canonical = measure_declared_bound_to_canonical(
3758                    &bound.0,
3759                    &bare,
3760                    units,
3761                    type_name.as_str(),
3762                    command,
3763                )?;
3764                Ok((LiteralValue::measure(canonical), Arc::clone(&owner)))
3765            };
3766            if let Some(min_w) = minimum {
3767                let (bound, bound_type) = resolve_bound(min_w, "minimum")?;
3768                compare_width(
3769                    &bound,
3770                    &bound_type,
3771                    ComparisonComputation::GreaterThanOrEqual,
3772                    format!(
3773                        "The span is below the minimum width of {} {}.",
3774                        min_w.0.display_str(),
3775                        min_w.1
3776                    ),
3777                )?;
3778            }
3779            if let Some(max_w) = maximum {
3780                let (bound, bound_type) = resolve_bound(max_w, "maximum")?;
3781                compare_width(
3782                    &bound,
3783                    &bound_type,
3784                    ComparisonComputation::LessThanOrEqual,
3785                    format!(
3786                        "The span is above the maximum width of {} {}.",
3787                        max_w.0.display_str(),
3788                        max_w.1
3789                    ),
3790                )?;
3791            }
3792        }
3793        _ => {}
3794    }
3795
3796    Ok(())
3797}
3798
3799/// Literal plan data against its declared type. Written row values are validated
3800/// when their row is built (`source_row_literal` in the graph builder).
3801fn validate_literal_data_against_types(plan: &ExecutionPlan) -> Vec<Error> {
3802    let mut errors = Vec::new();
3803
3804    for (data_path, data_definition) in &plan.data {
3805        let (expected_type, lit) = match data_definition {
3806            DataDefinition::Value {
3807                value,
3808                resolved_type,
3809                ..
3810            } => (resolved_type, value),
3811            DataDefinition::Import { .. }
3812            | DataDefinition::TypeDeclaration { .. }
3813            | DataDefinition::InstanceList { .. }
3814            | DataDefinition::Reference { .. } => continue,
3815        };
3816
3817        if let Err(msg) =
3818            validate_value_against_type(expected_type, lit, plan.expression_unit_index(), None)
3819        {
3820            let source = data_definition.source().clone();
3821            errors.push(Error::validation(
3822                format!(
3823                    "Invalid value for data {} (expected {}): {}",
3824                    data_path,
3825                    expected_type.name().as_str(),
3826                    msg
3827                ),
3828                Some(source),
3829                None::<String>,
3830            ));
3831        }
3832    }
3833
3834    errors
3835}
3836
3837fn validate_unit_conversion_targets(plan: &ExecutionPlan) -> Result<(), Error> {
3838    use crate::planning::normalize::NormalFormKind;
3839
3840    let mut errors: Vec<Error> = Vec::new();
3841    let mut visited = HashSet::new();
3842    let mut worklist: Vec<NormalFormId> =
3843        plan.rules.values().map(|rule| rule.normal_form).collect();
3844    while let Some(id) = worklist.pop() {
3845        if !visited.insert(id) {
3846            continue;
3847        }
3848        let nf = plan.normal_form(id);
3849        nf.kind.for_each_child(|child| worklist.push(child));
3850        if let NormalFormKind::UnitConversion(_, target) = &nf.kind {
3851            if let Some((unit_name, owning_type)) = conversion_target_declares_unit(target) {
3852                if !owning_type_declares_unit_name(owning_type.as_ref(), unit_name) {
3853                    errors.push(Error::validation(
3854                        format!(
3855                            "Unit conversion target '{unit_name}' is not declared on owning type '{}'",
3856                            owning_type.name()
3857                        ),
3858                        None::<Source>,
3859                        Some(plan.spec_name.clone()),
3860                    ));
3861                }
3862            }
3863        }
3864    }
3865    if let Some(error) = errors.into_iter().next() {
3866        return Err(error);
3867    }
3868    Ok(())
3869}
3870
3871#[cfg(test)]
3872mod tests {
3873    use super::*;
3874    use crate::computation::rational::rational_new;
3875    use crate::computation::{OperationResult, VetoType};
3876    use crate::evaluation::evaluate_data::EvaluateData;
3877    use crate::literals::DateGranularity;
3878    use crate::literals::TimezoneValue;
3879    use crate::parsing::ast::DateTimeValue;
3880    use crate::planning::semantics::{DataDefinition, DataPath, TypeSpecification};
3881    use crate::Engine;
3882    use crate::{EvaluateDataValue, ResourceLimits};
3883    use serde_json;
3884    use std::sync::Arc;
3885
3886    fn default_limits() -> ResourceLimits {
3887        ResourceLimits::default()
3888    }
3889
3890    fn resolve_evaluate_data(
3891        plan: &ExecutionPlan,
3892        values: IndexMap<String, EvaluateDataValue>,
3893    ) -> EvaluateData {
3894        EvaluateData::resolve(plan, values, &default_limits()).expect("resolve")
3895    }
3896
3897    fn binding<'a>(
3898        plan: &ExecutionPlan,
3899        evaluate_data: &'a EvaluateData,
3900        path: &DataPath,
3901    ) -> Option<&'a OperationResult> {
3902        let slot = plan
3903            .data
3904            .get_index_of(path)
3905            .expect("BUG: test path is plan data");
3906        evaluate_data.bindings.get(&slot)
3907    }
3908
3909    fn veto_reason<'a>(
3910        plan: &ExecutionPlan,
3911        evaluate_data: &'a EvaluateData,
3912        path: &DataPath,
3913    ) -> Option<&'a str> {
3914        match binding(plan, evaluate_data, path) {
3915            Some(OperationResult::Veto(veto)) => Some(match veto {
3916                VetoType::Computation { message } => message.as_str(),
3917                other => panic!("expected Computation veto, got {other:?}"),
3918            }),
3919            _ => None,
3920        }
3921    }
3922
3923    fn bound_value<'a>(
3924        plan: &ExecutionPlan,
3925        evaluate_data: &'a EvaluateData,
3926        path: &DataPath,
3927    ) -> Option<&'a crate::planning::semantics::BoundValueKind> {
3928        binding(plan, evaluate_data, path).and_then(OperationResult::value)
3929    }
3930
3931    fn input_data(pairs: &[(&str, &str)]) -> IndexMap<String, EvaluateDataValue> {
3932        pairs
3933            .iter()
3934            .map(|(k, v)| (k.to_string(), EvaluateDataValue::string(*v)))
3935            .collect()
3936    }
3937
3938    #[test]
3939    fn test_with_raw_values() {
3940        let mut engine = Engine::new();
3941        engine
3942            .load([(
3943                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
3944                    "test.lemma",
3945                ))),
3946                r#"
3947                spec test
3948                data age: number -> suggest 25
3949                "#
3950                .to_string(),
3951            )])
3952            .unwrap();
3953
3954        let plans = engine
3955            .plans
3956            .get_plans(None, "test")
3957            .expect("plans for test");
3958        let plan = plans.values().next().expect("plan");
3959        let data_path = DataPath::new(vec![], "age".to_string());
3960
3961        let values = input_data(&[("age", "30")]);
3962
3963        let evaluate_data = resolve_evaluate_data(plan, values);
3964        let updated_value = bound_value(plan, &evaluate_data, &data_path).expect("bound value");
3965        match &updated_value.value {
3966            crate::planning::semantics::ValueKind::Number(n) => {
3967                assert_eq!(n, &rational_new(30, 1));
3968            }
3969            other => panic!("Expected number literal, got {:?}", other),
3970        }
3971    }
3972
3973    #[test]
3974    fn test_with_raw_values_type_mismatch() {
3975        let mut engine = Engine::new();
3976        engine
3977            .load([(
3978                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
3979                    "test.lemma",
3980                ))),
3981                r#"
3982                spec test
3983                data age: number
3984                "#
3985                .to_string(),
3986            )])
3987            .unwrap();
3988
3989        let plans = engine
3990            .plans
3991            .get_plans(None, "test")
3992            .expect("plans for test");
3993        let plan = plans.values().next().expect("plan");
3994
3995        let values = input_data(&[("age", "thirty")]);
3996
3997        let evaluate_data = resolve_evaluate_data(plan, values);
3998        let data_path = DataPath::new(vec![], "age".to_string());
3999        match veto_reason(plan, &evaluate_data, &data_path) {
4000            Some(reason) => {
4001                assert!(
4002                    reason.contains("number"),
4003                    "type mismatch must record violation reason, got: {reason}"
4004                );
4005            }
4006            None => panic!("expected veto-bound data for age=thirty"),
4007        }
4008    }
4009
4010    #[test]
4011    fn test_with_raw_values_unknown_data_ignored() {
4012        let mut engine = Engine::new();
4013        engine
4014            .load([(
4015                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
4016                    "test.lemma",
4017                ))),
4018                r#"
4019                spec test
4020                data known: number
4021                "#
4022                .to_string(),
4023            )])
4024            .unwrap();
4025
4026        let plans = engine
4027            .plans
4028            .get_plans(None, "test")
4029            .expect("plans for test");
4030        let plan = plans.values().next().expect("plan");
4031
4032        let values = input_data(&[("unknown", "30")]);
4033
4034        let evaluate_data = resolve_evaluate_data(plan, values);
4035        assert!(evaluate_data.bindings.is_empty());
4036        assert!(evaluate_data.ignored_unknown.iter().any(|k| k == "unknown"));
4037    }
4038
4039    #[test]
4040    fn test_with_raw_values_nested() {
4041        let mut engine = Engine::new();
4042        engine
4043            .load([(
4044                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
4045                    "test.lemma",
4046                ))),
4047                r#"
4048                spec private
4049                data base_price: number
4050
4051                spec test
4052                uses rules: private
4053                "#
4054                .to_string(),
4055            )])
4056            .unwrap();
4057
4058        let plans = engine
4059            .plans
4060            .get_plans(None, "test")
4061            .expect("plans for test");
4062        let plan = plans.values().next().expect("plan");
4063
4064        let values = input_data(&[("rules.base_price", "100")]);
4065
4066        let evaluate_data = resolve_evaluate_data(plan, values);
4067        let Some(DataDefinition::Import { row, .. }) =
4068            plan.data.get(&DataPath::local("rules".into()))
4069        else {
4070            panic!("`rules` is the import slot of the plan");
4071        };
4072        let instance = crate::planning::semantics::InstanceKey::new(Arc::clone(row), None);
4073        let slot = plan
4074            .row_template(&row.identity)
4075            .data
4076            .get_index_of(&DataPath::local("base_price".into()))
4077            .expect("`base_price` is data of the imported template");
4078        let updated_value = evaluate_data.row_overrides.fields[&instance][&slot]
4079            .value()
4080            .expect("bound value");
4081        match &updated_value.value {
4082            crate::planning::semantics::ValueKind::Number(n) => {
4083                assert_eq!(n, &rational_new(100, 1));
4084            }
4085            other => panic!("Expected number literal, got {:?}", other),
4086        }
4087    }
4088
4089    #[test]
4090    fn evaluate_data_should_enforce_number_maximum_constraint() {
4091        // Higher-standard requirement: user input must be validated against type constraints.
4092        // If this test fails, Lemma accepts invalid values and gives false reassurance.
4093        let data_path = DataPath::new(vec![], "x".to_string());
4094
4095        let max10 = crate::planning::semantics::LemmaType::primitive(
4096            crate::planning::semantics::TypeSpecification::Number {
4097                minimum: None,
4098                maximum: Some(rational_new(10, 1)),
4099                decimals: None,
4100                help: String::new(),
4101            },
4102        );
4103        let source = Source::new(
4104            crate::parsing::source::SourceType::Volatile,
4105            crate::parsing::ast::Span {
4106                start: 0,
4107                end: 0,
4108                line: 1,
4109                col: 0,
4110            },
4111        );
4112        let mut data = IndexMap::new();
4113        data.insert(
4114            data_path.clone(),
4115            crate::planning::semantics::DataDefinition::Value {
4116                value: crate::planning::semantics::LiteralValue::number(rational_new(0, 1)),
4117                resolved_type: Arc::new(max10.clone()),
4118                source: source.clone(),
4119                origin: crate::planning::semantics::DataOrigin::Declared,
4120            },
4121        );
4122
4123        let input_key_index = data.keys().map(|p| (p.input_key(), p.clone())).collect();
4124        let data_len = data.len();
4125        let plan = ExecutionPlan {
4126            spec_name: "test".to_string(),
4127            commentary: None,
4128            data,
4129            normal_forms: Vec::new(),
4130            rules: IndexMap::new(),
4131            data_reference_order: Vec::new(),
4132            meta: IndexMap::new(),
4133            resolved_types: ResolvedSpecTypes::default(),
4134            family_units: Arc::new(FamilyUnitCatalog::default()),
4135            signature_index: IndexMap::new(),
4136            effective: EffectiveDate::Origin,
4137            effective_from: None,
4138            effective_to: None,
4139            start_line: 1,
4140            source_type: None,
4141            needed_by_rules: vec![NeededByRules::default(); data_len],
4142            data_display: IndexMap::new(),
4143            show_rules: IndexMap::new(),
4144            show_needed_by: IndexMap::new(),
4145            show_import_data: IndexMap::new(),
4146            reference_ends: IndexMap::new(),
4147            input_key_index,
4148            show_rule_index: IndexMap::new(),
4149            data_leaf: IndexMap::new(),
4150            member_targets: IndexMap::new(),
4151            row_templates: std::collections::BTreeMap::new(),
4152            own_spec: Arc::new(LemmaSpec::new("test".to_string())),
4153            edges: Vec::new(),
4154            identity: SpecIdentity {
4155                repository: None,
4156                spec: "test".to_string(),
4157                effective: EffectiveDate::Origin,
4158                slice: EffectiveDate::Origin,
4159            },
4160            rule_candidates: std::collections::BTreeMap::new(),
4161            self_row_rules: Vec::new(),
4162            instance_lists: IndexMap::new(),
4163            row_mask: Vec::new(),
4164            initial_values: Vec::new(),
4165            walk_settled_leaves: Vec::new(),
4166            rule_ref_indexes: Vec::new(),
4167            data_slot_leaves: Vec::new(),
4168            data_slot_scale_targets: Vec::new(),
4169        };
4170
4171        let values = input_data(&[("x", "11")]);
4172
4173        let evaluate_data = resolve_evaluate_data(&plan, values);
4174        match veto_reason(&plan, &evaluate_data, &data_path) {
4175            Some(reason) => {
4176                assert!(
4177                    reason.contains("maximum") || reason.contains("10"),
4178                    "x=11 must violate maximum 10, got: {reason}"
4179                );
4180            }
4181            None => panic!("expected veto-bound data for x=11"),
4182        }
4183    }
4184
4185    #[test]
4186    fn evaluate_data_should_enforce_text_enum_options() {
4187        // Higher-standard requirement: enum options must be enforced for text types.
4188        let data_path = DataPath::new(vec![], "tier".to_string());
4189
4190        let mut tier = crate::planning::semantics::LemmaType::primitive(
4191            crate::planning::semantics::TypeSpecification::Text {
4192                length: None,
4193                help: String::new(),
4194            },
4195        );
4196        tier.options.values = vec![
4197            crate::planning::semantics::OptionValue {
4198                value: crate::planning::semantics::BoundValueKind::unbound(
4199                    crate::planning::semantics::ValueKind::Text(Arc::from("silver")),
4200                ),
4201            },
4202            crate::planning::semantics::OptionValue {
4203                value: crate::planning::semantics::BoundValueKind::unbound(
4204                    crate::planning::semantics::ValueKind::Text(Arc::from("gold")),
4205                ),
4206            },
4207        ];
4208        let source = Source::new(
4209            crate::parsing::source::SourceType::Volatile,
4210            crate::parsing::ast::Span {
4211                start: 0,
4212                end: 0,
4213                line: 1,
4214                col: 0,
4215            },
4216        );
4217        let mut data = IndexMap::new();
4218        data.insert(
4219            data_path.clone(),
4220            crate::planning::semantics::DataDefinition::Value {
4221                value: crate::planning::semantics::LiteralValue::text("silver".to_string()),
4222                resolved_type: Arc::new(tier.clone()),
4223                origin: crate::planning::semantics::DataOrigin::Declared,
4224                source,
4225            },
4226        );
4227
4228        let input_key_index = data.keys().map(|p| (p.input_key(), p.clone())).collect();
4229        let data_len = data.len();
4230        let plan = ExecutionPlan {
4231            spec_name: "test".to_string(),
4232            commentary: None,
4233            data,
4234            normal_forms: Vec::new(),
4235            rules: IndexMap::new(),
4236            data_reference_order: Vec::new(),
4237            meta: IndexMap::new(),
4238            resolved_types: ResolvedSpecTypes::default(),
4239            family_units: Arc::new(FamilyUnitCatalog::default()),
4240            signature_index: IndexMap::new(),
4241            effective: EffectiveDate::Origin,
4242            effective_from: None,
4243            effective_to: None,
4244            start_line: 1,
4245            source_type: None,
4246            needed_by_rules: vec![NeededByRules::default(); data_len],
4247            data_display: IndexMap::new(),
4248            show_rules: IndexMap::new(),
4249            show_needed_by: IndexMap::new(),
4250            show_import_data: IndexMap::new(),
4251            reference_ends: IndexMap::new(),
4252            input_key_index,
4253            show_rule_index: IndexMap::new(),
4254            data_leaf: IndexMap::new(),
4255            member_targets: IndexMap::new(),
4256            row_templates: std::collections::BTreeMap::new(),
4257            own_spec: Arc::new(LemmaSpec::new("test".to_string())),
4258            edges: Vec::new(),
4259            identity: SpecIdentity {
4260                repository: None,
4261                spec: "test".to_string(),
4262                effective: EffectiveDate::Origin,
4263                slice: EffectiveDate::Origin,
4264            },
4265            rule_candidates: std::collections::BTreeMap::new(),
4266            self_row_rules: Vec::new(),
4267            instance_lists: IndexMap::new(),
4268            row_mask: Vec::new(),
4269            initial_values: Vec::new(),
4270            walk_settled_leaves: Vec::new(),
4271            rule_ref_indexes: Vec::new(),
4272            data_slot_leaves: Vec::new(),
4273            data_slot_scale_targets: Vec::new(),
4274        };
4275
4276        let values = input_data(&[("tier", "platinum")]);
4277
4278        let evaluate_data = resolve_evaluate_data(&plan, values);
4279        match veto_reason(&plan, &evaluate_data, &data_path) {
4280            Some(reason) => {
4281                assert!(
4282                    reason.contains("allowed options") || reason.contains("platinum"),
4283                    "invalid enum must record violation, got: {reason}"
4284                );
4285            }
4286            None => panic!("expected veto-bound data for tier=platinum"),
4287        }
4288    }
4289
4290    fn empty_plan(effective: crate::parsing::ast::EffectiveDate) -> ExecutionPlan {
4291        ExecutionPlan {
4292            spec_name: "s".into(),
4293            commentary: None,
4294            data: IndexMap::new(),
4295            normal_forms: Vec::new(),
4296            rules: IndexMap::new(),
4297            data_reference_order: Vec::new(),
4298            meta: IndexMap::new(),
4299            resolved_types: ResolvedSpecTypes::default(),
4300            family_units: Arc::new(FamilyUnitCatalog::default()),
4301            signature_index: IndexMap::new(),
4302            effective,
4303            effective_from: None,
4304            effective_to: None,
4305            start_line: 1,
4306            source_type: None,
4307            needed_by_rules: Vec::new(),
4308            data_display: IndexMap::new(),
4309            show_rules: IndexMap::new(),
4310            show_needed_by: IndexMap::new(),
4311            show_import_data: IndexMap::new(),
4312            reference_ends: IndexMap::new(),
4313            input_key_index: IndexMap::new(),
4314            show_rule_index: IndexMap::new(),
4315            data_leaf: IndexMap::new(),
4316            member_targets: IndexMap::new(),
4317            row_templates: std::collections::BTreeMap::new(),
4318            own_spec: Arc::new(LemmaSpec::new("test".to_string())),
4319            edges: Vec::new(),
4320            identity: SpecIdentity {
4321                repository: None,
4322                spec: "s".to_string(),
4323                effective: EffectiveDate::Origin,
4324                slice: EffectiveDate::Origin,
4325            },
4326            rule_candidates: std::collections::BTreeMap::new(),
4327            self_row_rules: Vec::new(),
4328            instance_lists: IndexMap::new(),
4329            row_mask: Vec::new(),
4330            initial_values: Vec::new(),
4331            walk_settled_leaves: Vec::new(),
4332            rule_ref_indexes: Vec::new(),
4333            data_slot_leaves: Vec::new(),
4334            data_slot_scale_targets: Vec::new(),
4335        }
4336    }
4337
4338    fn plans_by_effective(
4339        plans: impl IntoIterator<Item = ExecutionPlan>,
4340    ) -> BTreeMap<EffectiveDate, std::sync::Arc<ExecutionPlan>> {
4341        plans
4342            .into_iter()
4343            .map(|plan| (plan.effective.clone(), std::sync::Arc::new(plan)))
4344            .collect()
4345    }
4346
4347    /// Compiled plans for one spec name are ordered by `effective` key.
4348    #[test]
4349    fn plan_set_plans_are_in_ascending_effective_order() {
4350        let june = DateTimeValue {
4351            year: 2025,
4352            month: 6,
4353            day: 1,
4354            hour: 0,
4355            minute: 0,
4356            second: 0,
4357            microsecond: 0,
4358            timezone: None,
4359            granularity: DateGranularity::Full,
4360        };
4361        let dec = DateTimeValue {
4362            year: 2025,
4363            month: 12,
4364            day: 1,
4365            hour: 0,
4366            minute: 0,
4367            second: 0,
4368            microsecond: 0,
4369            timezone: None,
4370            granularity: DateGranularity::Full,
4371        };
4372
4373        let plans = plans_by_effective([
4374            empty_plan(EffectiveDate::Origin),
4375            empty_plan(EffectiveDate::DateTimeValue(june)),
4376            empty_plan(EffectiveDate::DateTimeValue(dec)),
4377        ]);
4378
4379        let effectives: Vec<_> = plans.keys().cloned().collect();
4380        for window in effectives.windows(2) {
4381            assert!(
4382                window[0] < window[1],
4383                "plans must be strictly ascending: {:?} >= {:?}",
4384                window[0],
4385                window[1]
4386            );
4387        }
4388    }
4389
4390    #[test]
4391    fn plan_at_exact_boundary_selects_later_slice() {
4392        use crate::parsing::ast::{DateTimeValue, EffectiveDate};
4393
4394        let june = DateTimeValue {
4395            year: 2025,
4396            month: 6,
4397            day: 1,
4398            hour: 0,
4399            minute: 0,
4400            second: 0,
4401            microsecond: 0,
4402            timezone: None,
4403
4404            granularity: DateGranularity::Full,
4405        };
4406        let dec = DateTimeValue {
4407            year: 2025,
4408            month: 12,
4409            day: 1,
4410            hour: 0,
4411            minute: 0,
4412            second: 0,
4413            microsecond: 0,
4414            timezone: None,
4415
4416            granularity: DateGranularity::Full,
4417        };
4418
4419        let june_key = EffectiveDate::DateTimeValue(june.clone());
4420        let dec_key = EffectiveDate::DateTimeValue(dec.clone());
4421        let plans = plans_by_effective([
4422            empty_plan(EffectiveDate::Origin),
4423            empty_plan(june_key.clone()),
4424            empty_plan(dec_key.clone()),
4425        ]);
4426
4427        let june_plan = plan_at(&plans, &june_key).expect("boundary instant");
4428        assert!(std::ptr::eq(
4429            june_plan,
4430            plans.get(&june_key).expect("june slice").as_ref()
4431        ));
4432
4433        let dec_plan = plan_at(&plans, &dec_key).expect("dec boundary");
4434        assert!(std::ptr::eq(
4435            dec_plan,
4436            plans.get(&dec_key).expect("dec slice").as_ref()
4437        ));
4438    }
4439
4440    #[test]
4441    fn plan_at_day_before_boundary_stays_in_earlier_slice() {
4442        use crate::parsing::ast::{DateTimeValue, EffectiveDate};
4443
4444        let june = DateTimeValue {
4445            year: 2025,
4446            month: 6,
4447            day: 1,
4448            hour: 0,
4449            minute: 0,
4450            second: 0,
4451            microsecond: 0,
4452            timezone: None,
4453
4454            granularity: DateGranularity::Full,
4455        };
4456        let may_end = DateTimeValue {
4457            year: 2025,
4458            month: 5,
4459            day: 31,
4460            hour: 23,
4461            minute: 59,
4462            second: 59,
4463            microsecond: 0,
4464            timezone: None,
4465
4466            granularity: DateGranularity::DateTime,
4467        };
4468
4469        let origin = EffectiveDate::Origin;
4470        let plans = plans_by_effective([
4471            empty_plan(origin.clone()),
4472            empty_plan(EffectiveDate::DateTimeValue(june)),
4473        ]);
4474
4475        let may_instant = EffectiveDate::DateTimeValue(may_end);
4476        let may_plan = plan_at(&plans, &may_instant).expect("may 31");
4477        assert!(std::ptr::eq(
4478            may_plan,
4479            plans.get(&origin).expect("origin slice").as_ref()
4480        ));
4481    }
4482
4483    #[test]
4484    fn plan_at_single_plan_matches_any_instant_after_start() {
4485        use crate::parsing::ast::{DateTimeValue, EffectiveDate};
4486
4487        let t = DateTimeValue {
4488            year: 2025,
4489            month: 3,
4490            day: 1,
4491            hour: 0,
4492            minute: 0,
4493            second: 0,
4494            microsecond: 0,
4495            timezone: None,
4496
4497            granularity: DateGranularity::Full,
4498        };
4499        let start = EffectiveDate::DateTimeValue(DateTimeValue {
4500            year: 2025,
4501            month: 1,
4502            day: 1,
4503            hour: 0,
4504            minute: 0,
4505            second: 0,
4506            microsecond: 0,
4507            timezone: None,
4508
4509            granularity: DateGranularity::Full,
4510        });
4511        let plans = plans_by_effective([empty_plan(start.clone())]);
4512        let instant = EffectiveDate::DateTimeValue(t);
4513        let selected = plan_at(&plans, &instant).expect("inside single slice");
4514        assert!(std::ptr::eq(
4515            selected,
4516            plans.get(&start).expect("single slice").as_ref()
4517        ));
4518    }
4519
4520    /// The show JSON shape is the IO contract for every non-Rust consumer
4521    /// (WASM playground, Hex, HTTP, TypeScript). Nail the exact envelope.
4522    #[test]
4523    fn show_json_shape_contract() {
4524        let mut engine = Engine::new();
4525        engine
4526            .load([(
4527                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
4528                    "test.lemma",
4529                ))),
4530                r#"
4531                spec pricing
4532                data bridge_height: measure
4533                  -> unit meter: 1
4534                  -> suggest 100 meter
4535                data quantity: number -> minimum 0
4536                rule cost: bridge_height * quantity
4537                "#
4538                .to_string(),
4539            )])
4540            .unwrap();
4541        let now = DateTimeValue::now();
4542        let schema = engine.show(None, "pricing", Some(&now)).unwrap();
4543
4544        let value = crate::api::show_document(&engine, &schema);
4545
4546        let bh = &value["properties"]["bridge_height"];
4547        assert!(
4548            bh.is_object(),
4549            "data entry must be a named object, not tuple"
4550        );
4551        assert!(
4552            bh.get("x-lemma-type").is_some(),
4553            "data entry must expose `x-lemma-type` field"
4554        );
4555        assert!(
4556            bh.get("x-lemma-suggestion").is_some(),
4557            "bridge_height exposes `-> suggest` as schema suggestion"
4558        );
4559        assert!(
4560            bh.get("x-lemma-value").is_none(),
4561            "bridge_height is not filled from spec"
4562        );
4563
4564        let ty = &bh["x-lemma-type"];
4565        assert_eq!(
4566            ty["kind"], "measure",
4567            "kind tag sits on the type object itself"
4568        );
4569        assert!(
4570            ty["units"].is_array(),
4571            "measure-only fields flatten up to top level"
4572        );
4573        assert!(
4574            ty.get("options").is_none(),
4575            "text-only fields must not leak"
4576        );
4577
4578        let quantity = &value["properties"]["quantity"];
4579        assert_eq!(quantity["x-lemma-type"]["kind"], "number");
4580        assert!(
4581            quantity.get("x-lemma-suggestion").is_none(),
4582            "quantity has no suggestion"
4583        );
4584        assert!(
4585            quantity.get("x-lemma-value").is_none(),
4586            "quantity has no fill literal"
4587        );
4588
4589        let cost = &value["x-lemma-rules"]["cost"];
4590        assert_eq!(
4591            cost["type"]["kind"], "measure",
4592            "rule types nest under type"
4593        );
4594        assert!(
4595            cost["type"]["units"].is_array()
4596                && !cost["type"]["units"].as_array().unwrap().is_empty(),
4597            "measure rule result types expose declared units"
4598        );
4599        assert!(
4600            cost["type"]["units"][0].get("factor").is_some(),
4601            "measure rule units use factor field"
4602        );
4603        assert!(cost.get("branches").is_some(), "ShowRule exposes branches");
4604        assert!(
4605            cost.get("depends_on_rules").is_some(),
4606            "ShowRule exposes depends_on_rules"
4607        );
4608    }
4609
4610    #[test]
4611    fn show_rule_result_units_contract() {
4612        let mut engine = Engine::new();
4613        engine
4614            .load([(
4615                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
4616                    "units_contract.lemma",
4617                ))),
4618                r#"
4619                spec units_contract
4620                data money: measure
4621                  -> unit eur: 1
4622                  -> unit usd: 0.91
4623                data rate: ratio
4624                  -> unit basis_points: 10000
4625                  -> unit percent: 100
4626                  -> suggest 500 basis_points
4627                rule total: money
4628                rule rate_out: rate
4629                "#
4630                .to_string(),
4631            )])
4632            .unwrap();
4633        let now = DateTimeValue::now();
4634        let schema = engine.show(None, "units_contract", Some(&now)).unwrap();
4635        let value = crate::api::show_document(&engine, &schema);
4636
4637        let money_units = &value["properties"]["money"]["x-lemma-type"]["units"];
4638        assert!(money_units.is_array() && !money_units.as_array().unwrap().is_empty());
4639        assert!(money_units[0].get("name").is_some());
4640        assert!(money_units[0].get("factor").is_some());
4641        assert!(money_units[0]["factor"].get("numer").is_some());
4642        assert!(money_units[0]["factor"].get("denom").is_some());
4643
4644        let rate_units = &value["properties"]["rate"]["x-lemma-type"]["units"];
4645        assert!(rate_units.is_array() && !rate_units.as_array().unwrap().is_empty());
4646        assert!(rate_units[0].get("name").is_some());
4647        assert!(rate_units[0].get("value").is_some());
4648        assert!(rate_units[0]["value"].get("numer").is_some());
4649        assert!(rate_units[0]["value"].get("denom").is_some());
4650
4651        let total_rule_units = &value["x-lemma-rules"]["total"]["type"]["units"];
4652        let money_unit_names: Vec<_> = money_units
4653            .as_array()
4654            .unwrap()
4655            .iter()
4656            .map(|u| u["name"].as_str().unwrap())
4657            .collect();
4658        let total_rule_unit_names: Vec<_> = total_rule_units
4659            .as_array()
4660            .unwrap()
4661            .iter()
4662            .map(|u| u["name"].as_str().unwrap())
4663            .collect();
4664        assert_eq!(total_rule_unit_names, money_unit_names);
4665
4666        let rate_out_rule_units = &value["x-lemma-rules"]["rate_out"]["type"]["units"];
4667        let rate_unit_names: Vec<_> = rate_units
4668            .as_array()
4669            .unwrap()
4670            .iter()
4671            .map(|u| u["name"].as_str().unwrap())
4672            .collect();
4673        let rate_out_rule_unit_names: Vec<_> = rate_out_rule_units
4674            .as_array()
4675            .unwrap()
4676            .iter()
4677            .map(|u| u["name"].as_str().unwrap())
4678            .collect();
4679        assert_eq!(rate_out_rule_unit_names, rate_unit_names);
4680    }
4681
4682    #[test]
4683    fn show_document_rules_round_trip_as_show_rule() {
4684        let mut engine = Engine::new();
4685        engine
4686            .load([(
4687                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("s.lemma"))),
4688                r#"
4689                spec s
4690                data age: number -> minimum 0 -> suggest 18
4691                data grade: text -> options "A", "B", "C"
4692                rule adult: age >= 18
4693                "#
4694                .to_string(),
4695            )])
4696            .unwrap();
4697        let now = DateTimeValue::now();
4698        let schema = engine.show(None, "s", Some(&now)).unwrap();
4699
4700        let document = crate::api::show_document(&engine, &schema);
4701        let rule_json = document["x-lemma-rules"]["adult"].clone();
4702        let rule: crate::api::ShowRule = serde_json::from_value(rule_json.clone()).unwrap();
4703        assert_eq!(serde_json::to_value(&rule).unwrap(), rule_json);
4704        assert!(
4705            document["properties"].get("grade").is_none(),
4706            "unused grade is Types-only, not Data"
4707        );
4708        let grade = &document["x-lemma-types"]["grade"];
4709        assert_eq!(grade["options"], serde_json::json!(["A", "B", "C"]));
4710        assert_eq!(grade["kind"], "text");
4711    }
4712
4713    const COST_PRICE_SPEC: &str = r#"
4714spec cost_price
4715uses lemma units
4716
4717data money: measure
4718  -> unit eur: 1.00
4719  -> unit inr: 0.0092
4720
4721data labor_cost: measure
4722  -> unit eur_per_hour: eur/hour
4723  -> unit inr_per_hour: inr/hour
4724  -> suggest 25 eur_per_hour
4725
4726data product_cost: measure
4727  -> unit eur_per_kg: eur/kilogram
4728  -> unit inr_per_kg: inr/kilogram
4729  -> suggest 4 eur_per_kg
4730
4731data throughput: measure
4732  -> unit kg_per_hour: kilogram/hour
4733  -> suggest 12 kg_per_hour
4734
4735rule cost_price: product_cost + labor_cost / throughput
4736"#;
4737
4738    fn cost_price_inputs() -> IndexMap<String, EvaluateDataValue> {
4739        let mut data = IndexMap::new();
4740        data.insert(
4741            "product_cost".into(),
4742            EvaluateDataValue::string("4 eur_per_kg"),
4743        );
4744        data.insert(
4745            "labor_cost".into(),
4746            EvaluateDataValue::string("25 eur_per_hour"),
4747        );
4748        data.insert(
4749            "throughput".into(),
4750            EvaluateDataValue::string("12 kg_per_hour"),
4751        );
4752        data
4753    }
4754
4755    const FILM_ACCESS: &str = r#"
4756spec premium_membership
4757uses lemma units
4758data start: date
4759data length: units.calendar
4760rule valid: now in start...start + length
4761
4762spec film_access
4763uses premium_membership
4764data type: text
4765  -> option "rental"
4766  -> option "purchase"
4767data views_consumed: number
4768data premium_member: boolean
4769rule max_views: 3
4770  unless premium_membership.valid then 10
4771  unless premium_member then 5
4772rule can_view: no
4773  unless type is "rental" and views_consumed < max_views then yes
4774  unless type is "purchase" then yes
4775"#;
4776
4777    fn film_access_effective() -> DateTimeValue {
4778        DateTimeValue {
4779            year: 2027,
4780            month: 2,
4781            day: 14,
4782            hour: 12,
4783            minute: 0,
4784            second: 0,
4785            microsecond: 0,
4786            timezone: Some(TimezoneValue { offset_minutes: 0 }),
4787            granularity: DateGranularity::DateTime,
4788        }
4789    }
4790
4791    #[test]
4792    fn evaluate_data_accepts_per_unit_measure_equivalent_to_canonical_magnitude() {
4793        let mut engine = Engine::new();
4794        engine
4795            .load([(
4796                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
4797                    "cost_price.lemma",
4798                ))),
4799                COST_PRICE_SPEC.to_string(),
4800            )])
4801            .expect("load");
4802        let plans = engine
4803            .plans
4804            .get_plans(None, "cost_price")
4805            .expect("plans for cost_price");
4806        let plan = plans.values().next().expect("plan");
4807        let mut data = IndexMap::new();
4808        data.insert(
4809            "product_cost".into(),
4810            EvaluateDataValue::string("4 eur_per_kg"),
4811        );
4812        data.insert(
4813            "labor_cost".into(),
4814            EvaluateDataValue::string("0.0069444444444444444444444444 eur_per_hour"),
4815        );
4816        data.insert(
4817            "throughput".into(),
4818            EvaluateDataValue::string("0.0033333333333333333333333333 kg_per_hour"),
4819        );
4820        let evaluate_data = resolve_evaluate_data(plan, data);
4821        assert!(
4822            !evaluate_data
4823                .bindings
4824                .values()
4825                .any(|b| matches!(b, OperationResult::Veto(_))),
4826            "parsed decimal run data values must not be veto-bound after input boundary: {:?}",
4827            evaluate_data.bindings
4828        );
4829    }
4830
4831    #[test]
4832    fn evaluate_data_accepts_per_unit_measure() {
4833        let mut engine = Engine::new();
4834        engine
4835            .load([(
4836                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
4837                    "cost_price.lemma",
4838                ))),
4839                COST_PRICE_SPEC.to_string(),
4840            )])
4841            .expect("load");
4842        let plans = engine
4843            .plans
4844            .get_plans(None, "cost_price")
4845            .expect("plans for cost_price");
4846        let plan = plans.values().next().expect("plan");
4847        let evaluate_data = resolve_evaluate_data(plan, cost_price_inputs());
4848        assert!(!evaluate_data
4849            .bindings
4850            .values()
4851            .any(|b| matches!(b, OperationResult::Veto(_))));
4852    }
4853
4854    #[test]
4855    fn evaluate_data_rejects_oversize_input() {
4856        let mut engine = Engine::new();
4857        engine
4858            .load([(
4859                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
4860                    "cost_price.lemma",
4861                ))),
4862                COST_PRICE_SPEC.to_string(),
4863            )])
4864            .expect("load");
4865        let plans = engine
4866            .plans
4867            .get_plans(None, "cost_price")
4868            .expect("plans for cost_price");
4869        let plan = plans.values().next().expect("plan");
4870        let mut data = cost_price_inputs();
4871        data.insert(
4872            "labor_cost".into(),
4873            EvaluateDataValue::string(
4874                "1000000000000000000000000000000000000000000000000000000000000 eur_per_hour",
4875            ),
4876        );
4877        let evaluate_data = resolve_evaluate_data(plan, data);
4878        assert!(matches!(
4879            binding(plan, &evaluate_data, &DataPath::local("labor_cost".into())),
4880            Some(OperationResult::Veto(_))
4881        ));
4882    }
4883    #[test]
4884    fn typedecl_default_stays_typedecl_on_immutable_plan() {
4885        let mut engine = Engine::new();
4886        engine
4887            .load([(
4888                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("s.lemma"))),
4889                r#"
4890        spec s
4891        data n: number -> suggest 42
4892        rule r: n
4893    "#
4894                .to_string(),
4895            )])
4896            .expect("load");
4897
4898        let plans = engine.plans.get_plans(None, "s").expect("plans for s");
4899        let plan = plans.values().next().expect("plan");
4900        let path = DataPath::local("n".into());
4901        match plan.data.get(&path).expect("n") {
4902            DataDefinition::TypeDeclaration {
4903                declared_suggestion: Some(_),
4904                ..
4905            } => {}
4906            other => panic!("expected TypeDeclaration with default, got {other:?}"),
4907        }
4908    }
4909
4910    fn response_missing_data_union(response: &crate::Response) -> Vec<String> {
4911        let mut seen = std::collections::HashSet::new();
4912        let mut names = Vec::new();
4913        for result in response.results.values() {
4914            for key in result.missing_data() {
4915                if seen.insert(key.clone()) {
4916                    names.push(key.clone());
4917                }
4918            }
4919        }
4920        names
4921    }
4922
4923    #[test]
4924    fn evaluate_prunes_inactive_kind_branches_for_total_price() {
4925        let code = r#"
4926spec bag
4927uses lemma units
4928
4929data weight: measure
4930  -> unit kg: 1
4931
4932data money: measure
4933  -> unit eur: 1
4934
4935data price_per_weight: measure
4936  -> unit eur_per_kg: eur/kg
4937
4938data item_cost: price_per_weight
4939data finish_a: price_per_weight
4940data finish_b: price_per_weight
4941
4942rule total_price: weight * (item_cost + finish_a + finish_b)
4943
4944spec calc
4945uses bag
4946  -> with item_cost: item_cost
4947  -> with finish_a: finish_a
4948
4949data kind: text -> options "red", "blue"
4950
4951rule price_red: 1.5 eur_per_kg
4952rule price_red_finish_a: 0.45 eur_per_kg
4953
4954rule price_blue: 2.0 eur_per_kg
4955rule price_blue_finish_a: 0.55 eur_per_kg
4956
4957rule item_cost: veto "No item cost"
4958  unless kind is "red" then price_red
4959  unless kind is "blue" then price_blue
4960
4961rule finish_a: veto "No finish_a"
4962  unless kind is "red" then price_red_finish_a
4963  unless kind is "blue" then price_blue_finish_a
4964
4965rule total_price: bag.total_price
4966"#;
4967
4968        let mut engine = Engine::new();
4969        engine
4970            .load([(
4971                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
4972                    "calc.lemma",
4973                ))),
4974                code.to_string(),
4975            )])
4976            .unwrap();
4977
4978        let now = DateTimeValue::now();
4979        let mut inputs = IndexMap::new();
4980        inputs.insert("kind".to_string(), "red".into());
4981        let response = engine
4982            .evaluate(
4983                None,
4984                "calc",
4985                Some(&now),
4986                inputs,
4987                Some(&["total_price".to_string()]),
4988                false,
4989            )
4990            .expect("evaluate must succeed");
4991
4992        assert_eq!(
4993            response_missing_data_union(&response),
4994            vec!["bag.weight".to_string(), "bag.finish_b".to_string(),],
4995            "supplied kind and with-bound finish_a/item_cost omitted"
4996        );
4997    }
4998
4999    #[test]
5000    fn evaluate_includes_membership_dates_when_premium_member_false() {
5001        let mut engine = Engine::new();
5002        engine
5003            .load([(crate::SourceType::Volatile, FILM_ACCESS.to_string())])
5004            .expect("film_access spec must load");
5005        let now = film_access_effective();
5006        let mut inputs = IndexMap::new();
5007        inputs.insert("type".to_string(), "rental".into());
5008        inputs.insert("views_consumed".to_string(), "6".into());
5009        inputs.insert("premium_member".to_string(), "false".into());
5010        let response = engine
5011            .evaluate(
5012                None,
5013                "film_access",
5014                Some(&now),
5015                inputs,
5016                Some(&["can_view".to_string()]),
5017                false,
5018            )
5019            .expect("evaluate must succeed");
5020
5021        assert_eq!(
5022            response_missing_data_union(&response),
5023            vec![
5024                "premium_membership.start".to_string(),
5025                "premium_membership.length".to_string(),
5026            ],
5027            "membership dates when premium_member is false"
5028        );
5029    }
5030
5031    #[test]
5032    fn evaluate_includes_membership_dates_when_premium_member_unknown() {
5033        let mut engine = Engine::new();
5034        engine
5035            .load([(crate::SourceType::Volatile, FILM_ACCESS.to_string())])
5036            .expect("film_access spec must load");
5037        let now = film_access_effective();
5038        let mut inputs = IndexMap::new();
5039        inputs.insert("type".to_string(), "rental".into());
5040        inputs.insert("views_consumed".to_string(), "6".into());
5041        let response = engine
5042            .evaluate(
5043                None,
5044                "film_access",
5045                Some(&now),
5046                inputs,
5047                Some(&["can_view".to_string()]),
5048                false,
5049            )
5050            .expect("evaluate must succeed");
5051
5052        assert_eq!(
5053            response_missing_data_union(&response),
5054            vec![
5055                "premium_member".to_string(),
5056                "premium_membership.start".to_string(),
5057                "premium_membership.length".to_string(),
5058            ],
5059            "premium_member plus membership dates when unknown"
5060        );
5061    }
5062
5063    const UNITS_SPEC: &str = r#"
5064spec units
5065uses lemma units
5066data money: measure
5067  -> unit eur: 1
5068"#;
5069
5070    const WAREHOUSING_SPEC: &str = r#"
5071spec warehousing
5072uses units
5073uses si: lemma units
5074
5075data units_per_pallet: number
5076  -> minimum 1
5077  -> suggest 1
5078
5079data storage_duration: si.duration
5080  -> minimum 0 week
5081  -> suggest 10 day
5082
5083data interbranch_transport_per_pallet: units.money
5084  -> minimum 0 eur
5085  -> suggest 0 eur
5086
5087data inbound_handling_per_pallet: units.money
5088  -> minimum 0 eur
5089  -> suggest 0 eur
5090
5091data storage_per_pallet_per_week: units.money
5092  -> minimum 0 eur
5093  -> suggest 10 eur
5094
5095data labeling_per_pallet: units.money
5096  -> minimum 0 eur
5097  -> suggest 0 eur
5098
5099data outbound_handling_per_pallet: units.money
5100  -> minimum 0 eur
5101  -> suggest 0 eur
5102
5103rule storage_cost_per_pallet:
5104  storage_per_pallet_per_week
5105  * (storage_duration as week as number -> ceil)
5106
5107rule total_logistics_per_pallet:
5108  interbranch_transport_per_pallet
5109  + inbound_handling_per_pallet
5110  + storage_cost_per_pallet
5111  + labeling_per_pallet
5112  + outbound_handling_per_pallet
5113
5114rule total_logistics_per_ce:
5115  total_logistics_per_pallet / units_per_pallet
5116"#;
5117
5118    const QUOTATION_SPEC: &str = r#"
5119spec quotation
5120uses wh: warehousing
5121rule total: wh.total_logistics_per_ce
5122"#;
5123
5124    fn load_cross_spec_fixtures(engine: &mut Engine) {
5125        engine
5126            .load([(crate::SourceType::Volatile, UNITS_SPEC.to_string())])
5127            .expect("units spec must load");
5128        engine
5129            .load([(crate::SourceType::Volatile, WAREHOUSING_SPEC.to_string())])
5130            .expect("warehousing spec must load");
5131    }
5132
5133    #[test]
5134    fn quotation_inherits_unit_scope_from_warehousing_import() {
5135        let mut engine = Engine::new();
5136        load_cross_spec_fixtures(&mut engine);
5137        engine
5138            .load([(crate::SourceType::Volatile, QUOTATION_SPEC.to_string())])
5139            .expect("quotation must plan without uses lemma units");
5140        let plans = engine
5141            .plans
5142            .get_plans(None, "quotation")
5143            .expect("plans for quotation");
5144        let plan = plans.values().next().expect("plan");
5145
5146        let expression_units = &plan.resolved_types.unit_index;
5147        expression_units
5148            .resolve("week")
5149            .expect("week reaches quotation via wh → si");
5150        expression_units
5151            .resolve("wh.week")
5152            .expect("alias.unit resolves week in warehousing scope");
5153        expression_units
5154            .resolve("minute")
5155            .expect("minute reaches quotation via transitive unit scope");
5156    }
5157
5158    fn warehousing_default_inputs(prefix: &str) -> IndexMap<String, EvaluateDataValue> {
5159        let key = |name: &str| {
5160            if prefix.is_empty() {
5161                name.to_string()
5162            } else {
5163                format!("{prefix}.{name}")
5164            }
5165        };
5166        IndexMap::from([
5167            (key("units_per_pallet"), EvaluateDataValue::string("1")),
5168            (key("storage_duration"), EvaluateDataValue::string("10 day")),
5169            (
5170                key("interbranch_transport_per_pallet"),
5171                EvaluateDataValue::string("0 eur"),
5172            ),
5173            (
5174                key("inbound_handling_per_pallet"),
5175                EvaluateDataValue::string("0 eur"),
5176            ),
5177            (
5178                key("storage_per_pallet_per_week"),
5179                EvaluateDataValue::string("10 eur"),
5180            ),
5181            (
5182                key("labeling_per_pallet"),
5183                EvaluateDataValue::string("0 eur"),
5184            ),
5185            (
5186                key("outbound_handling_per_pallet"),
5187                EvaluateDataValue::string("0 eur"),
5188            ),
5189        ])
5190    }
5191
5192    #[test]
5193    fn quotation_evaluates_cross_spec_duration_conversion() {
5194        let mut engine = Engine::new();
5195        load_cross_spec_fixtures(&mut engine);
5196        engine
5197            .load([(crate::SourceType::Volatile, QUOTATION_SPEC.to_string())])
5198            .expect("quotation must load");
5199        let plans = engine
5200            .plans
5201            .get_plans(None, "quotation")
5202            .expect("plans for quotation");
5203        let plan = plans.values().next().expect("plan");
5204        assert!(
5205            plan.resolved_types
5206                .unit_index
5207                .unique_owner("week")
5208                .is_none(),
5209            "consumer unit_index must not contain week"
5210        );
5211        let now = DateTimeValue::now();
5212        let response = engine
5213            .evaluate(
5214                None,
5215                "quotation",
5216                Some(&now),
5217                warehousing_default_inputs("wh"),
5218                None,
5219                false,
5220            )
5221            .expect("quotation must evaluate");
5222        let display = response
5223            .results
5224            .get("total")
5225            .expect("rule total must be present")
5226            .result()
5227            .expect("total must have display")
5228            .to_string();
5229        assert_eq!(
5230            display, "20 eur",
5231            "10 eur/week * ceil(10 day as week) / 1 CE must be 20 eur, got: {display}"
5232        );
5233    }
5234
5235    #[test]
5236    fn ratio_range_default_endpoints_must_be_ratio_not_measure() {
5237        let code = r#"
5238spec policy
5239data allowed_band: ratio range -> suggest 10%...50%
5240rule band: allowed_band
5241"#;
5242        let mut engine = Engine::new();
5243        engine
5244            .load([(
5245                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
5246                    "ratio_range_endpoint_typing.lemma",
5247                ))),
5248                code.to_string(),
5249            )])
5250            .unwrap();
5251
5252        let plans = engine
5253            .plans
5254            .get_plans(None, "policy")
5255            .expect("plans for policy");
5256        let plan = plans.values().next().expect("plan");
5257        let path = DataPath::local("allowed_band".into());
5258        let def = plan.data.get(&path).expect("allowed_band in plan.data");
5259        let suggestion = def
5260            .bound_suggestion()
5261            .expect("declared default must exist")
5262            .to_literal();
5263
5264        let (left, right) = match &suggestion.value {
5265            crate::planning::semantics::ValueKind::Range(l, r) => (l.as_ref(), r.as_ref()),
5266            other => panic!("expected Range, got {other:?}"),
5267        };
5268        for (label, endpoint) in [("left", left), ("right", right)] {
5269            assert!(
5270                matches!(
5271                    &endpoint.value,
5272                    crate::planning::semantics::ValueKind::Ratio(_)
5273                ),
5274                "{label} endpoint must be Ratio for a percent literal in a ratio range default, got {:?}",
5275                endpoint.value
5276            );
5277            assert!(
5278                matches!(
5279                    &endpoint.value,
5280                    crate::planning::semantics::ValueKind::Ratio(_)
5281                ),
5282                "{label} endpoint ValueKind must be Ratio (got {:?})",
5283                endpoint.value
5284            );
5285        }
5286    }
5287
5288    #[test]
5289    fn ratio_range_typedef_with_second_ratio_field_loads() {
5290        let code = r#"
5291spec policy
5292data margin_pct: ratio -> suggest 15%
5293data allowed_band: ratio range
5294rule margin: margin_pct
5295rule band_slot: allowed_band
5296"#;
5297        let mut engine = Engine::new();
5298        engine
5299            .load([(
5300                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
5301                    "ratio_range_load.lemma",
5302                ))),
5303                code.to_string(),
5304            )])
5305            .unwrap();
5306
5307        let plans = engine
5308            .plans
5309            .get_plans(None, "policy")
5310            .expect("plans for policy");
5311        let plan = plans.values().next().expect("plan");
5312        let path = DataPath::local("allowed_band".into());
5313        let def = plan.data.get(&path).expect("allowed_band in plan.data");
5314        let lemma_type = def
5315            .schema_type()
5316            .expect("allowed_band must be a typed data slot");
5317        match &lemma_type.specifications {
5318            TypeSpecification::RatioRange { units, .. } => {
5319                let names: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
5320                assert!(
5321                    names.contains(&"percent"),
5322                    "ratio range must inherit builtin percent, got {names:?}"
5323                );
5324            }
5325            other => panic!("allowed_band must be RatioRange, got {other:?}"),
5326        }
5327    }
5328
5329    #[test]
5330    fn show_display_indents_every_commentary_line() {
5331        let mut engine = Engine::new();
5332        engine
5333            .load([(
5334                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
5335                    "alpha2.lemma",
5336                ))),
5337                "spec alpha2\n\"\"\"\nAdded:\nSS: South Sudan\n\nRemoved:\nYU: Yugoslavia\n\"\"\"\ndata code: text\nrule name: code\n"
5338                    .to_string(),
5339            )])
5340            .unwrap();
5341        let now = DateTimeValue::now();
5342        let text = engine.show(None, "alpha2", Some(&now)).unwrap().to_string();
5343        assert!(
5344            text.starts_with(
5345                "Spec: alpha2\n  Added:\n  SS: South Sudan\n\n  Removed:\n  YU: Yugoslavia\n"
5346            ),
5347            "{text}"
5348        );
5349    }
5350
5351    #[test]
5352    fn show_display_puts_effective_window_on_header_without_other_versions() {
5353        let mut engine = Engine::new();
5354        engine
5355            .load([(
5356                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
5357                    "rates.lemma",
5358                ))),
5359                "spec rates 2024-01-01\n\"\"\"\nStandard rate\n\"\"\"\ndata x: 1\nrule r: x\n\nspec rates 2025-01-01\ndata x: 2\nrule r: x\n"
5360                    .to_string(),
5361            )])
5362            .unwrap();
5363        let effective: DateTimeValue = "2024-06-01".parse().expect("date must parse");
5364        let text = engine
5365            .show(None, "rates", Some(&effective))
5366            .unwrap()
5367            .to_string();
5368        assert!(
5369            text.starts_with("Spec: rates 2024-01-01...2025-01-01\n  Standard rate\n"),
5370            "{text}"
5371        );
5372        assert!(!text.contains("versions"), "{text}");
5373        assert!(!text.contains("effective_"), "{text}");
5374    }
5375
5376    #[test]
5377    fn show_display_undated_predecessor_uses_until() {
5378        let mut engine = Engine::new();
5379        engine
5380            .load([(
5381                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
5382                    "calc.lemma",
5383                ))),
5384                "spec calc\ndata x: 1\nrule r: x\n\nspec calc 2027\ndata x: 2\nrule r: x\n"
5385                    .to_string(),
5386            )])
5387            .unwrap();
5388        let effective: DateTimeValue = "2026-01-01".parse().expect("date must parse");
5389        let text = engine
5390            .show(None, "calc", Some(&effective))
5391            .unwrap()
5392            .to_string();
5393        assert!(text.starts_with("Spec: calc ...2027\n"), "{text}");
5394        assert!(!text.contains('→'), "{text}");
5395    }
5396
5397    fn nested_import_plan() -> (Engine, Arc<ExecutionPlan>) {
5398        let mut engine = Engine::new();
5399        engine
5400            .load([(
5401                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
5402                    "peel.lemma",
5403                ))),
5404                r#"
5405spec leaf
5406data x: number
5407rule rx: x
5408
5409spec mid
5410uses nested: leaf
5411data y: number
5412rule ry: y
5413
5414spec outer
5415uses m: mid
5416"#
5417                .to_string(),
5418            )])
5419            .expect("load nested import fixture");
5420        let plans = engine
5421            .plans
5422            .get_plans(None, "outer")
5423            .expect("plans for outer");
5424        let plan = Arc::clone(plans.values().next().expect("outer plan"));
5425        (engine, plan)
5426    }
5427
5428    /// Template plan of import `alias` of `plan`.
5429    fn import_template<'a>(plan: &'a ExecutionPlan, alias: &str) -> &'a ExecutionPlan {
5430        let Some(DataDefinition::Import { row, .. }) =
5431            plan.data.get(&DataPath::local(alias.to_string()))
5432        else {
5433            panic!("`{alias}` must be an import of `{}`", plan.spec_name);
5434        };
5435        plan.row_template(&row.identity)
5436    }
5437
5438    #[test]
5439    fn input_key_index_covers_own_slots_only() {
5440        let (_, plan) = nested_import_plan();
5441
5442        let mut entry_keys: Vec<String> = plan.data.keys().map(DataPath::input_key).collect();
5443        let mut index_keys: Vec<String> = plan.input_key_index.keys().cloned().collect();
5444        entry_keys.sort();
5445        index_keys.sort();
5446        assert_eq!(entry_keys, index_keys);
5447        assert_eq!(entry_keys, vec!["m".to_string()]);
5448        assert_eq!(plan.input_slots().count(), 0);
5449    }
5450
5451    #[test]
5452    fn import_rows_carry_their_template_data() {
5453        let (_, plan) = nested_import_plan();
5454
5455        let mid = import_template(&plan, "m");
5456        assert_eq!(mid.spec_name, "mid");
5457        assert!(matches!(
5458            mid.data.get(&DataPath::local("y".into())),
5459            Some(DataDefinition::Value { .. } | DataDefinition::TypeDeclaration { .. })
5460        ));
5461        let leaf = import_template(mid, "nested");
5462        assert_eq!(leaf.spec_name, "leaf");
5463        assert!(matches!(
5464            leaf.data.get(&DataPath::local("x".into())),
5465            Some(DataDefinition::Value { .. } | DataDefinition::TypeDeclaration { .. })
5466        ));
5467        assert!(plan
5468            .data
5469            .keys()
5470            .chain(mid.data.keys())
5471            .chain(leaf.data.keys())
5472            .all(|path| path.segments.is_empty()));
5473    }
5474
5475    #[test]
5476    fn rules_of_imports_stay_in_their_templates() {
5477        let (_, plan) = nested_import_plan();
5478
5479        assert!(plan.rules.is_empty());
5480        let mid = import_template(&plan, "m");
5481        let leaf = import_template(mid, "nested");
5482        let keys = |template: &ExecutionPlan| -> Vec<String> {
5483            template.rules.keys().map(RulePath::input_key).collect()
5484        };
5485        assert_eq!(keys(mid), vec!["ry".to_string()]);
5486        assert_eq!(keys(leaf), vec!["rx".to_string()]);
5487        assert!(plan.get_rule("m.ry").is_none());
5488    }
5489
5490    #[test]
5491    fn dotted_key_resolves_through_import_templates() {
5492        let (_, plan) = nested_import_plan();
5493        let (template, definition, _, field) =
5494            crate::evaluation::evaluate_data::row_field_definition(&plan, "m.nested.x")
5495                .expect("`m.nested.x` names data of the leaf template");
5496        assert_eq!(template.spec_name, "leaf");
5497        assert_eq!(field.data, "x");
5498        assert!(matches!(
5499            definition,
5500            DataDefinition::Value { .. } | DataDefinition::TypeDeclaration { .. }
5501        ));
5502        assert!(crate::evaluation::evaluate_data::row_field_definition(&plan, "m.x").is_none());
5503        let (template, definition, _, _) =
5504            crate::evaluation::evaluate_data::row_field_definition(&plan, "m.nested")
5505                .expect("`m.nested` names the `uses` slot of the mid template");
5506        assert_eq!(template.spec_name, "mid");
5507        assert!(matches!(definition, DataDefinition::Import { .. }));
5508    }
5509}