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    let first = path.segments.first()?;
1708    let import_marker = DataPath::local(first.uses.clone());
1709    let DataDefinition::Import { overlay, .. } = plan.data.get(&import_marker)? else {
1710        return None;
1711    };
1712    let relative = DataPath {
1713        segments: path.segments[1..].to_vec(),
1714        data: path.data.clone(),
1715    };
1716    match overlay.get(&relative)? {
1717        ImportOverlay::Literal(bound) => Some(bound.clone()),
1718        ImportOverlay::Reference { .. } => None,
1719    }
1720}
1721
1722fn insert_data_display_entry(
1723    out: &mut IndexMap<DataPath, ShowDataCache>,
1724    path: &DataPath,
1725    data: &DataDefinition,
1726) {
1727    if data.schema_type().is_none() || matches!(data, DataDefinition::Reference { .. }) {
1728        return;
1729    }
1730    let value = match data {
1731        DataDefinition::Value {
1732            value,
1733            resolved_type,
1734            ..
1735        } => Some(crate::planning::semantics::BoundValueKind {
1736            value: value.value.clone(),
1737            unit: resolved_type.unit().map(Arc::from),
1738        }),
1739        _ => data.bound_value().cloned(),
1740    };
1741    let suggestion = data.bound_suggestion().cloned();
1742    if value.is_some() || suggestion.is_some() {
1743        out.insert(path.clone(), ShowDataCache { value, suggestion });
1744    }
1745}
1746
1747/// Whether a show bound value is the fill or the suggestion for a data path.
1748#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1749pub(crate) enum ShowBoundRole {
1750    Value,
1751    Suggestion,
1752}
1753
1754pub(crate) fn show_bound_result_value(
1755    bound: &crate::planning::semantics::BoundValueKind,
1756    schema_type: &crate::planning::semantics::LemmaType,
1757    input_key: &str,
1758    role: ShowBoundRole,
1759) -> crate::result_value::RuleResultValue {
1760    let (fill, suggestion) = match role {
1761        ShowBoundRole::Value => (Some(bound), None),
1762        ShowBoundRole::Suggestion => (None, Some(bound)),
1763    };
1764    let display_type = crate::planning::semantics::lemma_type_with_display_binding(
1765        schema_type,
1766        None,
1767        fill,
1768        suggestion,
1769    );
1770    crate::result_value::type_scoped_result_value_from_literal(&bound.to_literal(), &display_type)
1771        .unwrap_or_else(|failure| {
1772            let role_name = match role {
1773                ShowBoundRole::Value => "value",
1774                ShowBoundRole::Suggestion => "suggestion",
1775            };
1776            panic!(
1777                "BUG: show {role_name} value for '{input_key}' failed type_scoped_result_value_from_literal: {}",
1778                crate::result_value::rule_result_value_failure_message(failure)
1779            )
1780        })
1781}
1782
1783/// Show value of instance list `path`: its source rows in order, each with the fields
1784/// it sets and no `result` (a row has no value of its own to display). Literal fields
1785/// carry their value; referenced fields carry [`RuleResultValue::reference`]. `None`
1786/// when the list has no source rows.
1787pub(crate) fn show_instance_list_value(
1788    plan: &ExecutionPlan,
1789    path: &DataPath,
1790) -> Option<RuleResultValue> {
1791    let source = plan
1792        .instance_lists
1793        .get(path)
1794        .expect("BUG: instance list data without a source");
1795    if source.rows.is_empty() {
1796        return None;
1797    }
1798    let template = plan.row_template(&source.identity);
1799    let mut items = Vec::with_capacity(source.rows.len());
1800    for row in source.rows.iter() {
1801        let mut fields = IndexMap::with_capacity(row.fields.len());
1802        for (field, row_source) in &row.fields {
1803            let key = field.input_key();
1804            let value = match row_source {
1805                DataSource::Literal(bound) => {
1806                    let field_type = row_end_type(template, &RowEnd::Data(field.clone()));
1807                    show_bound_result_value(bound, &field_type, &key, ShowBoundRole::Value)
1808                }
1809                DataSource::Reference(relative) => {
1810                    let consumer =
1811                        crate::planning::instance_list::absolute_reference(path, relative);
1812                    RuleResultValue {
1813                        reference: Some(consumer.input_key()),
1814                        ..Default::default()
1815                    }
1816                }
1817            };
1818            fields.insert(key, value);
1819        }
1820        items.push(RuleResultValue {
1821            fields: Some(fields),
1822            ..Default::default()
1823        });
1824    }
1825    Some(RuleResultValue {
1826        list: Some(items),
1827        ..Default::default()
1828    })
1829}
1830
1831/// One data entry in a [`Show`].
1832///
1833/// A named struct instead of a tuple so JSON-native consumers (TypeScript, Python, ...)
1834/// get stable field names. `fill` is a spec literal or literal `with` binding;
1835/// `suggestion` is a `-> suggest ...` hint only.
1836/// Empty `needed_by_rules` means the slot is offered for reuse (`data x: alias.slot`),
1837/// not needed by this spec's remaining rules after normalize.
1838#[derive(Debug, Clone, PartialEq)]
1839pub struct ShowData {
1840    pub lemma_type: LemmaType,
1841    /// Import hops to this slot (`[]` = this spec). Clone of [`DataPath::segments`].
1842    pub path: Vec<crate::planning::semantics::PathSegment>,
1843    pub value: Option<RuleResultValue>,
1844    pub suggestion: Option<RuleResultValue>,
1845    /// Show.rules keys that transitively need this data after normalize.
1846    /// Show.data only includes slots where this is non-empty.
1847    pub needed_by_rules: Vec<String>,
1848}
1849
1850/// Consumer [`Engine::show`] result: local offered types, inputs rules need,
1851/// this spec's rule graph (local rules plus reachable imports), and resolved
1852/// temporal window. Source: [`Engine::source`].
1853#[derive(Debug, Clone, PartialEq)]
1854pub struct Show {
1855    /// Interned repository name (`specs()` string). `None` = unnamed workspace.
1856    pub repository: Option<String>,
1857    pub spec: String,
1858    pub commentary: Option<String>,
1859    pub effective_from: Option<crate::parsing::ast::DateTimeValue>,
1860    pub effective_to: Option<crate::parsing::ast::DateTimeValue>,
1861    /// Planning instant of the execution-plan slice that produced this Show.
1862    /// Distinguishes temporal dependency slices that share the same declared window.
1863    pub plan_effective: EffectiveDate,
1864    pub start_line: usize,
1865    pub source_type: Option<crate::parsing::source::SourceType>,
1866    /// Local `data` declarations (empty path; type or value), declaration order.
1867    pub types: indexmap::IndexMap<String, LemmaType>,
1868    /// Inputs rules need (`needed_by_rules` non-empty), local or imported.
1869    pub data: indexmap::IndexMap<String, ShowData>,
1870    pub rules: indexmap::IndexMap<String, ShowRule>,
1871    /// Spec metadata, in declaration order.
1872    pub meta: IndexMap<String, Value>,
1873}
1874
1875impl std::fmt::Display for Show {
1876    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1877        match &self.repository {
1878            Some(repository) => write!(f, "Spec: {} {}", repository, self.spec)?,
1879            None => write!(f, "Spec: {}", self.spec)?,
1880        }
1881        crate::engine::write_effective_window(
1882            f,
1883            self.effective_from.as_ref(),
1884            self.effective_to.as_ref(),
1885        )?;
1886
1887        if let Some(commentary) = &self.commentary {
1888            for line in commentary.lines() {
1889                if line.is_empty() {
1890                    writeln!(f)?;
1891                } else {
1892                    write!(f, "\n  {line}")?;
1893                }
1894            }
1895        }
1896
1897        if !self.meta.is_empty() {
1898            write!(f, "\n\nMeta:")?;
1899            let mut entries: Vec<(&String, &Value)> = self.meta.iter().collect();
1900            entries.sort_by_key(|(k, _)| *k);
1901            for (key, value) in entries {
1902                write!(f, "\n  {}: {}", key, value)?;
1903            }
1904        }
1905
1906        if !self.types.is_empty() {
1907            write!(f, "\n\nTypes:")?;
1908            for (name, lemma_type) in &self.types {
1909                write!(f, "\n  {} ({})", name, lemma_type.specifications)?;
1910                for line in type_detail_lines(&lemma_type.specifications) {
1911                    write!(f, "\n    {}", line)?;
1912                }
1913                let help = lemma_type.specifications.help();
1914                if !help.is_empty() {
1915                    write!(f, "\n    help: {}", help)?;
1916                }
1917            }
1918        }
1919
1920        if !self.data.is_empty() {
1921            write!(f, "\n\nData:")?;
1922            for (name, entry) in &self.data {
1923                write!(f, "\n  {} ({})", name, entry.lemma_type.specifications)?;
1924                for line in type_detail_lines(&entry.lemma_type.specifications) {
1925                    write!(f, "\n    {}", line)?;
1926                }
1927                let help = entry.lemma_type.specifications.help();
1928                if !help.is_empty() {
1929                    write!(f, "\n    help: {}", help)?;
1930                }
1931                match &entry.value {
1932                    Some(RuleResultValue {
1933                        list: Some(rows), ..
1934                    }) if rows.iter().all(|row| row.fields.is_some()) => {
1935                        write!(f, "\n    value:")?;
1936                        for (index, row) in rows.iter().enumerate() {
1937                            let fields = row
1938                                .fields
1939                                .as_ref()
1940                                .expect("BUG: guard checked every row has fields")
1941                                .iter()
1942                                .map(|(name, field)| format!("{name} {field}"))
1943                                .collect::<Vec<_>>()
1944                                .join(", ");
1945                            write!(f, "\n      {}: {fields}", index + 1)?;
1946                        }
1947                    }
1948                    Some(val) => write!(f, "\n    value: {}", val)?,
1949                    None => {}
1950                }
1951                if let Some(val) = &entry.suggestion {
1952                    write!(f, "\n    suggestion: {}", val)?;
1953                }
1954                if !entry.needed_by_rules.is_empty() {
1955                    write!(
1956                        f,
1957                        "\n    needed_by_rules: {}",
1958                        entry.needed_by_rules.join(", ")
1959                    )?;
1960                }
1961            }
1962        }
1963
1964        if !self.rules.is_empty() {
1965            write!(f, "\n\nRules:")?;
1966            for (name, rule) in &self.rules {
1967                write!(f, "\n  {} ({})", name, rule.lemma_type.specifications)?;
1968                if !rule.depends_on_rules.is_empty() {
1969                    write!(
1970                        f,
1971                        "\n    depends_on_rules: {}",
1972                        rule.depends_on_rules.join(", ")
1973                    )?;
1974                }
1975            }
1976        }
1977
1978        if self.types.is_empty() && self.data.is_empty() && self.rules.is_empty() {
1979            write!(f, "\n  (no types, data, or rules)")?;
1980        }
1981
1982        Ok(())
1983    }
1984}
1985
1986/// Produce a human-readable summary of type constraints, or `None` when there
1987/// are no constraints worth showing (e.g. bare `boolean`).
1988/// Returns one formatted string per constraint or property of the type specification.
1989/// Uses `display_str` for all rational bounds so they render as decimals,
1990/// not as raw fractions.
1991pub fn type_detail_lines(spec: &TypeSpecification) -> Vec<String> {
1992    let mut lines = Vec::new();
1993    match spec {
1994        TypeSpecification::Measure {
1995            minimum,
1996            maximum,
1997            decimals,
1998            units,
1999            ..
2000        } => {
2001            let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
2002            if !unit_names.is_empty() {
2003                lines.push(format!("units: {}", unit_names.join(", ")));
2004            }
2005            if let Some(d) = decimals {
2006                lines.push(format!("decimals: {}", d));
2007            }
2008            if let Some((magnitude, unit_name)) = minimum {
2009                lines.push(format!(
2010                    "minimum: {} {}",
2011                    magnitude.display_str(),
2012                    unit_name
2013                ));
2014            }
2015            if let Some((magnitude, unit_name)) = maximum {
2016                lines.push(format!(
2017                    "maximum: {} {}",
2018                    magnitude.display_str(),
2019                    unit_name
2020                ));
2021            }
2022        }
2023        TypeSpecification::Number {
2024            minimum,
2025            maximum,
2026            decimals,
2027            ..
2028        } => {
2029            if let Some(d) = decimals {
2030                lines.push(format!("decimals: {}", d));
2031            }
2032            if let Some(v) = minimum {
2033                lines.push(format!("minimum: {}", v.display_str()));
2034            }
2035            if let Some(v) = maximum {
2036                lines.push(format!("maximum: {}", v.display_str()));
2037            }
2038        }
2039        TypeSpecification::Ratio {
2040            minimum,
2041            maximum,
2042            decimals,
2043            units,
2044            ..
2045        } => {
2046            let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
2047            if !unit_names.is_empty() {
2048                lines.push(format!("units: {}", unit_names.join(", ")));
2049            }
2050            if let Some(d) = decimals {
2051                lines.push(format!("decimals: {}", d));
2052            }
2053            if let Some(v) = minimum {
2054                lines.push(format!("minimum: {}", v.display_str()));
2055            }
2056            if let Some(v) = maximum {
2057                lines.push(format!("maximum: {}", v.display_str()));
2058            }
2059        }
2060        TypeSpecification::Text {
2061            options, length, ..
2062        } => {
2063            if let Some(l) = length {
2064                lines.push(format!("length: {}", l));
2065            }
2066            if !options.is_empty() {
2067                let quoted: Vec<String> = options.iter().map(|o| format!("\"{}\"", o)).collect();
2068                lines.push(format!("options: {}", quoted.join(", ")));
2069            }
2070        }
2071        TypeSpecification::Date {
2072            minimum, maximum, ..
2073        } => {
2074            if let Some(v) = minimum {
2075                lines.push(format!("minimum: {}", v));
2076            }
2077            if let Some(v) = maximum {
2078                lines.push(format!("maximum: {}", v));
2079            }
2080        }
2081        TypeSpecification::Time {
2082            minimum, maximum, ..
2083        } => {
2084            if let Some(v) = minimum {
2085                lines.push(format!("minimum: {}", v));
2086            }
2087            if let Some(v) = maximum {
2088                lines.push(format!("maximum: {}", v));
2089            }
2090        }
2091        TypeSpecification::MeasureRange {
2092            lower,
2093            upper,
2094            minimum,
2095            maximum,
2096            units,
2097            ..
2098        } => {
2099            let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
2100            if !unit_names.is_empty() {
2101                lines.push(format!("units: {}", unit_names.join(", ")));
2102            }
2103            if let Some((magnitude, unit_name)) = lower {
2104                lines.push(format!("lower: {} {}", magnitude.display_str(), unit_name));
2105            }
2106            if let Some((magnitude, unit_name)) = upper {
2107                lines.push(format!("upper: {} {}", magnitude.display_str(), unit_name));
2108            }
2109            if let Some((magnitude, unit_name)) = minimum {
2110                lines.push(format!(
2111                    "minimum: {} {}",
2112                    magnitude.display_str(),
2113                    unit_name
2114                ));
2115            }
2116            if let Some((magnitude, unit_name)) = maximum {
2117                lines.push(format!(
2118                    "maximum: {} {}",
2119                    magnitude.display_str(),
2120                    unit_name
2121                ));
2122            }
2123        }
2124        TypeSpecification::RatioRange {
2125            lower,
2126            upper,
2127            minimum,
2128            maximum,
2129            units,
2130            ..
2131        } => {
2132            let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
2133            if !unit_names.is_empty() {
2134                lines.push(format!("units: {}", unit_names.join(", ")));
2135            }
2136            if let Some(v) = lower {
2137                lines.push(format!("lower: {}", v.display_str()));
2138            }
2139            if let Some(v) = upper {
2140                lines.push(format!("upper: {}", v.display_str()));
2141            }
2142            if let Some(v) = minimum {
2143                lines.push(format!("minimum: {}", v.display_str()));
2144            }
2145            if let Some(v) = maximum {
2146                lines.push(format!("maximum: {}", v.display_str()));
2147            }
2148        }
2149        TypeSpecification::NumberRange {
2150            lower,
2151            upper,
2152            minimum,
2153            maximum,
2154            ..
2155        } => {
2156            if let Some(v) = lower {
2157                lines.push(format!("lower: {}", v.display_str()));
2158            }
2159            if let Some(v) = upper {
2160                lines.push(format!("upper: {}", v.display_str()));
2161            }
2162            if let Some(v) = minimum {
2163                lines.push(format!("minimum: {}", v.display_str()));
2164            }
2165            if let Some(v) = maximum {
2166                lines.push(format!("maximum: {}", v.display_str()));
2167            }
2168        }
2169        TypeSpecification::DateRange {
2170            lower,
2171            upper,
2172            minimum,
2173            maximum,
2174            ..
2175        } => {
2176            if let Some(v) = lower {
2177                lines.push(format!("lower: {}", v));
2178            }
2179            if let Some(v) = upper {
2180                lines.push(format!("upper: {}", v));
2181            }
2182            if let Some((magnitude, unit_name)) = minimum {
2183                lines.push(format!(
2184                    "minimum: {} {}",
2185                    magnitude.display_str(),
2186                    unit_name
2187                ));
2188            }
2189            if let Some((magnitude, unit_name)) = maximum {
2190                lines.push(format!(
2191                    "maximum: {} {}",
2192                    magnitude.display_str(),
2193                    unit_name
2194                ));
2195            }
2196        }
2197        TypeSpecification::TimeRange {
2198            lower,
2199            upper,
2200            minimum,
2201            maximum,
2202            ..
2203        } => {
2204            if let Some(v) = lower {
2205                lines.push(format!("lower: {}", v));
2206            }
2207            if let Some(v) = upper {
2208                lines.push(format!("upper: {}", v));
2209            }
2210            if let Some((magnitude, unit_name)) = minimum {
2211                lines.push(format!(
2212                    "minimum: {} {}",
2213                    magnitude.display_str(),
2214                    unit_name
2215                ));
2216            }
2217            if let Some((magnitude, unit_name)) = maximum {
2218                lines.push(format!(
2219                    "maximum: {} {}",
2220                    magnitude.display_str(),
2221                    unit_name
2222                ));
2223            }
2224        }
2225        TypeSpecification::Boolean { .. }
2226        | TypeSpecification::Veto { .. }
2227        | TypeSpecification::Undetermined
2228        | TypeSpecification::Spec { .. } => {}
2229        TypeSpecification::List { element, key } => {
2230            lines.push(format!("element: {element}"));
2231            match key {
2232                crate::planning::semantics::ListKey::Numbered => {
2233                    lines.push("key: numbered".to_string());
2234                }
2235                crate::planning::semantics::ListKey::Fields(fields) => {
2236                    lines.push(format!("key: {}", fields.join(", ")));
2237                }
2238            }
2239        }
2240    }
2241    lines
2242}
2243
2244impl ExecutionPlan {
2245    /// Expression-scope unit index (local types plus direct `uses` imports).
2246    pub(crate) fn expression_unit_index(&self) -> &crate::planning::unit_index::UnitIndex {
2247        &self.resolved_types.unit_index
2248    }
2249
2250    /// Per [`Self::data`] position: local [`Self::rules`] positions that transitively
2251    /// need that slot / row field (through rule references), alphabetical rule-name order.
2252    ///
2253    /// Built in plan topological order: at a rule-ref cell, OR the target rule's
2254    /// already-computed bitsets instead of descending Kind (rule references are
2255    /// evaluation boundaries). Walks the normalized body so constant-dead unless
2256    /// arms removed by normalize stay out of the index. Row fields land on the
2257    /// instance-list slot's [`NeededByRules::fields`] column.
2258    fn build_needed_by_rules(&self) -> (Vec<NeededByRules>, IndexMap<DataPath, Vec<u32>>) {
2259        use crate::planning::normalize::{LeafKind, NormalFormKind, NormalFormListOp};
2260
2261        let data_len = self.data.len();
2262        let words = data_len.div_ceil(64);
2263        let mut bits_by_rule: Vec<Vec<u64>> = Vec::with_capacity(self.rules.len());
2264        let mut nested_hits_by_rule: Vec<HashSet<DataPath>> = Vec::with_capacity(self.rules.len());
2265
2266        let mut visited = HashSet::new();
2267        let mut stack = Vec::new();
2268
2269        // Pass 1: data-slot bitsets (topo / rule_ref OR). Row field reads ignored here.
2270        for (rule_pos, (_, rule)) in self.rules.iter().enumerate() {
2271            let mut bits = vec![0u64; words];
2272            let mut nested_hits: HashSet<DataPath> = HashSet::new();
2273            visited.clear();
2274            stack.push(rule.normal_form);
2275            while let Some(id) = stack.pop() {
2276                if !visited.insert(id) {
2277                    continue;
2278                }
2279                let nf = self.normal_form(id);
2280                let mut needs = NeedsMarker {
2281                    plan: self,
2282                    rule_pos,
2283                    bits: &mut bits,
2284                    nested_hits: &mut nested_hits,
2285                    bits_by_rule: &bits_by_rule,
2286                    nested_hits_by_rule: &nested_hits_by_rule,
2287                };
2288                if let Some(ref_path) = &nf.rule_ref {
2289                    needs.mark(ImportNeed::Rule(ref_path.clone()));
2290                    continue;
2291                }
2292                match &nf.kind {
2293                    NormalFormKind::Leaf(LeafKind::DataPath(path)) => {
2294                        for need in self.data_read_needs(path) {
2295                            needs.mark(need);
2296                        }
2297                    }
2298                    NormalFormKind::Leaf(LeafKind::ImportRule(path)) if *path == rule.path => {
2299                        for need in self.rule_needs(path) {
2300                            needs.mark(need);
2301                        }
2302                    }
2303                    NormalFormKind::Leaf(LeafKind::ImportRule(path)) => {
2304                        needs.mark(ImportNeed::Rule(path.clone()));
2305                    }
2306                    NormalFormKind::OrderedDispatch { .. } => {
2307                        let origin = nf.origin.unwrap_or_else(|| {
2308                            panic!(
2309                                "BUG: non-rule_ref OrderedDispatch must carry origin (rule '{}')",
2310                                rule.path
2311                            )
2312                        });
2313                        stack.push(origin);
2314                    }
2315                    _ => nf.kind.for_each_child(|child| stack.push(child)),
2316                }
2317            }
2318            bits_by_rule.push(bits);
2319            nested_hits_by_rule.push(nested_hits);
2320        }
2321
2322        // Pass 2: row-field bitsets. Self RowEnd::Rule expands via completed bits_by_rule
2323        // (row rule reads are not topo edges).
2324        let mut row_bits_by_rule: Vec<BTreeMap<SpecIdentity, Vec<u64>>> =
2325            Vec::with_capacity(self.rules.len());
2326        for (rule_pos, (_, rule)) in self.rules.iter().enumerate() {
2327            let mut row_bits: BTreeMap<SpecIdentity, Vec<u64>> = BTreeMap::new();
2328            visited.clear();
2329            stack.push(rule.normal_form);
2330            while let Some(id) = stack.pop() {
2331                if !visited.insert(id) {
2332                    continue;
2333                }
2334                let nf = self.normal_form(id);
2335                if let Some(ref_path) = &nf.rule_ref {
2336                    let ref_pos = self.rules.get_index_of(ref_path).unwrap_or_else(|| {
2337                        panic!(
2338                            "BUG: rule_ref target '{ref_path}' missing from plan.rules (rule '{}')",
2339                            rule.path
2340                        )
2341                    });
2342                    if ref_pos >= rule_pos {
2343                        panic!(
2344                            "BUG: rule_ref target '{ref_path}' must precede '{}' in topo order",
2345                            rule.path
2346                        );
2347                    }
2348                    for (identity, dep_row) in &row_bits_by_rule[ref_pos] {
2349                        let entry = row_bits_entry(self, &mut row_bits, identity);
2350                        for (word, dep_word) in entry.iter_mut().zip(dep_row.iter()) {
2351                            *word |= *dep_word;
2352                        }
2353                    }
2354                    continue;
2355                }
2356                match &nf.kind {
2357                    NormalFormKind::Leaf(LeafKind::Row { identity, end, .. }) => {
2358                        mark_row_end_needs(
2359                            self,
2360                            identity,
2361                            end,
2362                            &bits_by_rule,
2363                            &mut row_bits,
2364                            &rule.path,
2365                        );
2366                    }
2367                    NormalFormKind::SpecMember {
2368                        base,
2369                        row: Some(row_read),
2370                        ..
2371                    } => {
2372                        let identity = match &self.result_type(*base).specifications {
2373                            TypeSpecification::Spec { spec } => spec,
2374                            other => panic!(
2375                                "BUG: SpecMember row read on non-spec type {other:?} (rule '{}')",
2376                                rule.path
2377                            ),
2378                        };
2379                        mark_row_end_needs(
2380                            self,
2381                            identity,
2382                            &row_read.end,
2383                            &bits_by_rule,
2384                            &mut row_bits,
2385                            &rule.path,
2386                        );
2387                        stack.push(*base);
2388                    }
2389                    NormalFormKind::ListCommand {
2390                        list,
2391                        op: NormalFormListOp::Find | NormalFormListOp::WithRow { .. },
2392                        row_level: Some(_),
2393                        ..
2394                    } => {
2395                        mark_list_key_fields(self, *list, &mut row_bits, &rule.path);
2396                        nf.kind.for_each_child(|child| stack.push(child));
2397                    }
2398                    NormalFormKind::OrderedDispatch { .. } => {
2399                        let origin = nf.origin.unwrap_or_else(|| {
2400                            panic!(
2401                                "BUG: non-rule_ref OrderedDispatch must carry origin (rule '{}')",
2402                                rule.path
2403                            )
2404                        });
2405                        stack.push(origin);
2406                    }
2407                    _ => nf.kind.for_each_child(|child| stack.push(child)),
2408                }
2409            }
2410            row_bits_by_rule.push(row_bits);
2411        }
2412
2413        let mut consumers: Vec<usize> = self
2414            .rules
2415            .keys()
2416            .enumerate()
2417            .filter(|(_, path)| self.show_rules.contains_key(*path))
2418            .map(|(pos, _)| pos)
2419            .collect();
2420        consumers.sort_by(|&a, &b| {
2421            self.rules[a]
2422                .path
2423                .input_key()
2424                .cmp(&self.rules[b].path.input_key())
2425        });
2426
2427        let mut needed_by_rules: Vec<NeededByRules> = self
2428            .data
2429            .iter()
2430            .map(|(path, definition)| {
2431                let fields = match definition {
2432                    DataDefinition::InstanceList { .. } => {
2433                        let identity = &self
2434                            .instance_lists
2435                            .get(path)
2436                            .unwrap_or_else(|| {
2437                                panic!("BUG: InstanceList '{path}' missing from instance_lists")
2438                            })
2439                            .identity;
2440                        vec![Vec::new(); self.row_template(identity).nested_input_keys().len()]
2441                    }
2442                    _ => Vec::new(),
2443                };
2444                NeededByRules {
2445                    rules: Vec::new(),
2446                    fields,
2447                }
2448            })
2449            .collect();
2450
2451        for &rule_pos in &consumers {
2452            let rule_id = u32::try_from(rule_pos).expect("BUG: rule count exceeds u32");
2453            let bits = &bits_by_rule[rule_pos];
2454            for (word_idx, &word) in bits.iter().enumerate() {
2455                let mut remaining = word;
2456                while remaining != 0 {
2457                    let bit = remaining.trailing_zeros() as usize;
2458                    let data_pos = word_idx * 64 + bit;
2459                    needed_by_rules[data_pos].rules.push(rule_id);
2460                    remaining &= remaining - 1;
2461                }
2462            }
2463            for (identity, row_bits) in &row_bits_by_rule[rule_pos] {
2464                let list_positions = self.instance_list_data_positions(identity);
2465                if list_positions.is_empty() {
2466                    panic!(
2467                        "BUG: row field needs for '{identity}' but no instance list of that type"
2468                    );
2469                }
2470                let template_len = self.row_template(identity).nested_input_keys().len();
2471                for (word_idx, &word) in row_bits.iter().enumerate() {
2472                    let mut remaining = word;
2473                    while remaining != 0 {
2474                        let bit = remaining.trailing_zeros() as usize;
2475                        let field_pos = word_idx * 64 + bit;
2476                        if field_pos < template_len {
2477                            for &list_pos in &list_positions {
2478                                needed_by_rules[list_pos].fields[field_pos].push(rule_id);
2479                            }
2480                        }
2481                        remaining &= remaining - 1;
2482                    }
2483                }
2484            }
2485        }
2486        let mut nested_needed: IndexMap<DataPath, Vec<u32>> = IndexMap::new();
2487        for &rule_pos in &consumers {
2488            let rule_id = u32::try_from(rule_pos).expect("BUG: rule count exceeds u32");
2489            let mut paths: Vec<DataPath> = nested_hits_by_rule[rule_pos].iter().cloned().collect();
2490            paths.sort();
2491            for path in paths {
2492                nested_needed.entry(path).or_default().push(rule_id);
2493            }
2494        }
2495        (needed_by_rules, nested_needed)
2496    }
2497
2498    /// [`Self::data`] positions of instance lists whose element identity is `identity`.
2499    fn instance_list_data_positions(&self, identity: &SpecIdentity) -> Vec<usize> {
2500        self.instance_lists
2501            .iter()
2502            .filter(|(_, source)| source.identity == *identity)
2503            .map(|(path, _)| {
2504                self.data
2505                    .get_index_of(path)
2506                    .unwrap_or_else(|| panic!("BUG: instance list '{path}' absent from plan.data"))
2507            })
2508            .collect()
2509    }
2510
2511    /// Union of [`NeededByRules::fields`] across instance lists of `identity`
2512    /// (alphabetical rule-name order already on each list column).
2513    pub(crate) fn row_field_needed_by_rules(
2514        &self,
2515        identity: &SpecIdentity,
2516        field_input_key: &str,
2517    ) -> Vec<u32> {
2518        let template = self.row_template(identity);
2519        let path = template
2520            .input_key_index
2521            .get(field_input_key)
2522            .unwrap_or_else(|| {
2523                panic!(
2524                    "BUG: row field '{field_input_key}' absent from template data of '{identity}'"
2525                )
2526            });
2527        let field_pos = template_bindable_field_index(template, path).unwrap_or_else(|| {
2528            panic!(
2529                "BUG: row field '{field_input_key}' path missing from template bindable fields of '{identity}'"
2530            )
2531        });
2532        let mut seen = HashSet::new();
2533        let mut union = Vec::new();
2534        for list_pos in self.instance_list_data_positions(identity) {
2535            let fields = &self.needed_by_rules[list_pos].fields;
2536            let consumers = fields.get(field_pos).unwrap_or_else(|| {
2537                panic!(
2538                    "BUG: needed_by_rules.fields len {} < field {field_pos} for list data pos {list_pos}",
2539                    fields.len()
2540                )
2541            });
2542            for &rule_id in consumers {
2543                if seen.insert(rule_id) {
2544                    union.push(rule_id);
2545                }
2546            }
2547        }
2548        union.sort_by(|&a, &b| {
2549            self.rules
2550                .get_index(a as usize)
2551                .expect("BUG: needed_by_rules rule id out of range")
2552                .1
2553                .path
2554                .input_key()
2555                .cmp(
2556                    &self
2557                        .rules
2558                        .get_index(b as usize)
2559                        .expect("BUG: needed_by_rules rule id out of range")
2560                        .1
2561                        .path
2562                        .input_key(),
2563                )
2564        });
2565        union
2566    }
2567
2568    /// Promptable [`DataPath`] for a normal-form data leaf, following reference
2569    /// chain ends for references. `None` when the leaf is a rule-target, import
2570    /// alias, instance list, or a named-`uses` member covered by site overlay /
2571    /// template default (not caller-promptable).
2572    ///
2573    /// Reference paths must appear in [`Self::reference_ends`].
2574    pub(crate) fn promptable_data_path<'a>(&'a self, path: &'a DataPath) -> Option<&'a DataPath> {
2575        match self.data.get(path) {
2576            Some(DataDefinition::Value { .. } | DataDefinition::TypeDeclaration { .. }) => {
2577                Some(path)
2578            }
2579            Some(DataDefinition::Reference { .. }) => match self.reference_ends.get(path) {
2580                Some(ReferenceEnd::Promptable(target)) => Some(target),
2581                Some(ReferenceEnd::Rule(_) | ReferenceEnd::Import) => None,
2582                None => {
2583                    panic!("BUG: reference '{path}' missing from reference_ends after planning")
2584                }
2585            },
2586            Some(DataDefinition::Import { .. } | DataDefinition::InstanceList { .. }) => None,
2587            None => {
2588                if let Some(end) = self.reference_ends.get(path) {
2589                    return match end {
2590                        ReferenceEnd::Promptable(target) => Some(target),
2591                        ReferenceEnd::Rule(_) | ReferenceEnd::Import => None,
2592                    };
2593                }
2594                match lookup_data_definition(&self.data, &self.row_templates, path) {
2595                    Some(DataDefinition::TypeDeclaration { .. } | DataDefinition::Value { .. })
2596                        if !self.import_overlay_binds(path) =>
2597                    {
2598                        Some(path)
2599                    }
2600                    Some(
2601                        DataDefinition::Value { .. }
2602                        | DataDefinition::TypeDeclaration { .. }
2603                        | DataDefinition::Reference { .. }
2604                        | DataDefinition::Import { .. }
2605                        | DataDefinition::InstanceList { .. },
2606                    ) => None,
2607                    None => panic!("BUG: normal-form DataPath leaf absent from plan.data: {path}"),
2608                }
2609            }
2610        }
2611    }
2612
2613    /// Needs of evaluating `path` on this plan: local rules from
2614    /// [`Self::needed_by_rules`] / [`Self::nested_needed_by_rules`], named-`uses`
2615    /// rules through [`import_rule_needs`].
2616    pub(crate) fn rule_needs(&self, path: &RulePath) -> Vec<ImportNeed> {
2617        if !path.segments.is_empty() {
2618            return import_rule_needs(self, path);
2619        }
2620        let rule_pos = self.rules.get_index_of(path).unwrap_or_else(|| {
2621            panic!(
2622                "BUG: rule '{}' missing from plan '{}' while reading its needs",
2623                path.rule, self.spec_name
2624            )
2625        });
2626        let rule_id = u32::try_from(rule_pos).expect("BUG: rule count exceeds u32");
2627        let mut out: Vec<ImportNeed> = self
2628            .needed_by_rules
2629            .iter()
2630            .zip(self.data.keys())
2631            .filter(|(needed, _)| needed.rules.contains(&rule_id))
2632            .map(|(_, data_path)| ImportNeed::Data(data_path.clone()))
2633            .collect();
2634        out.extend(
2635            self.nested_needed_by_rules
2636                .iter()
2637                .filter(|(_, rules)| rules.contains(&rule_id))
2638                .map(|(nested, _)| ImportNeed::Data(nested.clone())),
2639        );
2640        out
2641    }
2642
2643    /// Rule positions whose evaluation needs input `path`: [`Self::needed_by_rules`] for
2644    /// paths in [`Self::data`] (locals, projected lists), else
2645    /// [`Self::nested_needed_by_rules`], as [`NeedsMarker::mark`] stores them.
2646    pub(crate) fn data_needed_by(&self, path: &DataPath) -> &[u32] {
2647        match self.data.get_index_of(path) {
2648            Some(position) => {
2649                &self
2650                    .needed_by_rules
2651                    .get(position)
2652                    .unwrap_or_else(|| {
2653                        panic!(
2654                            "BUG: needed_by_rules len {} < data position {position}",
2655                            self.needed_by_rules.len()
2656                        )
2657                    })
2658                    .rules
2659            }
2660            None => self
2661                .nested_needed_by_rules
2662                .get(path)
2663                .map_or(&[], Vec::as_slice),
2664        }
2665    }
2666
2667    /// Needs of reading `path` on this plan (local slot or named-`uses` member).
2668    pub(crate) fn data_read_needs(&self, path: &DataPath) -> Vec<ImportNeed> {
2669        needs_of_end(self, self.reference_end_of(path))
2670    }
2671
2672    /// [`ReferenceEnd`] of any data path on this plan: local slots by definition and
2673    /// [`Self::reference_ends`], named-`uses` members by [`reference_end_on`].
2674    pub(crate) fn reference_end_of(&self, path: &DataPath) -> ReferenceEnd {
2675        if !path.segments.is_empty() {
2676            return reference_end_on(self, path).unwrap_or_else(|| {
2677                panic!(
2678                    "BUG: member '{path}' does not peel on plan '{}'",
2679                    self.spec_name
2680                )
2681            });
2682        }
2683        match self
2684            .data
2685            .get(path)
2686            .unwrap_or_else(|| panic!("BUG: data '{path}' missing from plan '{}'", self.spec_name))
2687        {
2688            DataDefinition::Value { .. }
2689            | DataDefinition::TypeDeclaration { .. }
2690            | DataDefinition::InstanceList { .. } => ReferenceEnd::Promptable(path.clone()),
2691            DataDefinition::Import { .. } => ReferenceEnd::Import,
2692            DataDefinition::Reference { .. } => {
2693                self.reference_ends.get(path).cloned().unwrap_or_else(|| {
2694                    panic!("BUG: reference '{path}' missing from reference_ends")
2695                })
2696            }
2697        }
2698    }
2699
2700    /// Whether a site overlay on a leading Import hop binds `path`'s relative member.
2701    pub(crate) fn import_overlay_binds(&self, path: &DataPath) -> bool {
2702        let mut hops = path.segments.iter();
2703        let Some(first) = hops.next() else {
2704            return false;
2705        };
2706        let import_marker = DataPath::local(first.uses.clone());
2707        let Some(DataDefinition::Import { overlay, .. }) = self.data.get(&import_marker) else {
2708            return false;
2709        };
2710        let relative = DataPath {
2711            segments: hops.cloned().collect(),
2712            data: path.data.clone(),
2713        };
2714        overlay.contains_key(&relative)
2715    }
2716
2717    /// Validate caller-requested rule names and return resolved [`RulePath`]s.
2718    ///
2719    /// `None` means all local rules (this spec's outputs). `Some(&[])` is an error.
2720    /// Unknown names in `Some` slice error. Explicit names may be any Show.rules key.
2721    pub fn validated_response_rule_paths(
2722        &self,
2723        rules: Option<&[String]>,
2724    ) -> Result<std::collections::HashSet<&RulePath>, Error> {
2725        let Some(rules) = rules else {
2726            return Ok(self
2727                .rules
2728                .values()
2729                .filter(|r| r.path.segments.is_empty())
2730                .map(|r| &r.path)
2731                .collect());
2732        };
2733        if rules.is_empty() {
2734            return Err(Error::request(
2735                "at least one rule required".to_string(),
2736                None::<String>,
2737            ));
2738        }
2739        let mut paths = std::collections::HashSet::new();
2740        for rule_name in rules {
2741            let rule = self.get_rule(rule_name).ok_or_else(|| {
2742                Error::request(
2743                    format!("Rule '{rule_name}' not found in spec '{}'", self.spec_name),
2744                    None::<String>,
2745                )
2746            })?;
2747            paths.insert(&rule.path);
2748        }
2749        Ok(paths)
2750    }
2751
2752    /// Look up a Show.rules key (`RulePath::input_key`) in the reachable graph.
2753    pub fn get_rule(&self, name: &str) -> Option<&ExecutableRule> {
2754        let canonical = crate::parsing::ast::canonical_logical_name(name);
2755        let path = self.show_rule_index.get(canonical.as_ref())?;
2756        self.rules.get(path)
2757    }
2758
2759    /// Look up a normal-form cell by id.
2760    pub(crate) fn normal_form(&self, id: NormalFormId) -> &NormalForm {
2761        self.normal_forms.get(id.index()).unwrap_or_else(|| {
2762            panic!(
2763                "BUG: NormalFormId {} out of range (table len {})",
2764                id.index(),
2765                self.normal_forms.len()
2766            )
2767        })
2768    }
2769
2770    /// Stamped result type of the cell at `id`.
2771    pub(crate) fn result_type(&self, id: NormalFormId) -> &Arc<LemmaType> {
2772        &self.normal_form(id).result_type
2773    }
2774
2775    /// Position of `path` in [`Self::rules`]. Every rule path a plan cell
2776    /// references was planned, so absence is a bug.
2777    pub(crate) fn rule_index(&self, path: &RulePath) -> RuleIndex {
2778        RuleIndex(
2779            self.rules
2780                .get_index_of(path)
2781                .unwrap_or_else(|| panic!("BUG: rule '{}' missing from execution plan", path.rule)),
2782        )
2783    }
2784
2785    pub(crate) fn rule_at(&self, index: RuleIndex) -> &ExecutableRule {
2786        self.rules
2787            .get_index(index.0)
2788            .map(|(_, rule)| rule)
2789            .unwrap_or_else(|| {
2790                panic!(
2791                    "BUG: rule index {} out of plan.rules range ({} rules)",
2792                    index.0,
2793                    self.rules.len()
2794                )
2795            })
2796    }
2797
2798    /// Data paths a caller can be prompted for, in declaration order.
2799    ///
2800    /// A path is promptable when it carries its own value slot: [`DataDefinition::Value`]
2801    /// (spec prefill, overridable) or [`DataDefinition::TypeDeclaration`] (typed input).
2802    /// [`DataDefinition::Reference`] paths are not promptable — a data target copies from
2803    /// its ultimate target (see [`ExecutionPlan::reference_ends`]) and a rule target is
2804    /// computed — and [`DataDefinition::Import`] paths are owned by the imported spec.
2805    ///
2806    /// Domain of keys that may appear in `RuleResult.missing_data` (as a set).
2807    /// Evaluation decides which unbound members are live and in what order; this
2808    /// iterator does not fix list order.
2809    pub(crate) fn promptable_data_paths(&self) -> impl Iterator<Item = &DataPath> {
2810        self.data
2811            .iter()
2812            .filter_map(|(path, definition)| match definition {
2813                DataDefinition::Value { .. } | DataDefinition::TypeDeclaration { .. } => Some(path),
2814                DataDefinition::Reference { .. }
2815                | DataDefinition::Import { .. }
2816                | DataDefinition::InstanceList { .. } => None,
2817            })
2818    }
2819}
2820
2821/// Follow an [`NormalFormKind::OrderedDispatch`] region to the origin arm body it selects,
2822/// using filled slots: a nested dispatch by its scrutinee slot, a conjunctive region by
2823/// its residual arms. Stops at rule-reference cells: those borrow another rule's Kind and
2824/// must compare as the origin arm body, not the body that rule would itself select.
2825/// `None` when a slot the walk reads is unset or vetoed (not decided yet).
2826pub(crate) fn resolve_nested_dispatch_body(
2827    plan: &ExecutionPlan,
2828    values: &[Option<crate::computation::OperationResult>],
2829    mut region: DispatchRegion,
2830) -> Option<NormalFormId> {
2831    use crate::computation::OperationResult;
2832    use crate::planning::normalize::NormalFormKind;
2833    use crate::planning::ordered_dispatch::region_of_scrutinee;
2834
2835    for _ in 0..=plan.normal_forms.len() {
2836        let body = match region {
2837            DispatchRegion::Body(body) => body,
2838            DispatchRegion::Conjunctive(group) => {
2839                region = DispatchRegion::Body(conjunctive_winner_body(plan, values, group)?);
2840                continue;
2841            }
2842        };
2843        let cell = plan.normal_form(body);
2844        if cell.rule_ref.is_some() {
2845            return Some(body);
2846        }
2847        let (scrutinee, boundaries, regions) = match &cell.kind {
2848            NormalFormKind::OrderedDispatch {
2849                scrutinee,
2850                boundaries,
2851                regions,
2852            } => (scrutinee, boundaries, regions),
2853            NormalFormKind::Piecewise(_) => {
2854                panic!("BUG: single-scrutinee region body is a Piecewise, not a conjunctive group")
2855            }
2856            NormalFormKind::Leaf(_)
2857            | NormalFormKind::Sum(_)
2858            | NormalFormKind::Product(_)
2859            | NormalFormKind::Subtract(_, _)
2860            | NormalFormKind::Divide(_, _)
2861            | NormalFormKind::Power(_, _)
2862            | NormalFormKind::Modulo(_, _)
2863            | NormalFormKind::Negate(_)
2864            | NormalFormKind::Reciprocal(_)
2865            | NormalFormKind::Comparison(_, _, _)
2866            | NormalFormKind::And(_)
2867            | NormalFormKind::Not(_)
2868            | NormalFormKind::MathOp(_, _)
2869            | NormalFormKind::UnitConversion(_, _)
2870            | NormalFormKind::Veto(_)
2871            | NormalFormKind::DateRelative(_, _)
2872            | NormalFormKind::DateCalendar(_, _, _)
2873            | NormalFormKind::RangeLiteral(_, _)
2874            | NormalFormKind::PastFutureRange(_, _)
2875            | NormalFormKind::RangeContainment(_, _)
2876            | NormalFormKind::RangeBound(_, _)
2877            | NormalFormKind::ResultIsVeto(_)
2878            | NormalFormKind::Now
2879            | NormalFormKind::SpecMember { .. }
2880            | NormalFormKind::ListCommand { .. } => return Some(body),
2881        };
2882        let literal = match values.get(scrutinee.index()).and_then(|slot| slot.as_ref()) {
2883            Some(OperationResult::Value(literal)) => literal,
2884            Some(OperationResult::Veto(_)) | None => return None,
2885        };
2886        let index = region_of_scrutinee(boundaries, &literal.value).ok()?;
2887        region = regions[index];
2888    }
2889    panic!("BUG: nested OrderedDispatch chain longer than the normal-form table");
2890}
2891
2892/// Origin arm body a conjunctive region's residual arms select, scanned like the origin
2893/// Piecewise. `None` while a residual condition is unset or vetoed. A group that
2894/// normalization rewrote (into a dispatch or a collapsed value) is itself the body.
2895fn conjunctive_winner_body(
2896    plan: &ExecutionPlan,
2897    values: &[Option<crate::computation::OperationResult>],
2898    group: NormalFormId,
2899) -> Option<NormalFormId> {
2900    use crate::evaluation::branch_semantics::{
2901        condition_outcome, piecewise_decision, PiecewiseDecision,
2902    };
2903    use crate::planning::normalize::NormalFormKind;
2904
2905    let NormalFormKind::Piecewise(arms) = &plan.normal_form(group).kind else {
2906        return Some(group);
2907    };
2908    let outcome = |arm: usize| {
2909        values
2910            .get(arms[arm].0.index())
2911            .and_then(|slot| slot.as_ref())
2912            .map(condition_outcome)
2913    };
2914    match piecewise_decision(arms.len(), outcome) {
2915        PiecewiseDecision::Taken { arm } => Some(arms[arm].1),
2916        PiecewiseDecision::Default => Some(arms[0].1),
2917        PiecewiseDecision::Undecided { .. } => None,
2918    }
2919}
2920
2921/// DataPath leaves reachable from `root` in evaluator decision-tree preorder.
2922///
2923/// Control liveness is derived from `values` (the evaluation value table), matching
2924/// the evaluator's own decisions:
2925/// - [`NormalFormKind::And`]: a filled `false` operand decides the conjunction, so it
2926///   is the only live operand; a missing sibling is never reported. Otherwise every
2927///   operand not settled `true` is live, since a later `false` can still decide.
2928/// - [`NormalFormKind::Piecewise`]: arms decided from filled condition slots, high to
2929///   low. An unfilled or vetoed condition before a winner is found means no decision:
2930///   every arm stays live.
2931/// - [`NormalFormKind::OrderedDispatch`]: only the region for the scrutinee slot. An
2932///   unfilled scrutinee means every origin arm stays live.
2933///
2934/// Every cell the evaluator visits owns its slot, including rule reference cells
2935/// (they hold the referenced rule's value).
2936///
2937/// Order matches the evaluator walk (`tree.rs`): first-seen [`DataPath`] wins
2938/// (rule references may share a leaf under distinct cell ids). Iterative DFS with
2939/// pop-time visited; children pushed in reverse evaluation order. Instance-list
2940/// commands and row member reads appear as [`ReachableInput::RowReads`]: their
2941/// missing row fields are recorded during evaluation, not in the plan.
2942pub(crate) fn reachable_data_paths(
2943    plan: &ExecutionPlan,
2944    root: NormalFormId,
2945    values: &[Option<crate::computation::OperationResult>],
2946) -> Vec<ReachableInput> {
2947    use crate::computation::OperationResult;
2948    use crate::evaluation::branch_semantics::{
2949        condition_outcome, piecewise_decision, BranchOutcome, PiecewiseDecision,
2950    };
2951    use crate::planning::normalize::LeafKind;
2952    use crate::planning::normalize::NormalFormKind;
2953    use crate::planning::ordered_dispatch::region_of_scrutinee;
2954
2955    fn slot_outcome(values: &[Option<OperationResult>], id: NormalFormId) -> Option<BranchOutcome> {
2956        values
2957            .get(id.index())
2958            .and_then(|slot| slot.as_ref())
2959            .map(condition_outcome)
2960    }
2961
2962    /// Piecewise pre-image of a dispatch cell.
2963    ///
2964    /// A cell that borrows a rule's Kind (`rule_ref`) records no `origin` of its
2965    /// own, and the rule it names can itself be a bare reference to a further
2966    /// rule (`rule alias: country`), so the fold can sit any number of
2967    /// references away. Every hop moves to a rule earlier in topological order,
2968    /// so the chain is shorter than the rule table.
2969    fn dispatch_origin(plan: &ExecutionPlan, cell: &NormalForm) -> NormalFormId {
2970        let mut current = cell;
2971        for _ in 0..=plan.rules.len() {
2972            if let Some(origin) = current.origin {
2973                return origin;
2974            }
2975            let ref_path = current.rule_ref.as_ref().unwrap_or_else(|| {
2976                panic!("BUG: OrderedDispatch without origin must carry rule_ref")
2977            });
2978            let body = plan
2979                .rules
2980                .get(ref_path)
2981                .unwrap_or_else(|| {
2982                    panic!("BUG: rule reference '{ref_path}' missing from plan.rules")
2983                })
2984                .normal_form;
2985            current = plan.normal_form(body);
2986        }
2987        panic!("BUG: rule reference chain over a dispatch cell does not terminate");
2988    }
2989
2990    /// Piecewise children the evaluator visits under `decision`, in evaluation
2991    /// order: `cond_n, body_n, …, cond_1, body_1, default`.
2992    fn live_piecewise_children(
2993        arms: &[(NormalFormId, NormalFormId)],
2994        decision: PiecewiseDecision,
2995    ) -> Vec<NormalFormId> {
2996        let mut live = Vec::new();
2997        match decision {
2998            PiecewiseDecision::Taken { arm } => {
2999                for (condition, _) in arms.iter().skip(arm + 1).rev() {
3000                    live.push(*condition);
3001                }
3002                live.push(arms[arm].0);
3003                live.push(arms[arm].1);
3004            }
3005            PiecewiseDecision::Default => {
3006                for (condition, _) in arms.iter().skip(1).rev() {
3007                    live.push(*condition);
3008                }
3009                live.push(arms[0].1);
3010            }
3011            PiecewiseDecision::Undecided { arm } => {
3012                for (condition, _) in arms.iter().skip(arm + 1).rev() {
3013                    live.push(*condition);
3014                }
3015                for (condition, body) in arms.iter().take(arm + 1).skip(1).rev() {
3016                    live.push(*condition);
3017                    live.push(*body);
3018                }
3019                live.push(arms[0].1);
3020            }
3021        }
3022        live
3023    }
3024
3025    let mut out = Vec::new();
3026    let mut seen_paths = HashSet::new();
3027    let mut visited = HashSet::new();
3028    let mut stack = vec![root];
3029
3030    while let Some(id) = stack.pop() {
3031        if !visited.insert(id) {
3032            continue;
3033        }
3034        let nf = plan.normal_form(id);
3035        let live: Vec<NormalFormId> = match &nf.kind {
3036            NormalFormKind::Leaf(LeafKind::DataPath(path)) => {
3037                if seen_paths.insert(path.clone()) {
3038                    out.push(ReachableInput::Data(path.clone()));
3039                }
3040                Vec::new()
3041            }
3042            NormalFormKind::Leaf(LeafKind::ImportRule(path)) => {
3043                out.push(ReachableInput::ImportRule(path.clone()));
3044                Vec::new()
3045            }
3046            NormalFormKind::Leaf(LeafKind::Row { .. }) => Vec::new(),
3047            NormalFormKind::ListCommand { row_level, .. } => {
3048                if row_level.is_some() {
3049                    out.push(ReachableInput::RowReads(id));
3050                }
3051                nf.kind.children()
3052            }
3053            NormalFormKind::And(children) => {
3054                match children.iter().copied().find(|&child| {
3055                    matches!(slot_outcome(values, child), Some(BranchOutcome::NotTaken))
3056                }) {
3057                    Some(decider) => vec![decider],
3058                    None => children
3059                        .iter()
3060                        .copied()
3061                        .filter(|&child| {
3062                            !matches!(slot_outcome(values, child), Some(BranchOutcome::Taken))
3063                        })
3064                        .collect(),
3065                }
3066            }
3067            NormalFormKind::Piecewise(arms) => {
3068                let decision =
3069                    piecewise_decision(arms.len(), |arm| slot_outcome(values, arms[arm].0));
3070                live_piecewise_children(arms, decision)
3071            }
3072            NormalFormKind::OrderedDispatch {
3073                scrutinee,
3074                boundaries,
3075                regions,
3076            } => {
3077                let origin_id = dispatch_origin(plan, nf);
3078                let NormalFormKind::Piecewise(arms) = &plan.normal_form(origin_id).kind else {
3079                    panic!("BUG: OrderedDispatch origin must be Piecewise");
3080                };
3081                visited.insert(origin_id);
3082
3083                match values.get(scrutinee.index()).and_then(|slot| slot.as_ref()) {
3084                    Some(OperationResult::Value(literal)) => {
3085                        match region_of_scrutinee(boundaries, &literal.value) {
3086                            Ok(region) => vec![*scrutinee, regions[region].cell()],
3087                            Err(_) => vec![*scrutinee],
3088                        }
3089                    }
3090                    Some(OperationResult::Veto(_)) => vec![*scrutinee],
3091                    // Origin Piecewise conditions are not filled under the dispatch
3092                    // value walk, so an open scrutinee keeps every origin arm live.
3093                    None => {
3094                        let mut live = vec![*scrutinee];
3095                        live.extend(live_piecewise_children(
3096                            arms,
3097                            PiecewiseDecision::Undecided {
3098                                arm: arms.len() - 1,
3099                            },
3100                        ));
3101                        live
3102                    }
3103                }
3104            }
3105            NormalFormKind::SpecMember {
3106                base, targets, row, ..
3107            } => {
3108                if row.is_some() {
3109                    out.push(ReachableInput::RowReads(id));
3110                }
3111                let chosen = match values.get(base.index()).and_then(|slot| slot.as_ref()) {
3112                    Some(OperationResult::Value(bound)) => match &bound.value {
3113                        crate::planning::semantics::ValueKind::Spec(
3114                            crate::planning::semantics::SpecValue::Fixed(instance),
3115                        ) => targets
3116                            .iter()
3117                            .find(|(candidate, _)| candidate == instance)
3118                            .map(|(_, target)| *target),
3119                        _ => None,
3120                    },
3121                    Some(OperationResult::Veto(_)) | None => None,
3122                };
3123                match chosen {
3124                    Some(target) => vec![*base, target],
3125                    None => vec![*base],
3126                }
3127            }
3128            NormalFormKind::Leaf(LeafKind::Literal(_))
3129            | NormalFormKind::Now
3130            | NormalFormKind::Veto(_)
3131            | NormalFormKind::Sum(_)
3132            | NormalFormKind::Product(_)
3133            | NormalFormKind::Subtract(_, _)
3134            | NormalFormKind::Divide(_, _)
3135            | NormalFormKind::Power(_, _)
3136            | NormalFormKind::Modulo(_, _)
3137            | NormalFormKind::Comparison(_, _, _)
3138            | NormalFormKind::RangeLiteral(_, _)
3139            | NormalFormKind::RangeContainment(_, _)
3140            | NormalFormKind::Negate(_)
3141            | NormalFormKind::Reciprocal(_)
3142            | NormalFormKind::Not(_)
3143            | NormalFormKind::MathOp(_, _)
3144            | NormalFormKind::UnitConversion(_, _)
3145            | NormalFormKind::DateRelative(_, _)
3146            | NormalFormKind::DateCalendar(_, _, _)
3147            | NormalFormKind::PastFutureRange(_, _)
3148            | NormalFormKind::RangeBound(_, _)
3149            | NormalFormKind::ResultIsVeto(_) => nf.kind.children(),
3150        };
3151        stack.extend(live.into_iter().rev());
3152    }
3153    out
3154}
3155
3156/// One input [`reachable_data_paths`] found, in evaluation order.
3157pub(crate) enum ReachableInput {
3158    Data(DataPath),
3159    /// A cell that reads list rows; its missing row fields are in the request state.
3160    RowReads(NormalFormId),
3161    /// A named-`uses` rule: its inputs follow the template's own walk under the
3162    /// import context (see `EvaluationContext::missing_data_for_rule`).
3163    ImportRule(RulePath),
3164}
3165
3166pub(crate) fn validate_value_against_type(
3167    expected_type: &LemmaType,
3168    value: &LiteralValue,
3169    unit_index: &crate::planning::unit_index::UnitIndex,
3170) -> Result<(), String> {
3171    use std::borrow::Cow;
3172
3173    let mut pending = vec![(expected_type, Cow::Borrowed(value))];
3174    while let Some((expected, value)) = pending.pop() {
3175        match (&expected.specifications, &value.value) {
3176            (TypeSpecification::List { element, .. }, ValueKind::List(rows)) => {
3177                for row in rows.iter().rev() {
3178                    pending.push((element.as_ref(), Cow::Owned(row.to_literal())));
3179                }
3180            }
3181            (
3182                range_spec @ (TypeSpecification::NumberRange { .. }
3183                | TypeSpecification::DateRange { .. }
3184                | TypeSpecification::TimeRange { .. }
3185                | TypeSpecification::MeasureRange { .. }
3186                | TypeSpecification::RatioRange { .. }),
3187                ValueKind::Range(left, right),
3188            ) => validate_range_literal(
3189                expected,
3190                range_spec,
3191                left.as_ref(),
3192                right.as_ref(),
3193                unit_index,
3194            )?,
3195            _ => validate_scalar_against_type(expected, &value)?,
3196        }
3197    }
3198    Ok(())
3199}
3200
3201fn validate_scalar_against_type(
3202    expected_type: &LemmaType,
3203    value: &LiteralValue,
3204) -> Result<(), String> {
3205    use crate::computation::bigint::BigInt;
3206    use crate::computation::rational::{
3207        checked_mul, rational_new, try_pow_integer, RationalInteger,
3208    };
3209
3210    fn exceeds_decimal_places(magnitude: &RationalInteger, max_decimals: u8) -> bool {
3211        let scale = match try_pow_integer(
3212            &rational_new(10, 1),
3213            &BigInt::from_i64(i64::from(max_decimals)),
3214        ) {
3215            Ok(value) => value,
3216            Err(_) => return true,
3217        };
3218        let scaled = match checked_mul(magnitude, &scale) {
3219            Ok(value) => value,
3220            Err(_) => return true,
3221        };
3222        match RationalInteger::try_reduce_ref(&scaled) {
3223            Ok(reduced) => !reduced.is_integer(),
3224            Err(_) => true,
3225        }
3226    }
3227
3228    fn format_rational_for_validation_message(
3229        expected_type: &crate::planning::semantics::LemmaType,
3230        magnitude: &RationalInteger,
3231    ) -> String {
3232        expected_type
3233            .try_rational_as_decimal(magnitude)
3234            .map(|decimal| decimal.to_string())
3235            .unwrap_or_else(|_| magnitude.display_str())
3236    }
3237
3238    match (&expected_type.specifications, &value.value) {
3239        (
3240            TypeSpecification::Number {
3241                minimum,
3242                maximum,
3243                decimals,
3244                ..
3245            },
3246            ValueKind::Number(n),
3247        ) => {
3248            use std::cmp::Ordering;
3249            if let Some(d) = decimals {
3250                if exceeds_decimal_places(n, *d) {
3251                    return Err(format!(
3252                        "{} exceeds decimals constraint {d}",
3253                        n.display_str()
3254                    ));
3255                }
3256            }
3257            if let Some(min) = minimum {
3258                if n.try_cmp(min)
3259                    .map_err(|failure| format!("minimum bound compare failed: {failure}"))?
3260                    == Ordering::Less
3261                {
3262                    return Err(format!(
3263                        "{} is below minimum {}",
3264                        format_rational_for_validation_message(expected_type, n),
3265                        format_rational_for_validation_message(expected_type, min)
3266                    ));
3267                }
3268            }
3269            if let Some(max) = maximum {
3270                if n.try_cmp(max)
3271                    .map_err(|failure| format!("maximum bound compare failed: {failure}"))?
3272                    == Ordering::Greater
3273                {
3274                    return Err(format!(
3275                        "{} is above maximum {}",
3276                        format_rational_for_validation_message(expected_type, n),
3277                        format_rational_for_validation_message(expected_type, max)
3278                    ));
3279                }
3280            }
3281            Ok(())
3282        }
3283        (
3284            TypeSpecification::Measure {
3285                minimum,
3286                maximum,
3287                decimals,
3288                units,
3289                ..
3290            },
3291            ValueKind::Measure(magnitude),
3292        ) => {
3293            use crate::computation::rational::checked_div;
3294            use crate::planning::semantics::measure_declared_bound_to_canonical;
3295            use std::cmp::Ordering;
3296            let unit = expected_type
3297                .unit()
3298                .or_else(|| {
3299                    units
3300                        .iter()
3301                        .find(|u| u.is_canonical_factor())
3302                        .or_else(|| units.iter().next())
3303                        .map(|u| u.name.as_str())
3304                })
3305                .ok_or_else(|| {
3306                    format!(
3307                        "measure type '{}' has no declared units for validation",
3308                        expected_type.name()
3309                    )
3310                })?;
3311            let measure_unit = units.get(unit)?;
3312            let factor = &measure_unit.factor;
3313            let in_unit = checked_div(magnitude, factor).map_err(|failure| {
3314                format!("cannot de-canonicalize measure for validation: {failure}")
3315            })?;
3316            if let Some(d) = decimals {
3317                if exceeds_decimal_places(&in_unit, *d) {
3318                    return Err(format!(
3319                        "{} {unit} exceeds decimals constraint {d}",
3320                        in_unit.display_str()
3321                    ));
3322                }
3323            }
3324            if let Some(bound) = minimum {
3325                let canonical_min = measure_declared_bound_to_canonical(
3326                    &bound.0,
3327                    &bound.1,
3328                    units,
3329                    expected_type.name().as_str(),
3330                    "minimum",
3331                )?;
3332                if magnitude
3333                    .try_cmp(&canonical_min)
3334                    .map_err(|failure| format!("minimum bound compare failed: {failure}"))?
3335                    == Ordering::Less
3336                {
3337                    let min_in_unit = checked_div(&canonical_min, factor).map_err(|failure| {
3338                        format!("cannot de-canonicalize minimum for validation: {failure}")
3339                    })?;
3340                    let value_display = format!(
3341                        "{} {}",
3342                        format_rational_for_validation_message(expected_type, &in_unit),
3343                        unit
3344                    );
3345                    let bound_display = format!(
3346                        "{} {}",
3347                        format_rational_for_validation_message(expected_type, &min_in_unit),
3348                        measure_unit.name
3349                    );
3350                    return Err(format!("{value_display} is below minimum {bound_display}"));
3351                }
3352            }
3353            if let Some(bound) = maximum {
3354                let canonical_max = measure_declared_bound_to_canonical(
3355                    &bound.0,
3356                    &bound.1,
3357                    units,
3358                    expected_type.name().as_str(),
3359                    "maximum",
3360                )?;
3361                if magnitude
3362                    .try_cmp(&canonical_max)
3363                    .map_err(|failure| format!("maximum bound compare failed: {failure}"))?
3364                    == Ordering::Greater
3365                {
3366                    let max_in_unit = checked_div(&canonical_max, factor).map_err(|failure| {
3367                        format!("cannot de-canonicalize maximum for validation: {failure}")
3368                    })?;
3369                    let value_display = format!(
3370                        "{} {}",
3371                        format_rational_for_validation_message(expected_type, &in_unit),
3372                        unit
3373                    );
3374                    let bound_display = format!(
3375                        "{} {}",
3376                        format_rational_for_validation_message(expected_type, &max_in_unit),
3377                        measure_unit.name
3378                    );
3379                    return Err(format!("{value_display} is above maximum {bound_display}"));
3380                }
3381            }
3382            Ok(())
3383        }
3384        (
3385            TypeSpecification::Text {
3386                length, options, ..
3387            },
3388            ValueKind::Text(s),
3389        ) => {
3390            let len = s.chars().count();
3391            if let Some(exact) = length {
3392                if len != *exact {
3393                    return Err(format!(
3394                        "'{}' has length {} but required length is {}",
3395                        s, len, exact
3396                    ));
3397                }
3398            }
3399            if !options.is_empty() && !options.iter().any(|opt| opt == s) {
3400                return Err(format!(
3401                    "'{}' is not in allowed options: {}",
3402                    s,
3403                    options.join(", ")
3404                ));
3405            }
3406            Ok(())
3407        }
3408        (
3409            TypeSpecification::Ratio {
3410                minimum,
3411                maximum,
3412                decimals,
3413                units,
3414                ..
3415            },
3416            ValueKind::Ratio(r),
3417        ) => {
3418            use crate::computation::rational::checked_mul;
3419            use std::cmp::Ordering;
3420
3421            let primary_unit = expected_type
3422                .unit()
3423                .or_else(|| expected_type.ratio_primary_unit());
3424
3425            if let Some(d) = decimals {
3426                let magnitude_for_decimals = match primary_unit {
3427                    Some(unit) => {
3428                        let ratio_unit = units.get(unit)?;
3429                        checked_mul(r, &ratio_unit.value).map_err(|failure| failure.to_string())?
3430                    }
3431                    None => r.clone(),
3432                };
3433                if exceeds_decimal_places(&magnitude_for_decimals, *d) {
3434                    return Err(format!(
3435                        "{} exceeds decimals constraint {d}",
3436                        magnitude_for_decimals.display_str()
3437                    ));
3438                }
3439            }
3440            if let Some(type_minimum) = minimum {
3441                if r.try_cmp(type_minimum)
3442                    .map_err(|failure| format!("minimum bound compare failed: {failure}"))?
3443                    == Ordering::Less
3444                {
3445                    let message = match primary_unit {
3446                        Some(unit) => {
3447                            let ratio_unit = units.get(unit)?;
3448                            let value_per_unit = checked_mul(r, &ratio_unit.value)
3449                                .map_err(|failure| failure.to_string())?;
3450                            let bound_per_unit = ratio_unit.minimum.clone().expect(
3451                                "BUG: RatioUnit.minimum missing after type minimum set by sync_ratio_units_from_canonical",
3452                            );
3453                            format!(
3454                                "{} {unit} is below minimum {} {unit}",
3455                                format_rational_for_validation_message(
3456                                    expected_type,
3457                                    &value_per_unit
3458                                ),
3459                                format_rational_for_validation_message(
3460                                    expected_type,
3461                                    &bound_per_unit.clone()
3462                                ),
3463                            )
3464                        }
3465                        None => format!(
3466                            "{} is below minimum {}",
3467                            format_rational_for_validation_message(expected_type, r),
3468                            format_rational_for_validation_message(expected_type, type_minimum),
3469                        ),
3470                    };
3471                    return Err(message);
3472                }
3473            }
3474            if let Some(type_maximum) = maximum {
3475                if r.try_cmp(type_maximum)
3476                    .map_err(|failure| format!("maximum bound compare failed: {failure}"))?
3477                    == Ordering::Greater
3478                {
3479                    let message = match primary_unit {
3480                        Some(unit) => {
3481                            let ratio_unit = units.get(unit)?;
3482                            let value_per_unit = checked_mul(r, &ratio_unit.value)
3483                                .map_err(|failure| failure.to_string())?;
3484                            let bound_per_unit = ratio_unit.maximum.clone().expect(
3485                                "BUG: RatioUnit.maximum missing after type maximum set by sync_ratio_units_from_canonical",
3486                            );
3487                            format!(
3488                                "{} {unit} is above maximum {} {unit}",
3489                                format_rational_for_validation_message(
3490                                    expected_type,
3491                                    &value_per_unit
3492                                ),
3493                                format_rational_for_validation_message(
3494                                    expected_type,
3495                                    &bound_per_unit.clone()
3496                                ),
3497                            )
3498                        }
3499                        None => format!(
3500                            "{} is above maximum {}",
3501                            format_rational_for_validation_message(expected_type, r),
3502                            format_rational_for_validation_message(expected_type, type_maximum),
3503                        ),
3504                    };
3505                    return Err(message);
3506                }
3507            }
3508            Ok(())
3509        }
3510        (
3511            TypeSpecification::Ratio {
3512                minimum,
3513                maximum,
3514                decimals,
3515                units: _,
3516                ..
3517            },
3518            ValueKind::Number(n),
3519        ) => {
3520            use std::cmp::Ordering;
3521            if let Some(d) = decimals {
3522                if exceeds_decimal_places(n, *d) {
3523                    return Err(format!(
3524                        "{} exceeds decimals constraint {d}",
3525                        n.display_str()
3526                    ));
3527                }
3528            }
3529            if let Some(type_minimum) = minimum {
3530                if n.try_cmp(type_minimum)
3531                    .map_err(|failure| format!("minimum bound compare failed: {failure}"))?
3532                    == Ordering::Less
3533                {
3534                    return Err(format!(
3535                        "{} is below minimum {}",
3536                        format_rational_for_validation_message(expected_type, n),
3537                        format_rational_for_validation_message(expected_type, type_minimum)
3538                    ));
3539                }
3540            }
3541            if let Some(type_maximum) = maximum {
3542                if n.try_cmp(type_maximum)
3543                    .map_err(|failure| format!("maximum bound compare failed: {failure}"))?
3544                    == Ordering::Greater
3545                {
3546                    return Err(format!(
3547                        "{} is above maximum {}",
3548                        format_rational_for_validation_message(expected_type, n),
3549                        format_rational_for_validation_message(expected_type, type_maximum)
3550                    ));
3551                }
3552            }
3553            Ok(())
3554        }
3555        (
3556            TypeSpecification::Date {
3557                minimum, maximum, ..
3558            },
3559            ValueKind::Date(dt),
3560        ) => {
3561            use crate::planning::semantics::{compare_semantic_dates, date_time_to_semantic};
3562            use std::cmp::Ordering;
3563            if let Some(min) = minimum {
3564                let min_sem = date_time_to_semantic(min);
3565                if compare_semantic_dates(dt, &min_sem) == Ordering::Less {
3566                    return Err(format!("{} is below minimum {}", dt, min));
3567                }
3568            }
3569            if let Some(max) = maximum {
3570                let max_sem = date_time_to_semantic(max);
3571                if compare_semantic_dates(dt, &max_sem) == Ordering::Greater {
3572                    return Err(format!("{} is above maximum {}", dt, max));
3573                }
3574            }
3575            Ok(())
3576        }
3577        (
3578            TypeSpecification::Time {
3579                minimum, maximum, ..
3580            },
3581            ValueKind::Time(t),
3582        ) => {
3583            use crate::planning::semantics::{compare_semantic_times, time_to_semantic};
3584            use std::cmp::Ordering;
3585            if let Some(min) = minimum {
3586                let min_sem = time_to_semantic(min);
3587                if compare_semantic_times(t, &min_sem) == Ordering::Less {
3588                    return Err(format!("{} is below minimum {}", t, min));
3589                }
3590            }
3591            if let Some(max) = maximum {
3592                let max_sem = time_to_semantic(max);
3593                if compare_semantic_times(t, &max_sem) == Ordering::Greater {
3594                    return Err(format!("{} is above maximum {}", t, max));
3595                }
3596            }
3597            Ok(())
3598        }
3599        (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_)) => Ok(()),
3600        (TypeSpecification::List { .. }, ValueKind::List(_))
3601        | (
3602            TypeSpecification::NumberRange { .. }
3603            | TypeSpecification::DateRange { .. }
3604            | TypeSpecification::TimeRange { .. }
3605            | TypeSpecification::MeasureRange { .. }
3606            | TypeSpecification::RatioRange { .. },
3607            ValueKind::Range(_, _),
3608        ) => unreachable!("BUG: list and range values are walked by validate_value_against_type"),
3609        (TypeSpecification::Veto { .. }, _) | (TypeSpecification::Undetermined, _) => Ok(()),
3610        (TypeSpecification::Spec { .. }, ValueKind::Spec(_)) => Ok(()),
3611        (spec, value_kind) if !value_kind_matches_spec(value_kind, spec) => unreachable!(
3612            "BUG: validate_value_against_type called with mismatched type/value: \
3613             spec={:?}, value={:?} — typing must be enforced before validation",
3614            spec, value_kind
3615        ),
3616        (spec, value_kind) => unreachable!(
3617            "BUG: validate_value_against_type missed a value_kind_matches_spec pair: \
3618             spec={:?}, value={:?}",
3619            spec, value_kind
3620        ),
3621    }
3622}
3623
3624fn validate_range_literal(
3625    expected_type: &LemmaType,
3626    range_spec: &TypeSpecification,
3627    left: &crate::planning::semantics::TypedLiteral,
3628    right: &crate::planning::semantics::TypedLiteral,
3629    unit_index: &crate::planning::unit_index::UnitIndex,
3630) -> Result<(), String> {
3631    use crate::computation::{comparison_operation, OperationResult};
3632    use crate::planning::semantics::{
3633        compare_semantic_dates, compare_semantic_times, measure_declared_bound_to_canonical,
3634        ValueKind,
3635    };
3636    use std::cmp::Ordering;
3637    use std::sync::Arc;
3638
3639    let element_spec = range_spec
3640        .element_from_range()
3641        .expect("BUG: element_from_range missing arm for validated range");
3642    if let TypeSpecification::Measure {
3643        units,
3644        decomposition,
3645        ..
3646    } = &element_spec
3647    {
3648        assert!(
3649            decomposition.is_some() || units.is_empty(),
3650            "BUG: measure range '{}' with units must carry a decomposition after planning",
3651            expected_type.name()
3652        );
3653    }
3654    let element_type = Arc::new(LemmaType::primitive(element_spec));
3655    validate_scalar_against_type(element_type.as_ref(), &left.to_literal())?;
3656    validate_scalar_against_type(element_type.as_ref(), &right.to_literal())?;
3657
3658    let ordering = match (&left.value, &right.value) {
3659        (ValueKind::Number(l), ValueKind::Number(r)) => l
3660            .try_cmp(r)
3661            .map_err(|failure| format!("range endpoint compare failed: {failure}"))?,
3662        (ValueKind::Date(l), ValueKind::Date(r)) => compare_semantic_dates(l, r),
3663        (ValueKind::Time(l), ValueKind::Time(r)) => compare_semantic_times(l, r),
3664        (ValueKind::Ratio(l), ValueKind::Ratio(r)) => l
3665            .try_cmp(r)
3666            .map_err(|failure| format!("range endpoint compare failed: {failure}"))?,
3667        (ValueKind::Measure(l), ValueKind::Measure(r)) => l
3668            .try_cmp(r)
3669            .map_err(|failure| format!("range endpoint compare failed: {failure}"))?,
3670        (left_kind, right_kind) => unreachable!(
3671            "BUG: range endpoints have mismatched value kinds after typing: {left_kind:?} vs {right_kind:?}"
3672        ),
3673    };
3674    if ordering == Ordering::Greater {
3675        return Err(format!(
3676            "range left endpoint {} is above right endpoint {}",
3677            left.display_value(),
3678            right.display_value()
3679        ));
3680    }
3681
3682    let range_lit = LiteralValue {
3683        value: ValueKind::Range(Box::new(left.clone()), Box::new(right.clone())),
3684    };
3685    let range_type = Arc::new(expected_type.clone());
3686
3687    let compare_width = |bound: &LiteralValue,
3688                         bound_type: &Arc<LemmaType>,
3689                         op: ComparisonComputation,
3690                         fail_msg: String|
3691     -> Result<(), String> {
3692        match comparison_operation(&range_lit.value, &range_type, &op, &bound.value, bound_type) {
3693            OperationResult::Value(result) => match &result.value {
3694                ValueKind::Boolean(true) => Ok(()),
3695                ValueKind::Boolean(false) => Err(fail_msg),
3696                other => unreachable!("BUG: width comparison must return boolean, got {other:?}"),
3697            },
3698            OperationResult::Veto(veto) => Err(veto.to_string()),
3699        }
3700    };
3701
3702    match range_spec {
3703        TypeSpecification::NumberRange {
3704            minimum, maximum, ..
3705        } => {
3706            if let Some(min_w) = minimum {
3707                compare_width(
3708                    &LiteralValue::number(min_w.clone()),
3709                    crate::planning::semantics::primitive_number_arc(),
3710                    ComparisonComputation::GreaterThanOrEqual,
3711                    format!("span is below minimum width {}", min_w.display_str()),
3712                )?;
3713            }
3714            if let Some(max_w) = maximum {
3715                compare_width(
3716                    &LiteralValue::number(max_w.clone()),
3717                    crate::planning::semantics::primitive_number_arc(),
3718                    ComparisonComputation::LessThanOrEqual,
3719                    format!("span is above maximum width {}", max_w.display_str()),
3720                )?;
3721            }
3722        }
3723        TypeSpecification::RatioRange {
3724            minimum, maximum, ..
3725        } => {
3726            if let Some(min_w) = minimum {
3727                compare_width(
3728                    &LiteralValue::ratio(min_w.clone()),
3729                    crate::planning::semantics::primitive_ratio_arc(),
3730                    ComparisonComputation::GreaterThanOrEqual,
3731                    format!("span is below minimum width {}", min_w.display_str()),
3732                )?;
3733            }
3734            if let Some(max_w) = maximum {
3735                compare_width(
3736                    &LiteralValue::ratio(max_w.clone()),
3737                    crate::planning::semantics::primitive_ratio_arc(),
3738                    ComparisonComputation::LessThanOrEqual,
3739                    format!("span is above maximum width {}", max_w.display_str()),
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                        "span is below minimum width {} {}",
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                        "span is above maximum width {} {}",
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 =
3790                |bound: &(crate::computation::rational::RationalInteger, String),
3791                 command: &str|
3792                 -> Result<(LiteralValue, Arc<LemmaType>), String> {
3793                    let (bare, owner) = unit_index
3794                        .resolve(bound.1.as_str())
3795                        .map_err(|err| format!("{command} width unit '{}': {err}", bound.1))?;
3796                    if allow_calendar {
3797                        if !owner.is_duration_like() && !owner.is_calendar_like() {
3798                            return Err(format!(
3799                                "{command} width unit '{bare}' must be a duration or calendar unit",
3800                            ));
3801                        }
3802                    } else if !owner.is_duration_like() {
3803                        return Err(format!(
3804                            "{command} width unit '{bare}' must be a duration unit",
3805                        ));
3806                    }
3807                    let TypeSpecification::Measure { units, .. } = &owner.specifications else {
3808                        return Err(format!(
3809                            "{command} width unit '{bare}' must resolve to a measure type",
3810                        ));
3811                    };
3812                    let type_name = owner.name();
3813                    let canonical = measure_declared_bound_to_canonical(
3814                        &bound.0,
3815                        &bare,
3816                        units,
3817                        type_name.as_str(),
3818                        command,
3819                    )?;
3820                    Ok((LiteralValue::measure(canonical), Arc::clone(&owner)))
3821                };
3822            if let Some(min_w) = minimum {
3823                let (bound, bound_type) = resolve_bound(min_w, "minimum")?;
3824                compare_width(
3825                    &bound,
3826                    &bound_type,
3827                    ComparisonComputation::GreaterThanOrEqual,
3828                    format!(
3829                        "span is below minimum width {} {}",
3830                        min_w.0.display_str(),
3831                        min_w.1
3832                    ),
3833                )?;
3834            }
3835            if let Some(max_w) = maximum {
3836                let (bound, bound_type) = resolve_bound(max_w, "maximum")?;
3837                compare_width(
3838                    &bound,
3839                    &bound_type,
3840                    ComparisonComputation::LessThanOrEqual,
3841                    format!(
3842                        "span is above maximum width {} {}",
3843                        max_w.0.display_str(),
3844                        max_w.1
3845                    ),
3846                )?;
3847            }
3848        }
3849        _ => {}
3850    }
3851
3852    Ok(())
3853}
3854
3855fn validate_literal_data_against_types(plan: &ExecutionPlan) -> Vec<Error> {
3856    let mut errors = Vec::new();
3857
3858    for (data_path, data_definition) in &plan.data {
3859        let (expected_type, lit) = match data_definition {
3860            DataDefinition::Value {
3861                value,
3862                resolved_type,
3863                ..
3864            } => (resolved_type, value),
3865            DataDefinition::TypeDeclaration { .. }
3866            | DataDefinition::Import { .. }
3867            | DataDefinition::InstanceList { .. }
3868            | DataDefinition::Reference { .. } => continue,
3869        };
3870
3871        if let Err(msg) =
3872            validate_value_against_type(expected_type, lit, plan.expression_unit_index())
3873        {
3874            let source = data_definition.source().clone();
3875            errors.push(Error::validation(
3876                format!(
3877                    "Invalid value for data {} (expected {}): {}",
3878                    data_path,
3879                    expected_type.name().as_str(),
3880                    msg
3881                ),
3882                Some(source),
3883                None::<String>,
3884            ));
3885        }
3886    }
3887
3888    errors
3889}
3890
3891fn validate_unit_conversion_targets(plan: &ExecutionPlan) -> Result<(), Error> {
3892    use crate::planning::normalize::NormalFormKind;
3893
3894    let mut errors: Vec<Error> = Vec::new();
3895    let mut visited = HashSet::new();
3896    let mut worklist: Vec<NormalFormId> =
3897        plan.rules.values().map(|rule| rule.normal_form).collect();
3898    while let Some(id) = worklist.pop() {
3899        if !visited.insert(id) {
3900            continue;
3901        }
3902        let nf = plan.normal_form(id);
3903        nf.kind.for_each_child(|child| worklist.push(child));
3904        if let NormalFormKind::UnitConversion(_, target) = &nf.kind {
3905            if let Some((unit_name, owning_type)) =
3906                crate::computation::units::conversion_target_declares_unit(target)
3907            {
3908                if !crate::computation::units::owning_type_declares_unit_name(
3909                    owning_type.as_ref(),
3910                    unit_name,
3911                ) {
3912                    errors.push(Error::validation(
3913                        format!(
3914                            "Unit conversion target '{unit_name}' is not declared on owning type '{}'",
3915                            owning_type.name()
3916                        ),
3917                        None::<Source>,
3918                        Some(plan.spec_name.clone()),
3919                    ));
3920                }
3921            }
3922        }
3923    }
3924    if let Some(error) = errors.into_iter().next() {
3925        return Err(error);
3926    }
3927    Ok(())
3928}
3929
3930#[cfg(test)]
3931mod tests {
3932    use super::*;
3933    use crate::computation::rational::{rational_new, rational_zero};
3934    use crate::computation::{OperationResult, VetoType};
3935    use crate::evaluation::evaluate_data::EvaluateData;
3936    use crate::literals::DateGranularity;
3937    use crate::literals::TimezoneValue;
3938    use crate::parsing::ast::DateTimeValue;
3939    use crate::planning::semantics::{DataDefinition, DataPath, PathSegment, TypeSpecification};
3940    use crate::Engine;
3941    use crate::{EvaluateDataValue, ResourceLimits};
3942    use serde_json;
3943    use std::str::FromStr;
3944    use std::sync::Arc;
3945
3946    fn default_limits() -> ResourceLimits {
3947        ResourceLimits::default()
3948    }
3949
3950    fn resolve_evaluate_data(
3951        plan: &ExecutionPlan,
3952        values: IndexMap<String, EvaluateDataValue>,
3953    ) -> EvaluateData {
3954        EvaluateData::resolve(plan, values, &default_limits()).expect("resolve")
3955    }
3956
3957    fn veto_reason<'a>(evaluate_data: &'a EvaluateData, path: &DataPath) -> Option<&'a str> {
3958        match evaluate_data.bindings.get(path) {
3959            Some(OperationResult::Veto(veto)) => Some(match veto {
3960                VetoType::Computation { message } => message.as_str(),
3961                other => panic!("expected Computation veto, got {other:?}"),
3962            }),
3963            _ => None,
3964        }
3965    }
3966
3967    fn bound_value<'a>(
3968        evaluate_data: &'a EvaluateData,
3969        path: &DataPath,
3970    ) -> Option<&'a crate::planning::semantics::BoundValueKind> {
3971        evaluate_data
3972            .bindings
3973            .get(path)
3974            .and_then(OperationResult::value)
3975    }
3976
3977    fn input_data(pairs: &[(&str, &str)]) -> IndexMap<String, EvaluateDataValue> {
3978        pairs
3979            .iter()
3980            .map(|(k, v)| (k.to_string(), EvaluateDataValue::string(*v)))
3981            .collect()
3982    }
3983
3984    #[test]
3985    fn test_with_raw_values() {
3986        let mut engine = Engine::new();
3987        engine
3988            .load([(
3989                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
3990                    "test.lemma",
3991                ))),
3992                r#"
3993                spec test
3994                data age: number -> suggest 25
3995                "#
3996                .to_string(),
3997            )])
3998            .unwrap();
3999
4000        let plans = engine
4001            .plans
4002            .get_plans(None, "test")
4003            .expect("plans for test");
4004        let plan = plans.values().next().expect("plan");
4005        let data_path = DataPath::new(vec![], "age".to_string());
4006
4007        let values = input_data(&[("age", "30")]);
4008
4009        let evaluate_data = resolve_evaluate_data(plan, values);
4010        let updated_value = bound_value(&evaluate_data, &data_path).expect("bound value");
4011        match &updated_value.value {
4012            crate::planning::semantics::ValueKind::Number(n) => {
4013                assert_eq!(n, &rational_new(30, 1));
4014            }
4015            other => panic!("Expected number literal, got {:?}", other),
4016        }
4017    }
4018
4019    #[test]
4020    fn test_with_raw_values_type_mismatch() {
4021        let mut engine = Engine::new();
4022        engine
4023            .load([(
4024                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
4025                    "test.lemma",
4026                ))),
4027                r#"
4028                spec test
4029                data age: number
4030                "#
4031                .to_string(),
4032            )])
4033            .unwrap();
4034
4035        let plans = engine
4036            .plans
4037            .get_plans(None, "test")
4038            .expect("plans for test");
4039        let plan = plans.values().next().expect("plan");
4040
4041        let values = input_data(&[("age", "thirty")]);
4042
4043        let evaluate_data = resolve_evaluate_data(plan, values);
4044        let data_path = DataPath::new(vec![], "age".to_string());
4045        match veto_reason(&evaluate_data, &data_path) {
4046            Some(reason) => {
4047                assert!(
4048                    reason.contains("number"),
4049                    "type mismatch must record violation reason, got: {reason}"
4050                );
4051            }
4052            None => panic!("expected veto-bound data for age=thirty"),
4053        }
4054    }
4055
4056    #[test]
4057    fn test_with_raw_values_unknown_data_ignored() {
4058        let mut engine = Engine::new();
4059        engine
4060            .load([(
4061                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
4062                    "test.lemma",
4063                ))),
4064                r#"
4065                spec test
4066                data known: number
4067                "#
4068                .to_string(),
4069            )])
4070            .unwrap();
4071
4072        let plans = engine
4073            .plans
4074            .get_plans(None, "test")
4075            .expect("plans for test");
4076        let plan = plans.values().next().expect("plan");
4077
4078        let values = input_data(&[("unknown", "30")]);
4079
4080        let evaluate_data = resolve_evaluate_data(plan, values);
4081        assert!(evaluate_data.bindings.is_empty());
4082        assert!(evaluate_data.ignored_unknown.iter().any(|k| k == "unknown"));
4083    }
4084
4085    #[test]
4086    fn test_with_raw_values_nested() {
4087        let mut engine = Engine::new();
4088        engine
4089            .load([(
4090                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
4091                    "test.lemma",
4092                ))),
4093                r#"
4094                spec private
4095                data base_price: number
4096
4097                spec test
4098                uses rules: private
4099                "#
4100                .to_string(),
4101            )])
4102            .unwrap();
4103
4104        let plans = engine
4105            .plans
4106            .get_plans(None, "test")
4107            .expect("plans for test");
4108        let plan = plans.values().next().expect("plan");
4109
4110        let values = input_data(&[("rules.base_price", "100")]);
4111
4112        let evaluate_data = resolve_evaluate_data(plan, values);
4113        let data_path = DataPath {
4114            segments: vec![PathSegment {
4115                uses: "rules".to_string(),
4116                repository: None,
4117                spec: "private".to_string(),
4118            }],
4119            data: "base_price".to_string(),
4120        };
4121        let updated_value = bound_value(&evaluate_data, &data_path).expect("bound value");
4122        match &updated_value.value {
4123            crate::planning::semantics::ValueKind::Number(n) => {
4124                assert_eq!(n, &rational_new(100, 1));
4125            }
4126            other => panic!("Expected number literal, got {:?}", other),
4127        }
4128    }
4129
4130    #[test]
4131    fn evaluate_data_should_enforce_number_maximum_constraint() {
4132        // Higher-standard requirement: user input must be validated against type constraints.
4133        // If this test fails, Lemma accepts invalid values and gives false reassurance.
4134        let data_path = DataPath::new(vec![], "x".to_string());
4135
4136        let max10 = crate::planning::semantics::LemmaType::primitive(
4137            crate::planning::semantics::TypeSpecification::Number {
4138                minimum: None,
4139                maximum: Some(rational_new(10, 1)),
4140                decimals: None,
4141                help: String::new(),
4142            },
4143        );
4144        let source = Source::new(
4145            crate::parsing::source::SourceType::Volatile,
4146            crate::parsing::ast::Span {
4147                start: 0,
4148                end: 0,
4149                line: 1,
4150                col: 0,
4151            },
4152        );
4153        let mut data = IndexMap::new();
4154        data.insert(
4155            data_path.clone(),
4156            crate::planning::semantics::DataDefinition::Value {
4157                value: crate::planning::semantics::LiteralValue::number(rational_new(0, 1)),
4158                resolved_type: Arc::new(max10.clone()),
4159                source: source.clone(),
4160            },
4161        );
4162
4163        let input_key_index = data.keys().map(|p| (p.input_key(), p.clone())).collect();
4164        let data_len = data.len();
4165        let plan = ExecutionPlan {
4166            spec_name: "test".to_string(),
4167            commentary: None,
4168            data,
4169            normal_forms: Vec::new(),
4170            rules: IndexMap::new(),
4171            data_reference_order: Vec::new(),
4172            meta: IndexMap::new(),
4173            resolved_types: ResolvedSpecTypes::default(),
4174            family_units: Arc::new(FamilyUnitCatalog::default()),
4175            signature_index: IndexMap::new(),
4176            effective: EffectiveDate::Origin,
4177            effective_from: None,
4178            effective_to: None,
4179            start_line: 1,
4180            source_type: None,
4181            needed_by_rules: vec![NeededByRules::default(); data_len],
4182            nested_needed_by_rules: IndexMap::new(),
4183            data_display: IndexMap::new(),
4184            show_rules: IndexMap::new(),
4185            reference_ends: IndexMap::new(),
4186            input_key_index,
4187            show_rule_index: IndexMap::new(),
4188            data_leaf: IndexMap::new(),
4189            row_templates: std::collections::BTreeMap::new(),
4190            planning_input: Arc::new(Vec::new()),
4191            identity: SpecIdentity {
4192                repository: None,
4193                spec: "test".to_string(),
4194                effective: EffectiveDate::Origin,
4195            },
4196            rule_candidates: std::collections::BTreeMap::new(),
4197            self_row_rules: Vec::new(),
4198            instance_lists: IndexMap::new(),
4199            row_mask: Vec::new(),
4200        };
4201
4202        let values = input_data(&[("x", "11")]);
4203
4204        let evaluate_data = resolve_evaluate_data(&plan, values);
4205        match veto_reason(&evaluate_data, &data_path) {
4206            Some(reason) => {
4207                assert!(
4208                    reason.contains("maximum") || reason.contains("10"),
4209                    "x=11 must violate maximum 10, got: {reason}"
4210                );
4211            }
4212            None => panic!("expected veto-bound data for x=11"),
4213        }
4214    }
4215
4216    #[test]
4217    fn evaluate_data_should_enforce_text_enum_options() {
4218        // Higher-standard requirement: enum options must be enforced for text types.
4219        let data_path = DataPath::new(vec![], "tier".to_string());
4220
4221        let tier = crate::planning::semantics::LemmaType::primitive(
4222            crate::planning::semantics::TypeSpecification::Text {
4223                length: None,
4224                options: vec!["silver".to_string(), "gold".to_string()],
4225                help: String::new(),
4226            },
4227        );
4228        let source = Source::new(
4229            crate::parsing::source::SourceType::Volatile,
4230            crate::parsing::ast::Span {
4231                start: 0,
4232                end: 0,
4233                line: 1,
4234                col: 0,
4235            },
4236        );
4237        let mut data = IndexMap::new();
4238        data.insert(
4239            data_path.clone(),
4240            crate::planning::semantics::DataDefinition::Value {
4241                value: crate::planning::semantics::LiteralValue::text("silver".to_string()),
4242                resolved_type: Arc::new(tier.clone()),
4243                source,
4244            },
4245        );
4246
4247        let input_key_index = data.keys().map(|p| (p.input_key(), p.clone())).collect();
4248        let data_len = data.len();
4249        let plan = ExecutionPlan {
4250            spec_name: "test".to_string(),
4251            commentary: None,
4252            data,
4253            normal_forms: Vec::new(),
4254            rules: IndexMap::new(),
4255            data_reference_order: Vec::new(),
4256            meta: IndexMap::new(),
4257            resolved_types: ResolvedSpecTypes::default(),
4258            family_units: Arc::new(FamilyUnitCatalog::default()),
4259            signature_index: IndexMap::new(),
4260            effective: EffectiveDate::Origin,
4261            effective_from: None,
4262            effective_to: None,
4263            start_line: 1,
4264            source_type: None,
4265            needed_by_rules: vec![NeededByRules::default(); data_len],
4266            nested_needed_by_rules: IndexMap::new(),
4267            data_display: IndexMap::new(),
4268            show_rules: IndexMap::new(),
4269            reference_ends: IndexMap::new(),
4270            input_key_index,
4271            show_rule_index: IndexMap::new(),
4272            data_leaf: IndexMap::new(),
4273            row_templates: std::collections::BTreeMap::new(),
4274            planning_input: Arc::new(Vec::new()),
4275            identity: SpecIdentity {
4276                repository: None,
4277                spec: "test".to_string(),
4278                effective: EffectiveDate::Origin,
4279            },
4280            rule_candidates: std::collections::BTreeMap::new(),
4281            self_row_rules: Vec::new(),
4282            instance_lists: IndexMap::new(),
4283            row_mask: Vec::new(),
4284        };
4285
4286        let values = input_data(&[("tier", "platinum")]);
4287
4288        let evaluate_data = resolve_evaluate_data(&plan, values);
4289        match veto_reason(&evaluate_data, &data_path) {
4290            Some(reason) => {
4291                assert!(
4292                    reason.contains("allowed options") || reason.contains("platinum"),
4293                    "invalid enum must record violation, got: {reason}"
4294                );
4295            }
4296            None => panic!("expected veto-bound data for tier=platinum"),
4297        }
4298    }
4299
4300    #[test]
4301    fn evaluate_data_should_enforce_measure_decimals() {
4302        // Higher-standard requirement: decimals should be enforced on measure inputs,
4303        // unless the language explicitly defines rounding semantics.
4304        let data_path = DataPath::new(vec![], "price".to_string());
4305
4306        let money = crate::planning::semantics::LemmaType::primitive(
4307            crate::planning::semantics::TypeSpecification::Measure {
4308                minimum: None,
4309                maximum: None,
4310                decimals: Some(2),
4311                units: crate::planning::semantics::MeasureUnits::from(vec![
4312                    crate::planning::semantics::MeasureUnit::from_decimal_factor(
4313                        "eur".to_string(),
4314                        rust_decimal::Decimal::from_str("1.0").unwrap(),
4315                        Vec::new(),
4316                    )
4317                    .expect("eur unit factor must be exact decimal"),
4318                ]),
4319                traits: Vec::new(),
4320                decomposition: None,
4321                unit: None,
4322                help: String::new(),
4323            },
4324        );
4325        let source = Source::new(
4326            crate::parsing::source::SourceType::Volatile,
4327            crate::parsing::ast::Span {
4328                start: 0,
4329                end: 0,
4330                line: 1,
4331                col: 0,
4332            },
4333        );
4334        let mut data = IndexMap::new();
4335        data.insert(
4336            data_path.clone(),
4337            crate::planning::semantics::DataDefinition::Value {
4338                value: crate::planning::semantics::LiteralValue::measure(rational_zero()),
4339                resolved_type: Arc::new(money.clone()),
4340                source,
4341            },
4342        );
4343
4344        let input_key_index = data.keys().map(|p| (p.input_key(), p.clone())).collect();
4345        let data_len = data.len();
4346        let plan = ExecutionPlan {
4347            spec_name: "test".to_string(),
4348            commentary: None,
4349            data,
4350            normal_forms: Vec::new(),
4351            rules: IndexMap::new(),
4352            data_reference_order: Vec::new(),
4353            meta: IndexMap::new(),
4354            resolved_types: ResolvedSpecTypes::default(),
4355            family_units: Arc::new(FamilyUnitCatalog::default()),
4356            signature_index: IndexMap::new(),
4357            effective: EffectiveDate::Origin,
4358            effective_from: None,
4359            effective_to: None,
4360            start_line: 1,
4361            source_type: None,
4362            needed_by_rules: vec![NeededByRules::default(); data_len],
4363            nested_needed_by_rules: IndexMap::new(),
4364            data_display: IndexMap::new(),
4365            show_rules: IndexMap::new(),
4366            reference_ends: IndexMap::new(),
4367            input_key_index,
4368            show_rule_index: IndexMap::new(),
4369            data_leaf: IndexMap::new(),
4370            row_templates: std::collections::BTreeMap::new(),
4371            planning_input: Arc::new(Vec::new()),
4372            identity: SpecIdentity {
4373                repository: None,
4374                spec: "test".to_string(),
4375                effective: EffectiveDate::Origin,
4376            },
4377            rule_candidates: std::collections::BTreeMap::new(),
4378            self_row_rules: Vec::new(),
4379            instance_lists: IndexMap::new(),
4380            row_mask: Vec::new(),
4381        };
4382
4383        let values = input_data(&[("price", "1.234 eur")]);
4384
4385        let evaluate_data = resolve_evaluate_data(&plan, values);
4386        match veto_reason(&evaluate_data, &data_path) {
4387            Some(reason) => {
4388                assert!(
4389                    reason.contains("decimals") || reason.contains("decimal"),
4390                    "1.234 eur must violate decimals=2, got: {reason}"
4391                );
4392            }
4393            None => panic!("expected veto-bound data for price=1.234 eur"),
4394        }
4395    }
4396
4397    fn empty_plan(effective: crate::parsing::ast::EffectiveDate) -> ExecutionPlan {
4398        ExecutionPlan {
4399            spec_name: "s".into(),
4400            commentary: None,
4401            data: IndexMap::new(),
4402            normal_forms: Vec::new(),
4403            rules: IndexMap::new(),
4404            data_reference_order: Vec::new(),
4405            meta: IndexMap::new(),
4406            resolved_types: ResolvedSpecTypes::default(),
4407            family_units: Arc::new(FamilyUnitCatalog::default()),
4408            signature_index: IndexMap::new(),
4409            effective,
4410            effective_from: None,
4411            effective_to: None,
4412            start_line: 1,
4413            source_type: None,
4414            needed_by_rules: Vec::new(),
4415            nested_needed_by_rules: IndexMap::new(),
4416            data_display: IndexMap::new(),
4417            show_rules: IndexMap::new(),
4418            reference_ends: IndexMap::new(),
4419            input_key_index: IndexMap::new(),
4420            show_rule_index: IndexMap::new(),
4421            data_leaf: IndexMap::new(),
4422            row_templates: std::collections::BTreeMap::new(),
4423            planning_input: Arc::new(Vec::new()),
4424            identity: SpecIdentity {
4425                repository: None,
4426                spec: "s".to_string(),
4427                effective: EffectiveDate::Origin,
4428            },
4429            rule_candidates: std::collections::BTreeMap::new(),
4430            self_row_rules: Vec::new(),
4431            instance_lists: IndexMap::new(),
4432            row_mask: Vec::new(),
4433        }
4434    }
4435
4436    fn plans_by_effective(
4437        plans: impl IntoIterator<Item = ExecutionPlan>,
4438    ) -> BTreeMap<EffectiveDate, std::sync::Arc<ExecutionPlan>> {
4439        plans
4440            .into_iter()
4441            .map(|plan| (plan.effective.clone(), std::sync::Arc::new(plan)))
4442            .collect()
4443    }
4444
4445    /// Compiled plans for one spec name are ordered by `effective` key.
4446    #[test]
4447    fn plan_set_plans_are_in_ascending_effective_order() {
4448        let june = DateTimeValue {
4449            year: 2025,
4450            month: 6,
4451            day: 1,
4452            hour: 0,
4453            minute: 0,
4454            second: 0,
4455            microsecond: 0,
4456            timezone: None,
4457            granularity: DateGranularity::Full,
4458        };
4459        let dec = DateTimeValue {
4460            year: 2025,
4461            month: 12,
4462            day: 1,
4463            hour: 0,
4464            minute: 0,
4465            second: 0,
4466            microsecond: 0,
4467            timezone: None,
4468            granularity: DateGranularity::Full,
4469        };
4470
4471        let plans = plans_by_effective([
4472            empty_plan(EffectiveDate::Origin),
4473            empty_plan(EffectiveDate::DateTimeValue(june)),
4474            empty_plan(EffectiveDate::DateTimeValue(dec)),
4475        ]);
4476
4477        let effectives: Vec<_> = plans.keys().cloned().collect();
4478        for window in effectives.windows(2) {
4479            assert!(
4480                window[0] < window[1],
4481                "plans must be strictly ascending: {:?} >= {:?}",
4482                window[0],
4483                window[1]
4484            );
4485        }
4486    }
4487
4488    #[test]
4489    fn plan_at_exact_boundary_selects_later_slice() {
4490        use crate::parsing::ast::{DateTimeValue, EffectiveDate};
4491
4492        let june = DateTimeValue {
4493            year: 2025,
4494            month: 6,
4495            day: 1,
4496            hour: 0,
4497            minute: 0,
4498            second: 0,
4499            microsecond: 0,
4500            timezone: None,
4501
4502            granularity: DateGranularity::Full,
4503        };
4504        let dec = DateTimeValue {
4505            year: 2025,
4506            month: 12,
4507            day: 1,
4508            hour: 0,
4509            minute: 0,
4510            second: 0,
4511            microsecond: 0,
4512            timezone: None,
4513
4514            granularity: DateGranularity::Full,
4515        };
4516
4517        let june_key = EffectiveDate::DateTimeValue(june.clone());
4518        let dec_key = EffectiveDate::DateTimeValue(dec.clone());
4519        let plans = plans_by_effective([
4520            empty_plan(EffectiveDate::Origin),
4521            empty_plan(june_key.clone()),
4522            empty_plan(dec_key.clone()),
4523        ]);
4524
4525        let june_plan = plan_at(&plans, &june_key).expect("boundary instant");
4526        assert!(std::ptr::eq(
4527            june_plan,
4528            plans.get(&june_key).expect("june slice").as_ref()
4529        ));
4530
4531        let dec_plan = plan_at(&plans, &dec_key).expect("dec boundary");
4532        assert!(std::ptr::eq(
4533            dec_plan,
4534            plans.get(&dec_key).expect("dec slice").as_ref()
4535        ));
4536    }
4537
4538    #[test]
4539    fn plan_at_day_before_boundary_stays_in_earlier_slice() {
4540        use crate::parsing::ast::{DateTimeValue, EffectiveDate};
4541
4542        let june = DateTimeValue {
4543            year: 2025,
4544            month: 6,
4545            day: 1,
4546            hour: 0,
4547            minute: 0,
4548            second: 0,
4549            microsecond: 0,
4550            timezone: None,
4551
4552            granularity: DateGranularity::Full,
4553        };
4554        let may_end = DateTimeValue {
4555            year: 2025,
4556            month: 5,
4557            day: 31,
4558            hour: 23,
4559            minute: 59,
4560            second: 59,
4561            microsecond: 0,
4562            timezone: None,
4563
4564            granularity: DateGranularity::DateTime,
4565        };
4566
4567        let origin = EffectiveDate::Origin;
4568        let plans = plans_by_effective([
4569            empty_plan(origin.clone()),
4570            empty_plan(EffectiveDate::DateTimeValue(june)),
4571        ]);
4572
4573        let may_instant = EffectiveDate::DateTimeValue(may_end);
4574        let may_plan = plan_at(&plans, &may_instant).expect("may 31");
4575        assert!(std::ptr::eq(
4576            may_plan,
4577            plans.get(&origin).expect("origin slice").as_ref()
4578        ));
4579    }
4580
4581    #[test]
4582    fn plan_at_single_plan_matches_any_instant_after_start() {
4583        use crate::parsing::ast::{DateTimeValue, EffectiveDate};
4584
4585        let t = DateTimeValue {
4586            year: 2025,
4587            month: 3,
4588            day: 1,
4589            hour: 0,
4590            minute: 0,
4591            second: 0,
4592            microsecond: 0,
4593            timezone: None,
4594
4595            granularity: DateGranularity::Full,
4596        };
4597        let start = EffectiveDate::DateTimeValue(DateTimeValue {
4598            year: 2025,
4599            month: 1,
4600            day: 1,
4601            hour: 0,
4602            minute: 0,
4603            second: 0,
4604            microsecond: 0,
4605            timezone: None,
4606
4607            granularity: DateGranularity::Full,
4608        });
4609        let plans = plans_by_effective([empty_plan(start.clone())]);
4610        let instant = EffectiveDate::DateTimeValue(t);
4611        let selected = plan_at(&plans, &instant).expect("inside single slice");
4612        assert!(std::ptr::eq(
4613            selected,
4614            plans.get(&start).expect("single slice").as_ref()
4615        ));
4616    }
4617
4618    /// The show JSON shape is the IO contract for every non-Rust consumer
4619    /// (WASM playground, Hex, HTTP, TypeScript). Nail the exact envelope.
4620    #[test]
4621    fn show_json_shape_contract() {
4622        let mut engine = Engine::new();
4623        engine
4624            .load([(
4625                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
4626                    "test.lemma",
4627                ))),
4628                r#"
4629                spec pricing
4630                data bridge_height: measure
4631                  -> unit meter: 1
4632                  -> suggest 100 meter
4633                data quantity: number -> minimum 0
4634                rule cost: bridge_height * quantity
4635                "#
4636                .to_string(),
4637            )])
4638            .unwrap();
4639        let now = DateTimeValue::now();
4640        let schema = engine.show(None, "pricing", Some(&now)).unwrap();
4641
4642        let value = crate::api::show_document(&engine, &schema);
4643
4644        let bh = &value["properties"]["bridge_height"];
4645        assert!(
4646            bh.is_object(),
4647            "data entry must be a named object, not tuple"
4648        );
4649        assert!(
4650            bh.get("x-lemma-type").is_some(),
4651            "data entry must expose `x-lemma-type` field"
4652        );
4653        assert!(
4654            bh.get("x-lemma-suggestion").is_some(),
4655            "bridge_height exposes `-> suggest` as schema suggestion"
4656        );
4657        assert!(
4658            bh.get("x-lemma-value").is_none(),
4659            "bridge_height is not filled from spec"
4660        );
4661
4662        let ty = &bh["x-lemma-type"];
4663        assert_eq!(
4664            ty["kind"], "measure",
4665            "kind tag sits on the type object itself"
4666        );
4667        assert!(
4668            ty["units"].is_array(),
4669            "measure-only fields flatten up to top level"
4670        );
4671        assert!(
4672            ty.get("options").is_none(),
4673            "text-only fields must not leak"
4674        );
4675
4676        let quantity = &value["properties"]["quantity"];
4677        assert_eq!(quantity["x-lemma-type"]["kind"], "number");
4678        assert!(
4679            quantity.get("x-lemma-suggestion").is_none(),
4680            "quantity has no suggestion"
4681        );
4682        assert!(
4683            quantity.get("x-lemma-value").is_none(),
4684            "quantity has no fill literal"
4685        );
4686
4687        let cost = &value["x-lemma-rules"]["cost"];
4688        assert_eq!(
4689            cost["type"]["kind"], "measure",
4690            "rule types nest under type"
4691        );
4692        assert!(
4693            cost["type"]["units"].is_array()
4694                && !cost["type"]["units"].as_array().unwrap().is_empty(),
4695            "measure rule result types expose declared units"
4696        );
4697        assert!(
4698            cost["type"]["units"][0].get("factor").is_some(),
4699            "measure rule units use factor field"
4700        );
4701        assert!(cost.get("branches").is_some(), "ShowRule exposes branches");
4702        assert!(
4703            cost.get("depends_on_rules").is_some(),
4704            "ShowRule exposes depends_on_rules"
4705        );
4706    }
4707
4708    #[test]
4709    fn show_rule_result_units_contract() {
4710        let mut engine = Engine::new();
4711        engine
4712            .load([(
4713                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
4714                    "units_contract.lemma",
4715                ))),
4716                r#"
4717                spec units_contract
4718                data money: measure
4719                  -> unit eur: 1
4720                  -> unit usd: 0.91
4721                data rate: ratio
4722                  -> unit basis_points: 10000
4723                  -> unit percent: 100
4724                  -> suggest 500 basis_points
4725                rule total: money
4726                rule rate_out: rate
4727                "#
4728                .to_string(),
4729            )])
4730            .unwrap();
4731        let now = DateTimeValue::now();
4732        let schema = engine.show(None, "units_contract", Some(&now)).unwrap();
4733        let value = crate::api::show_document(&engine, &schema);
4734
4735        let money_units = &value["properties"]["money"]["x-lemma-type"]["units"];
4736        assert!(money_units.is_array() && !money_units.as_array().unwrap().is_empty());
4737        assert!(money_units[0].get("name").is_some());
4738        assert!(money_units[0].get("factor").is_some());
4739        assert!(money_units[0]["factor"].get("numer").is_some());
4740        assert!(money_units[0]["factor"].get("denom").is_some());
4741
4742        let rate_units = &value["properties"]["rate"]["x-lemma-type"]["units"];
4743        assert!(rate_units.is_array() && !rate_units.as_array().unwrap().is_empty());
4744        assert!(rate_units[0].get("name").is_some());
4745        assert!(rate_units[0].get("value").is_some());
4746        assert!(rate_units[0]["value"].get("numer").is_some());
4747        assert!(rate_units[0]["value"].get("denom").is_some());
4748
4749        let total_rule_units = &value["x-lemma-rules"]["total"]["type"]["units"];
4750        let money_unit_names: Vec<_> = money_units
4751            .as_array()
4752            .unwrap()
4753            .iter()
4754            .map(|u| u["name"].as_str().unwrap())
4755            .collect();
4756        let total_rule_unit_names: Vec<_> = total_rule_units
4757            .as_array()
4758            .unwrap()
4759            .iter()
4760            .map(|u| u["name"].as_str().unwrap())
4761            .collect();
4762        assert_eq!(total_rule_unit_names, money_unit_names);
4763
4764        let rate_out_rule_units = &value["x-lemma-rules"]["rate_out"]["type"]["units"];
4765        let rate_unit_names: Vec<_> = rate_units
4766            .as_array()
4767            .unwrap()
4768            .iter()
4769            .map(|u| u["name"].as_str().unwrap())
4770            .collect();
4771        let rate_out_rule_unit_names: Vec<_> = rate_out_rule_units
4772            .as_array()
4773            .unwrap()
4774            .iter()
4775            .map(|u| u["name"].as_str().unwrap())
4776            .collect();
4777        assert_eq!(rate_out_rule_unit_names, rate_unit_names);
4778    }
4779
4780    #[test]
4781    fn show_document_rules_round_trip_as_show_rule() {
4782        let mut engine = Engine::new();
4783        engine
4784            .load([(
4785                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("s.lemma"))),
4786                r#"
4787                spec s
4788                data age: number -> minimum 0 -> suggest 18
4789                data grade: text -> options "A", "B", "C"
4790                rule adult: age >= 18
4791                "#
4792                .to_string(),
4793            )])
4794            .unwrap();
4795        let now = DateTimeValue::now();
4796        let schema = engine.show(None, "s", Some(&now)).unwrap();
4797
4798        let document = crate::api::show_document(&engine, &schema);
4799        let rule_json = document["x-lemma-rules"]["adult"].clone();
4800        let rule: crate::api::ShowRule = serde_json::from_value(rule_json.clone()).unwrap();
4801        assert_eq!(serde_json::to_value(&rule).unwrap(), rule_json);
4802        assert!(
4803            document["properties"].get("grade").is_none(),
4804            "unused grade is Types-only, not Data"
4805        );
4806        let grade = &document["x-lemma-types"]["grade"];
4807        assert_eq!(grade["options"], serde_json::json!(["A", "B", "C"]));
4808        assert_eq!(grade["kind"], "text");
4809    }
4810
4811    const COST_PRICE_SPEC: &str = r#"
4812spec cost_price
4813uses lemma units
4814
4815data money: measure
4816  -> unit eur: 1.00
4817  -> unit inr: 0.0092
4818  -> decimals 2
4819
4820data labor_cost: measure
4821  -> unit eur_per_hour: eur/hour
4822  -> unit inr_per_hour: inr/hour
4823  -> suggest 25 eur_per_hour
4824
4825data product_cost: measure
4826  -> unit eur_per_kg: eur/kilogram
4827  -> unit inr_per_kg: inr/kilogram
4828  -> suggest 4 eur_per_kg
4829
4830data throughput: measure
4831  -> unit kg_per_hour: kilogram/hour
4832  -> suggest 12 kg_per_hour
4833
4834rule cost_price: product_cost + labor_cost / throughput
4835"#;
4836
4837    fn cost_price_inputs() -> IndexMap<String, EvaluateDataValue> {
4838        let mut data = IndexMap::new();
4839        data.insert(
4840            "product_cost".into(),
4841            EvaluateDataValue::string("4 eur_per_kg"),
4842        );
4843        data.insert(
4844            "labor_cost".into(),
4845            EvaluateDataValue::string("25 eur_per_hour"),
4846        );
4847        data.insert(
4848            "throughput".into(),
4849            EvaluateDataValue::string("12 kg_per_hour"),
4850        );
4851        data
4852    }
4853
4854    const FILM_ACCESS: &str = r#"
4855spec premium_membership
4856uses lemma units
4857data start: date
4858data length: units.calendar
4859rule valid: now in start...start + length
4860
4861spec film_access
4862uses premium_membership
4863data type: text
4864  -> option "rental"
4865  -> option "purchase"
4866data views_consumed: number
4867data premium_member: boolean
4868rule max_views: 3
4869  unless premium_membership.valid then 10
4870  unless premium_member then 5
4871rule can_view: no
4872  unless type is "rental" and views_consumed < max_views then yes
4873  unless type is "purchase" then yes
4874"#;
4875
4876    fn film_access_effective() -> DateTimeValue {
4877        DateTimeValue {
4878            year: 2027,
4879            month: 2,
4880            day: 14,
4881            hour: 12,
4882            minute: 0,
4883            second: 0,
4884            microsecond: 0,
4885            timezone: Some(TimezoneValue { offset_minutes: 0 }),
4886            granularity: DateGranularity::DateTime,
4887        }
4888    }
4889
4890    #[test]
4891    fn evaluate_data_accepts_per_unit_measure_equivalent_to_canonical_magnitude() {
4892        let mut engine = Engine::new();
4893        engine
4894            .load([(
4895                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
4896                    "cost_price.lemma",
4897                ))),
4898                COST_PRICE_SPEC.to_string(),
4899            )])
4900            .expect("load");
4901        let plans = engine
4902            .plans
4903            .get_plans(None, "cost_price")
4904            .expect("plans for cost_price");
4905        let plan = plans.values().next().expect("plan");
4906        let mut data = IndexMap::new();
4907        data.insert(
4908            "product_cost".into(),
4909            EvaluateDataValue::string("4 eur_per_kg"),
4910        );
4911        data.insert(
4912            "labor_cost".into(),
4913            EvaluateDataValue::string("0.0069444444444444444444444444 eur_per_hour"),
4914        );
4915        data.insert(
4916            "throughput".into(),
4917            EvaluateDataValue::string("0.0033333333333333333333333333 kg_per_hour"),
4918        );
4919        let evaluate_data = resolve_evaluate_data(plan, data);
4920        assert!(
4921            !evaluate_data
4922                .bindings
4923                .values()
4924                .any(|b| matches!(b, OperationResult::Veto(_))),
4925            "parsed decimal run data values must not be veto-bound after input boundary: {:?}",
4926            evaluate_data.bindings
4927        );
4928    }
4929
4930    #[test]
4931    fn evaluate_data_accepts_per_unit_measure() {
4932        let mut engine = Engine::new();
4933        engine
4934            .load([(
4935                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
4936                    "cost_price.lemma",
4937                ))),
4938                COST_PRICE_SPEC.to_string(),
4939            )])
4940            .expect("load");
4941        let plans = engine
4942            .plans
4943            .get_plans(None, "cost_price")
4944            .expect("plans for cost_price");
4945        let plan = plans.values().next().expect("plan");
4946        let evaluate_data = resolve_evaluate_data(plan, cost_price_inputs());
4947        assert!(!evaluate_data
4948            .bindings
4949            .values()
4950            .any(|b| matches!(b, OperationResult::Veto(_))));
4951    }
4952
4953    #[test]
4954    fn evaluate_data_rejects_oversize_input() {
4955        let mut engine = Engine::new();
4956        engine
4957            .load([(
4958                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
4959                    "cost_price.lemma",
4960                ))),
4961                COST_PRICE_SPEC.to_string(),
4962            )])
4963            .expect("load");
4964        let plans = engine
4965            .plans
4966            .get_plans(None, "cost_price")
4967            .expect("plans for cost_price");
4968        let plan = plans.values().next().expect("plan");
4969        let mut data = cost_price_inputs();
4970        data.insert(
4971            "labor_cost".into(),
4972            EvaluateDataValue::string(
4973                "1000000000000000000000000000000000000000000000000000000000000 eur_per_hour",
4974            ),
4975        );
4976        let evaluate_data = resolve_evaluate_data(plan, data);
4977        assert!(matches!(
4978            evaluate_data
4979                .bindings
4980                .get(&DataPath::local("labor_cost".into())),
4981            Some(OperationResult::Veto(_))
4982        ));
4983    }
4984    #[test]
4985    fn typedecl_default_stays_typedecl_on_immutable_plan() {
4986        let mut engine = Engine::new();
4987        engine
4988            .load([(
4989                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("s.lemma"))),
4990                r#"
4991        spec s
4992        data n: number -> suggest 42
4993        rule r: n
4994    "#
4995                .to_string(),
4996            )])
4997            .expect("load");
4998
4999        let plans = engine.plans.get_plans(None, "s").expect("plans for s");
5000        let plan = plans.values().next().expect("plan");
5001        let path = DataPath::local("n".into());
5002        match plan.data.get(&path).expect("n") {
5003            DataDefinition::TypeDeclaration {
5004                declared_suggestion: Some(_),
5005                ..
5006            } => {}
5007            other => panic!("expected TypeDeclaration with default, got {other:?}"),
5008        }
5009    }
5010
5011    fn response_missing_data_union(response: &crate::Response) -> Vec<String> {
5012        let mut seen = std::collections::HashSet::new();
5013        let mut names = Vec::new();
5014        for result in response.results.values() {
5015            for key in result.missing_data() {
5016                if seen.insert(key.clone()) {
5017                    names.push(key.clone());
5018                }
5019            }
5020        }
5021        names
5022    }
5023
5024    #[test]
5025    fn evaluate_prunes_inactive_nut_branches_for_total_price() {
5026        let code = r#"
5027spec bag
5028uses lemma units
5029
5030data weight: measure
5031  -> unit kg: 1
5032
5033data money: measure
5034  -> unit eur: 1
5035
5036data price_per_weight: measure
5037  -> unit eur_per_kg: eur/kg
5038
5039data item_cost: price_per_weight
5040data roasting: price_per_weight
5041data chocolatizing: price_per_weight
5042
5043rule total_price: weight * (item_cost + roasting + chocolatizing)
5044
5045spec calc
5046uses bag
5047  -> with item_cost: item_cost
5048  -> with roasting: roasting
5049
5050data type_of_nut: text -> options "peanut", "cashew"
5051
5052rule price_peanut: 1.5 eur_per_kg
5053rule price_peanut_roasting: 0.45 eur_per_kg
5054
5055rule price_cashew: 2.0 eur_per_kg
5056rule price_cashew_roasting: 0.55 eur_per_kg
5057
5058rule item_cost: veto "No item cost"
5059  unless type_of_nut is "peanut" then price_peanut
5060  unless type_of_nut is "cashew" then price_cashew
5061
5062rule roasting: veto "No roasting"
5063  unless type_of_nut is "peanut" then price_peanut_roasting
5064  unless type_of_nut is "cashew" then price_cashew_roasting
5065
5066rule total_price: bag.total_price
5067"#;
5068
5069        let mut engine = Engine::new();
5070        engine
5071            .load([(
5072                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
5073                    "calc.lemma",
5074                ))),
5075                code.to_string(),
5076            )])
5077            .unwrap();
5078
5079        let now = DateTimeValue::now();
5080        let mut inputs = IndexMap::new();
5081        inputs.insert("type_of_nut".to_string(), "peanut".into());
5082        let response = engine
5083            .evaluate(
5084                None,
5085                "calc",
5086                Some(&now),
5087                inputs,
5088                Some(&["total_price".to_string()]),
5089                false,
5090            )
5091            .expect("evaluate must succeed");
5092
5093        assert_eq!(
5094            response_missing_data_union(&response),
5095            vec!["bag.weight".to_string(), "bag.chocolatizing".to_string(),],
5096            "supplied type_of_nut and with-bound roasting/item_cost omitted"
5097        );
5098    }
5099
5100    #[test]
5101    fn evaluate_includes_membership_dates_when_premium_member_false() {
5102        let mut engine = Engine::new();
5103        engine
5104            .load([(crate::SourceType::Volatile, FILM_ACCESS.to_string())])
5105            .expect("film_access spec must load");
5106        let now = film_access_effective();
5107        let mut inputs = IndexMap::new();
5108        inputs.insert("type".to_string(), "rental".into());
5109        inputs.insert("views_consumed".to_string(), "6".into());
5110        inputs.insert("premium_member".to_string(), "false".into());
5111        let response = engine
5112            .evaluate(
5113                None,
5114                "film_access",
5115                Some(&now),
5116                inputs,
5117                Some(&["can_view".to_string()]),
5118                false,
5119            )
5120            .expect("evaluate must succeed");
5121
5122        assert_eq!(
5123            response_missing_data_union(&response),
5124            vec![
5125                "premium_membership.start".to_string(),
5126                "premium_membership.length".to_string(),
5127            ],
5128            "membership dates when premium_member is false"
5129        );
5130    }
5131
5132    #[test]
5133    fn evaluate_includes_membership_dates_when_premium_member_unknown() {
5134        let mut engine = Engine::new();
5135        engine
5136            .load([(crate::SourceType::Volatile, FILM_ACCESS.to_string())])
5137            .expect("film_access spec must load");
5138        let now = film_access_effective();
5139        let mut inputs = IndexMap::new();
5140        inputs.insert("type".to_string(), "rental".into());
5141        inputs.insert("views_consumed".to_string(), "6".into());
5142        let response = engine
5143            .evaluate(
5144                None,
5145                "film_access",
5146                Some(&now),
5147                inputs,
5148                Some(&["can_view".to_string()]),
5149                false,
5150            )
5151            .expect("evaluate must succeed");
5152
5153        assert_eq!(
5154            response_missing_data_union(&response),
5155            vec![
5156                "premium_member".to_string(),
5157                "premium_membership.start".to_string(),
5158                "premium_membership.length".to_string(),
5159            ],
5160            "premium_member plus membership dates when unknown"
5161        );
5162    }
5163
5164    const UNITS_SPEC: &str = r#"
5165spec units
5166uses lemma units
5167data money: measure
5168  -> unit eur: 1
5169  -> decimals 2
5170"#;
5171
5172    const WAREHOUSING_SPEC: &str = r#"
5173spec warehousing
5174uses units
5175uses si: lemma units
5176
5177data units_per_pallet: number
5178  -> minimum 1
5179  -> suggest 1
5180
5181data storage_duration: si.duration
5182  -> minimum 0 week
5183  -> suggest 10 day
5184
5185data interbranch_transport_per_pallet: units.money
5186  -> minimum 0 eur
5187  -> suggest 0 eur
5188
5189data inbound_handling_per_pallet: units.money
5190  -> minimum 0 eur
5191  -> suggest 0 eur
5192
5193data storage_per_pallet_per_week: units.money
5194  -> minimum 0 eur
5195  -> suggest 10 eur
5196
5197data labeling_per_pallet: units.money
5198  -> minimum 0 eur
5199  -> suggest 0 eur
5200
5201data outbound_handling_per_pallet: units.money
5202  -> minimum 0 eur
5203  -> suggest 0 eur
5204
5205rule storage_cost_per_pallet:
5206  storage_per_pallet_per_week
5207  * ceil (storage_duration as week as number)
5208
5209rule total_logistics_per_pallet:
5210  interbranch_transport_per_pallet
5211  + inbound_handling_per_pallet
5212  + storage_cost_per_pallet
5213  + labeling_per_pallet
5214  + outbound_handling_per_pallet
5215
5216rule total_logistics_per_ce:
5217  total_logistics_per_pallet / units_per_pallet
5218"#;
5219
5220    const QUOTATION_SPEC: &str = r#"
5221spec quotation
5222uses wh: warehousing
5223rule total: wh.total_logistics_per_ce
5224"#;
5225
5226    fn load_cross_spec_fixtures(engine: &mut Engine) {
5227        engine
5228            .load([(crate::SourceType::Volatile, UNITS_SPEC.to_string())])
5229            .expect("units spec must load");
5230        engine
5231            .load([(crate::SourceType::Volatile, WAREHOUSING_SPEC.to_string())])
5232            .expect("warehousing spec must load");
5233    }
5234
5235    #[test]
5236    fn quotation_inherits_unit_scope_from_warehousing_import() {
5237        let mut engine = Engine::new();
5238        load_cross_spec_fixtures(&mut engine);
5239        engine
5240            .load([(crate::SourceType::Volatile, QUOTATION_SPEC.to_string())])
5241            .expect("quotation must plan without uses lemma units");
5242        let plans = engine
5243            .plans
5244            .get_plans(None, "quotation")
5245            .expect("plans for quotation");
5246        let plan = plans.values().next().expect("plan");
5247
5248        let expression_units = &plan.resolved_types.unit_index;
5249        expression_units
5250            .resolve("week")
5251            .expect("week reaches quotation via wh → si");
5252        expression_units
5253            .resolve("wh.week")
5254            .expect("alias.unit resolves week in warehousing scope");
5255        expression_units
5256            .resolve("minute")
5257            .expect("minute reaches quotation via transitive unit scope");
5258    }
5259
5260    fn warehousing_default_inputs(prefix: &str) -> IndexMap<String, EvaluateDataValue> {
5261        let key = |name: &str| {
5262            if prefix.is_empty() {
5263                name.to_string()
5264            } else {
5265                format!("{prefix}.{name}")
5266            }
5267        };
5268        IndexMap::from([
5269            (key("units_per_pallet"), EvaluateDataValue::string("1")),
5270            (key("storage_duration"), EvaluateDataValue::string("10 day")),
5271            (
5272                key("interbranch_transport_per_pallet"),
5273                EvaluateDataValue::string("0 eur"),
5274            ),
5275            (
5276                key("inbound_handling_per_pallet"),
5277                EvaluateDataValue::string("0 eur"),
5278            ),
5279            (
5280                key("storage_per_pallet_per_week"),
5281                EvaluateDataValue::string("10 eur"),
5282            ),
5283            (
5284                key("labeling_per_pallet"),
5285                EvaluateDataValue::string("0 eur"),
5286            ),
5287            (
5288                key("outbound_handling_per_pallet"),
5289                EvaluateDataValue::string("0 eur"),
5290            ),
5291        ])
5292    }
5293
5294    #[test]
5295    fn quotation_evaluates_cross_spec_duration_conversion() {
5296        let mut engine = Engine::new();
5297        load_cross_spec_fixtures(&mut engine);
5298        engine
5299            .load([(crate::SourceType::Volatile, QUOTATION_SPEC.to_string())])
5300            .expect("quotation must load");
5301        let plans = engine
5302            .plans
5303            .get_plans(None, "quotation")
5304            .expect("plans for quotation");
5305        let plan = plans.values().next().expect("plan");
5306        assert!(
5307            plan.resolved_types
5308                .unit_index
5309                .unique_owner("week")
5310                .is_none(),
5311            "consumer unit_index must not contain week"
5312        );
5313        let now = DateTimeValue::now();
5314        let response = engine
5315            .evaluate(
5316                None,
5317                "quotation",
5318                Some(&now),
5319                warehousing_default_inputs("wh"),
5320                None,
5321                false,
5322            )
5323            .expect("quotation must evaluate");
5324        let display = response
5325            .results
5326            .get("total")
5327            .expect("rule total must be present")
5328            .result()
5329            .expect("total must have display")
5330            .to_string();
5331        assert_eq!(
5332            display, "20.00 eur",
5333            "10 eur/week * ceil(10 day as week) / 1 CE must be 20.00 eur, got: {display}"
5334        );
5335    }
5336
5337    #[test]
5338    fn ratio_range_default_endpoints_must_be_ratio_not_measure() {
5339        let code = r#"
5340spec policy
5341data allowed_band: ratio range -> suggest 10%...50%
5342rule band: allowed_band
5343"#;
5344        let mut engine = Engine::new();
5345        engine
5346            .load([(
5347                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
5348                    "ratio_range_endpoint_typing.lemma",
5349                ))),
5350                code.to_string(),
5351            )])
5352            .unwrap();
5353
5354        let plans = engine
5355            .plans
5356            .get_plans(None, "policy")
5357            .expect("plans for policy");
5358        let plan = plans.values().next().expect("plan");
5359        let path = DataPath::local("allowed_band".into());
5360        let def = plan.data.get(&path).expect("allowed_band in plan.data");
5361        let suggestion = def
5362            .bound_suggestion()
5363            .expect("declared default must exist")
5364            .to_literal();
5365
5366        let (left, right) = match &suggestion.value {
5367            crate::planning::semantics::ValueKind::Range(l, r) => (l.as_ref(), r.as_ref()),
5368            other => panic!("expected Range, got {other:?}"),
5369        };
5370        for (label, endpoint) in [("left", left), ("right", right)] {
5371            assert!(
5372                matches!(
5373                    &endpoint.value,
5374                    crate::planning::semantics::ValueKind::Ratio(_)
5375                ),
5376                "{label} endpoint must be Ratio for a percent literal in a ratio range default, got {:?}",
5377                endpoint.value
5378            );
5379            assert!(
5380                matches!(
5381                    &endpoint.value,
5382                    crate::planning::semantics::ValueKind::Ratio(_)
5383                ),
5384                "{label} endpoint ValueKind must be Ratio (got {:?})",
5385                endpoint.value
5386            );
5387        }
5388    }
5389
5390    #[test]
5391    fn ratio_range_typedef_with_second_ratio_field_loads() {
5392        let code = r#"
5393spec policy
5394data margin_pct: ratio -> suggest 15%
5395data allowed_band: ratio range
5396rule margin: margin_pct
5397rule band_slot: allowed_band
5398"#;
5399        let mut engine = Engine::new();
5400        engine
5401            .load([(
5402                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
5403                    "ratio_range_load.lemma",
5404                ))),
5405                code.to_string(),
5406            )])
5407            .unwrap();
5408
5409        let plans = engine
5410            .plans
5411            .get_plans(None, "policy")
5412            .expect("plans for policy");
5413        let plan = plans.values().next().expect("plan");
5414        let path = DataPath::local("allowed_band".into());
5415        let def = plan.data.get(&path).expect("allowed_band in plan.data");
5416        let lemma_type = def
5417            .schema_type()
5418            .expect("allowed_band must be a typed data slot");
5419        match &lemma_type.specifications {
5420            TypeSpecification::RatioRange { units, .. } => {
5421                let names: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
5422                assert!(
5423                    names.contains(&"percent"),
5424                    "ratio range must inherit builtin percent, got {names:?}"
5425                );
5426            }
5427            other => panic!("allowed_band must be RatioRange, got {other:?}"),
5428        }
5429    }
5430
5431    #[test]
5432    fn show_display_indents_every_commentary_line() {
5433        let mut engine = Engine::new();
5434        engine
5435            .load([(
5436                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
5437                    "alpha2.lemma",
5438                ))),
5439                "spec alpha2\n\"\"\"\nAdded:\nSS: South Sudan\n\nRemoved:\nYU: Yugoslavia\n\"\"\"\ndata code: text\nrule name: code\n"
5440                    .to_string(),
5441            )])
5442            .unwrap();
5443        let now = DateTimeValue::now();
5444        let text = engine.show(None, "alpha2", Some(&now)).unwrap().to_string();
5445        assert!(
5446            text.starts_with(
5447                "Spec: alpha2\n  Added:\n  SS: South Sudan\n\n  Removed:\n  YU: Yugoslavia\n"
5448            ),
5449            "{text}"
5450        );
5451    }
5452
5453    #[test]
5454    fn show_display_puts_effective_window_on_header_without_other_versions() {
5455        let mut engine = Engine::new();
5456        engine
5457            .load([(
5458                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
5459                    "rates.lemma",
5460                ))),
5461                "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"
5462                    .to_string(),
5463            )])
5464            .unwrap();
5465        let effective: DateTimeValue = "2024-06-01".parse().expect("date must parse");
5466        let text = engine
5467            .show(None, "rates", Some(&effective))
5468            .unwrap()
5469            .to_string();
5470        assert!(
5471            text.starts_with("Spec: rates 2024-01-01...2025-01-01\n  Standard rate\n"),
5472            "{text}"
5473        );
5474        assert!(!text.contains("versions"), "{text}");
5475        assert!(!text.contains("effective_"), "{text}");
5476    }
5477
5478    #[test]
5479    fn show_display_undated_predecessor_uses_until() {
5480        let mut engine = Engine::new();
5481        engine
5482            .load([(
5483                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
5484                    "calc.lemma",
5485                ))),
5486                "spec calc\ndata x: 1\nrule r: x\n\nspec calc 2027\ndata x: 2\nrule r: x\n"
5487                    .to_string(),
5488            )])
5489            .unwrap();
5490        let effective: DateTimeValue = "2026-01-01".parse().expect("date must parse");
5491        let text = engine
5492            .show(None, "calc", Some(&effective))
5493            .unwrap()
5494            .to_string();
5495        assert!(text.starts_with("Spec: calc ...2027\n"), "{text}");
5496        assert!(!text.contains('→'), "{text}");
5497    }
5498
5499    fn nested_import_plan() -> (Engine, Arc<ExecutionPlan>) {
5500        let mut engine = Engine::new();
5501        engine
5502            .load([(
5503                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
5504                    "peel.lemma",
5505                ))),
5506                r#"
5507spec leaf
5508data x: number
5509rule rx: x
5510
5511spec mid
5512uses nested: leaf
5513data y: number
5514rule ry: y
5515
5516spec outer
5517uses m: mid
5518"#
5519                .to_string(),
5520            )])
5521            .expect("load nested import fixture");
5522        let plans = engine
5523            .plans
5524            .get_plans(None, "outer")
5525            .expect("plans for outer");
5526        let plan = Arc::clone(plans.values().next().expect("outer plan"));
5527        (engine, plan)
5528    }
5529
5530    #[test]
5531    fn input_key_index_matches_input_key_entries_for_nested_plan() {
5532        let (_, plan) = nested_import_plan();
5533
5534        let mut entry_keys: Vec<String> = plan
5535            .input_key_entries()
5536            .into_iter()
5537            .map(|(path, _)| path.input_key())
5538            .collect();
5539        let mut index_keys: Vec<String> = plan.input_key_index.keys().cloned().collect();
5540        entry_keys.sort();
5541        index_keys.sort();
5542        assert_eq!(entry_keys, index_keys);
5543
5544        let show_keys: Vec<String> = plan
5545            .nested_input_keys()
5546            .into_iter()
5547            .map(|(path, _)| path.input_key())
5548            .collect();
5549        assert!(!show_keys.iter().any(|key| key == "m"), "{show_keys:?}");
5550        assert!(entry_keys.iter().any(|key| key == "m"), "{entry_keys:?}");
5551        assert!(
5552            show_keys.len() < entry_keys.len(),
5553            "{show_keys:?} vs {entry_keys:?}"
5554        );
5555    }
5556
5557    #[test]
5558    fn peel_data_local_one_hop_two_hops_and_non_import() {
5559        let (_, plan) = nested_import_plan();
5560
5561        let local = DataPath::local("m".into());
5562        let (_, path, def) = plan
5563            .peel_data(&local)
5564            .expect("local import marker peels on outer");
5565        assert_eq!(path.data, "m");
5566        assert!(matches!(def, DataDefinition::Import { .. }));
5567
5568        let one = DataPath {
5569            segments: vec![PathSegment {
5570                uses: "m".into(),
5571                repository: None,
5572                spec: "mid".into(),
5573            }],
5574            data: "y".into(),
5575        };
5576        let (body, path, def) = plan.peel_data(&one).expect("one hop");
5577        assert_eq!(body.identity.spec, "mid");
5578        assert_eq!(path.data, "y");
5579        assert!(matches!(
5580            def,
5581            DataDefinition::Value { .. } | DataDefinition::TypeDeclaration { .. }
5582        ));
5583
5584        let two = DataPath {
5585            segments: vec![
5586                PathSegment {
5587                    uses: "m".into(),
5588                    repository: None,
5589                    spec: "mid".into(),
5590                },
5591                PathSegment {
5592                    uses: "nested".into(),
5593                    repository: None,
5594                    spec: "leaf".into(),
5595                },
5596            ],
5597            data: "x".into(),
5598        };
5599        let (body, path, def) = plan.peel_data(&two).expect("two hops");
5600        assert_eq!(body.identity.spec, "leaf");
5601        assert_eq!(path.data, "x");
5602        assert!(matches!(
5603            def,
5604            DataDefinition::Value { .. } | DataDefinition::TypeDeclaration { .. }
5605        ));
5606
5607        let bad = DataPath {
5608            segments: vec![PathSegment {
5609                uses: "y".into(),
5610                repository: None,
5611                spec: "mid".into(),
5612            }],
5613            data: "x".into(),
5614        };
5615        assert!(plan.peel_data(&bad).is_none());
5616
5617        let (mid, _, _) = plan.peel_data(&one).expect("mid for non-import hop");
5618        let non_import_hop = DataPath {
5619            segments: vec![PathSegment {
5620                uses: "y".into(),
5621                repository: None,
5622                spec: "mid".into(),
5623            }],
5624            data: "x".into(),
5625        };
5626        assert!(mid.peel_data(&non_import_hop).is_none());
5627    }
5628
5629    #[test]
5630    fn peel_rule_local_one_hop_two_hops() {
5631        let (_, plan) = nested_import_plan();
5632
5633        let one = RulePath {
5634            segments: vec![PathSegment {
5635                uses: "m".into(),
5636                repository: None,
5637                spec: "mid".into(),
5638            }],
5639            rule: "ry".into(),
5640        };
5641        let (body, local) = plan.peel_rule(&one).expect("one hop rule");
5642        assert_eq!(body.identity.spec, "mid");
5643        assert_eq!(local.rule, "ry");
5644        assert!(local.segments.is_empty());
5645
5646        let two = RulePath {
5647            segments: vec![
5648                PathSegment {
5649                    uses: "m".into(),
5650                    repository: None,
5651                    spec: "mid".into(),
5652                },
5653                PathSegment {
5654                    uses: "nested".into(),
5655                    repository: None,
5656                    spec: "leaf".into(),
5657                },
5658            ],
5659            rule: "rx".into(),
5660        };
5661        let (body, local) = plan.peel_rule(&two).expect("two hop rule");
5662        assert_eq!(body.identity.spec, "leaf");
5663        assert_eq!(local.rule, "rx");
5664    }
5665
5666    #[test]
5667    fn qualify_data_names_returns_qualified_path_that_peels() {
5668        let (_, plan) = nested_import_plan();
5669        let qualified = plan
5670            .qualify_data_names(&["m", "nested", "x"])
5671            .expect("qualified path");
5672        assert_eq!(qualified.input_key(), "m.nested.x");
5673        assert_eq!(qualified.segments.len(), 2);
5674        assert_eq!(qualified.data, "x");
5675        let (body, _, _) = plan.peel_data(&qualified).expect("qualified path peels");
5676        assert_eq!(body.identity.spec, "leaf");
5677    }
5678}