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