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