Skip to main content

lemma/planning/
normalize.rs

1//! Expression normalization: algebraic simplification for drift-free evaluation.
2//!
3//! Internal [`NormalForm`] IR used by planning; sealed into [`ExecutionPlan`] tables.
4
5use crate::computation::rational::{NumericFailure, RationalInteger};
6use crate::formatting::expression::{CommandView, PredicateView, Syntax, SyntaxNode, SyntaxView};
7use crate::parsing::arena::{ExprKind, ExprRef};
8use crate::parsing::ast::{
9    CalendarPeriodUnit, DateCalendarKind, DateRelativeKind, ListPredicateForm, LogicalConnective,
10};
11use crate::planning::execution_plan::ExecutionPlan;
12use crate::planning::instance_list::RowHop;
13use crate::planning::ordered_dispatch::DispatchKey;
14use crate::planning::semantics::{
15    primitive_boolean_arc, primitive_date_arc, primitive_number_arc, primitive_ratio_arc,
16    primitive_text_arc, primitive_time_arc, ArithmeticComputation, ComparisonComputation,
17    DataDefinition, DataPath, Expression, ExpressionKind, LemmaType, ListPredKind, LiteralValue,
18    MathematicalComputation, PathSegment, RangeBound, RoundingMode, RowEnd, RulePath,
19    SemanticConversionTarget, Source, SpecIdentity, ValueKind, VetoExpression,
20};
21use crate::planning::typing::{
22    comparison_type, compute_arithmetic_result_type, date_predicate_type,
23    infer_range_type_from_endpoint_types, logical_connective_type, logical_not_type, math_op_type,
24    past_future_range_type, piecewise_type, quantity_integer_power_type, range_bound_result_type,
25    range_containment_type, result_is_veto_type, step_rounding_type, unit_conversion_type,
26    MeasureScope,
27};
28use crate::Error;
29use indexmap::IndexMap;
30use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
31use std::sync::Arc;
32
33mod rewrite;
34
35fn normalization_error(source: Option<Source>, failure: NumericFailure, context: &str) -> Error {
36    Error::validation(format!("{context}: {failure}"), source, None::<String>)
37}
38
39/// Literal for an exactly folded plain-number rational.
40///
41/// Contract: callers fold plain numbers only. Unit-bearing operands never reach
42/// this function; measure folds carry their unit signature on the result type.
43fn literal_from_folded_rational(rational: RationalInteger) -> LiteralValue {
44    LiteralValue::number(rational)
45}
46
47/// One rule lowered into the shared NormalForm graph and normalized.
48pub(crate) struct NormalizedRule {
49    pub(crate) body: NormalFormId,
50    /// Type of the lowered root before any rewrite: the source expression typed
51    /// by [`Cells::type_of`]. Equals the graph's inferred rule type.
52    pub(crate) source_type: Arc<LemmaType>,
53}
54
55/// Plan-level inputs to per-rule normalization that do not change across the
56/// execution order. Per-rule inputs (branches, source) stay explicit.
57pub(crate) struct NormalizeContext<'a> {
58    pub(crate) data: &'a IndexMap<DataPath, DataDefinition>,
59    /// Template plans of instance-list row specs, by row spec.
60    pub(crate) row_templates: &'a BTreeMap<SpecIdentity, Arc<ExecutionPlan>>,
61    /// The spec being planned: rows of its self-lists read this plan.
62    pub(crate) self_identity: &'a SpecIdentity,
63    /// Present rules in topo order (same set as the plan).
64    pub(crate) rule_order: &'a [RulePath],
65    /// Measure scope cells are typed in: the plan's unit + signature indexes.
66    pub(crate) measure_scope: MeasureScope<'a>,
67    pub(crate) max_normalized_expression_nodes: usize,
68}
69
70/// Completed rule roots, graph rule types, and data for lower.
71struct LowerCtx<'a> {
72    completed_rules: &'a HashMap<RulePath, NormalFormId>,
73    /// [`crate::planning::graph::RuleNode::rule_type`] for every rule (pre-topo).
74    rule_types: &'a HashMap<RulePath, Arc<LemmaType>>,
75    data: &'a IndexMap<DataPath, DataDefinition>,
76    row_templates: &'a BTreeMap<SpecIdentity, Arc<ExecutionPlan>>,
77    self_identity: &'a SpecIdentity,
78    rule_order: &'a [RulePath],
79}
80
81impl LowerCtx<'_> {
82    /// Member `path.name` on rows of `identity`, hop by hop.
83    fn row_member_chain(
84        &self,
85        identity: &SpecIdentity,
86        path: &[String],
87        name: &str,
88    ) -> Option<Vec<RowHop>> {
89        let parts: Vec<&str> = path
90            .iter()
91            .map(String::as_str)
92            .chain(std::iter::once(name))
93            .collect();
94        let rules: BTreeSet<RulePath>;
95        let view = if identity == self.self_identity {
96            rules = self.rule_order.iter().cloned().collect();
97            crate::planning::instance_list::RowEndView::Root {
98                data: self.data,
99                rules: &rules,
100            }
101        } else {
102            crate::planning::instance_list::RowEndView::Plan(self.template(identity))
103        };
104        crate::planning::instance_list::row_end_chain(identity, view, self.row_templates, &parts)
105    }
106
107    fn row_end_type(&self, identity: &SpecIdentity, end: &RowEnd) -> Arc<LemmaType> {
108        if identity != self.self_identity {
109            return crate::planning::instance_list::row_end_type(self.template(identity), end);
110        }
111        match end {
112            RowEnd::Data(path) => crate::planning::instance_list::row_data_type(self.data, path),
113            RowEnd::Rule(rule) => Arc::clone(self.rule_types.get(rule).unwrap_or_else(|| {
114                panic!("BUG: row rule '{}' has no rule type", rule.input_key())
115            })),
116        }
117    }
118
119    fn template(&self, identity: &SpecIdentity) -> &ExecutionPlan {
120        self.row_templates
121            .get(identity)
122            .unwrap_or_else(|| panic!("BUG: row field of '{identity}' without a template plan"))
123    }
124}
125
126pub(crate) fn build_normalized_rule(
127    ctx: &NormalizeContext<'_>,
128    completed_rules: &HashMap<RulePath, NormalFormId>,
129    rule_types: &HashMap<RulePath, Arc<LemmaType>>,
130    expression: &Expression,
131    source: Option<Source>,
132    interner: &mut NormalFormInterner,
133) -> Result<NormalizedRule, Error> {
134    let max_normalized_expression_nodes = ctx.max_normalized_expression_nodes;
135    let lower = LowerCtx {
136        completed_rules,
137        rule_types,
138        data: ctx.data,
139        row_templates: ctx.row_templates,
140        self_identity: ctx.self_identity,
141        rule_order: ctx.rule_order,
142    };
143
144    let mut cells = Cells::new(interner, ctx.measure_scope);
145    let root = to_normal_form(expression, &mut cells, &lower);
146    let source_type = Arc::clone(cells.result_type(root));
147    let nf = rewrite::normalize(root, cells, source.clone())?;
148
149    if normal_form_exceeds_node_budget(&interner.forms, nf, max_normalized_expression_nodes) {
150        return Err(expression_node_limit_error(
151            max_normalized_expression_nodes,
152            source,
153        ));
154    }
155
156    Ok(NormalizedRule {
157        body: nf,
158        source_type,
159    })
160}
161
162fn expression_node_limit_error(limit: usize, source: Option<Source>) -> Error {
163    Error::resource_limit_exceeded(
164        "max_normalized_expression_nodes",
165        format!("{limit} expression nodes"),
166        format!(
167            "more than {limit} unique normal-form cells reachable from the rule root (rule references count as one cell)"
168        ),
169        "Restructure the rule or reduce repeated references to other rules",
170        source,
171        None,
172        None,
173    )
174}
175
176/// Whether the normal form, counted as unique DAG cells reachable from `root`,
177/// exceeds the node budget (IR size = distinct cells, not tree expansion).
178/// Rule references count as one cell: the target body was already budgeted when its
179/// rule completed. Rule references are evaluation boundaries, so this measures only
180/// intra-rule IR size.
181pub(crate) fn normal_form_exceeds_node_budget(
182    forms: &[NormalForm],
183    root: NormalFormId,
184    budget: usize,
185) -> bool {
186    let mut visited = HashSet::new();
187    let mut worklist = vec![root];
188    while let Some(current) = worklist.pop() {
189        if !visited.insert(current) {
190            continue;
191        }
192        if visited.len() > budget {
193            return true;
194        }
195        let cell = forms.get(current.index()).unwrap_or_else(|| {
196            panic!(
197                "BUG: NormalFormId {} out of range during node budget walk (table len {})",
198                current.0,
199                forms.len()
200            )
201        });
202        if cell.rule_ref.is_some() {
203            continue;
204        }
205        cell.kind.for_each_child(|child| worklist.push(child));
206        if let Some(origin) = cell.origin {
207            worklist.push(origin);
208        }
209    }
210    false
211}
212
213/// Maximum nesting depth of a NormalForm DAG (leaves and rule references have
214/// depth 1). Rule references are evaluation boundaries, so planning measures only
215/// intra-rule Kind nesting. Used by tests to check that bound.
216#[cfg(test)]
217pub(crate) fn normal_form_depth(forms: &[NormalForm], root: NormalFormId) -> usize {
218    let mut memo: HashMap<NormalFormId, usize> = HashMap::new();
219    // Explicit post-order: (id, children_pushed). Embeds are leaves (depth 1).
220    let mut stack: Vec<(NormalFormId, bool)> = vec![(root, false)];
221    while let Some((id, children_pushed)) = stack.pop() {
222        if memo.contains_key(&id) {
223            continue;
224        }
225        let cell = forms.get(id.index()).unwrap_or_else(|| {
226            panic!(
227                "BUG: NormalFormId {} out of range during depth walk (table len {})",
228                id.0,
229                forms.len()
230            )
231        });
232        if cell.rule_ref.is_some() {
233            memo.insert(id, 1);
234            continue;
235        }
236        if !children_pushed {
237            stack.push((id, true));
238            cell.kind.for_each_child(|child| {
239                if !memo.contains_key(&child) {
240                    stack.push((child, false));
241                }
242            });
243            if let Some(origin) = cell.origin {
244                if !memo.contains_key(&origin) {
245                    stack.push((origin, false));
246                }
247            }
248            continue;
249        }
250        let mut depth = 1usize;
251        cell.kind.for_each_child(|child| {
252            let child_depth = *memo.get(&child).unwrap_or_else(|| {
253                panic!(
254                    "BUG: child NormalFormId {} missing from depth memo after post-order push",
255                    child.0
256                )
257            });
258            depth = depth.max(1 + child_depth);
259        });
260        if let Some(origin) = cell.origin {
261            let origin_depth = *memo.get(&origin).unwrap_or_else(|| {
262                panic!(
263                    "BUG: origin NormalFormId {} missing from depth memo after post-order push",
264                    origin.0
265                )
266            });
267            depth = depth.max(1 + origin_depth);
268        }
269        memo.insert(id, depth);
270    }
271    *memo
272        .get(&root)
273        .unwrap_or_else(|| panic!("BUG: root NormalFormId {} missing from depth memo", root.0))
274}
275
276/// Leaves in the shipped equation DAG: literals and data only.
277#[derive(Clone, Debug, PartialEq, Eq, Hash)]
278pub enum LeafKind {
279    Literal(LiteralValue),
280    DataPath(crate::planning::semantics::DataPath),
281    /// Value of the current value-list row at `level`.
282    RowValue {
283        level: u32,
284    },
285}
286
287/// A member chain read on a row, through the row spec's template plans: every end
288/// but the last reads a `uses` slot, as that instance; the last is the member read.
289#[derive(Clone, Debug, PartialEq, Eq, Hash)]
290pub struct RowRead {
291    pub ends: Vec<RowEnd>,
292    pub result_type: Arc<LemmaType>,
293}
294
295/// Index into a [`NormalFormInterner`] / shipped [`crate::planning::execution_plan::ExecutionPlan::normal_forms`].
296#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
297pub struct NormalFormId(u32);
298
299impl NormalFormId {
300    pub fn index(self) -> usize {
301        self.0 as usize
302    }
303
304    pub fn from_index(index: usize) -> Self {
305        Self(u32::try_from(index).expect("BUG: normal_forms table exceeds u32::MAX"))
306    }
307}
308
309/// One operand of a [`NormalFormKind::Sum`] or [`NormalFormKind::Product`]. `inverse`
310/// marks a subtracted (Sum) or divided (Product) operand. The first term of a
311/// Sum/Product is never inverse.
312#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
313pub struct ArithmeticTerm {
314    pub operand: NormalFormId,
315    pub inverse: bool,
316}
317
318impl ArithmeticTerm {
319    pub(crate) fn direct(operand: NormalFormId) -> Self {
320        Self {
321            operand,
322            inverse: false,
323        }
324    }
325
326    pub(crate) fn inverted(operand: NormalFormId) -> Self {
327        Self {
328            operand,
329            inverse: true,
330        }
331    }
332}
333
334/// One region body of a [`NormalFormKind::OrderedDispatch`] table.
335#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
336pub enum DispatchRegion {
337    /// The region's value is this origin arm body.
338    Body(NormalFormId),
339    /// A conjunctive nest: a Piecewise over the residual arms of one key. Its arm
340    /// bodies are origin arm bodies, and its winner is decided by the same scan as the
341    /// origin Piecewise.
342    Conjunctive(NormalFormId),
343}
344
345impl DispatchRegion {
346    pub fn cell(self) -> NormalFormId {
347        match self {
348            DispatchRegion::Body(id) | DispatchRegion::Conjunctive(id) => id,
349        }
350    }
351
352    fn map_cell(self, map: &mut impl FnMut(NormalFormId) -> NormalFormId) -> Self {
353        match self {
354            DispatchRegion::Body(id) => DispatchRegion::Body(map(id)),
355            DispatchRegion::Conjunctive(id) => DispatchRegion::Conjunctive(map(id)),
356        }
357    }
358}
359
360/// Algebraic structure only. Children are [`NormalFormId`]. Hash/Eq drive cons —
361/// no source spans or rule-ref identity here (those live on [`NormalForm`]).
362#[derive(Clone, Debug, PartialEq, Eq, Hash)]
363pub enum NormalFormKind {
364    Leaf(LeafKind),
365    Sum(Vec<ArithmeticTerm>),
366    Product(Vec<ArithmeticTerm>),
367    Subtract(NormalFormId, NormalFormId),
368    Divide(NormalFormId, NormalFormId),
369    Power(NormalFormId, NormalFormId),
370    Modulo(NormalFormId, NormalFormId),
371    Negate(NormalFormId),
372    Reciprocal(NormalFormId),
373    Comparison(NormalFormId, ComparisonComputation, NormalFormId),
374    /// Logical operands in source order.
375    Logical(LogicalConnective, Vec<NormalFormId>),
376    Not(NormalFormId),
377    MathOp(MathematicalComputation, NormalFormId),
378    /// `operand -> round` or `operand -> round step`.
379    StepRounding(RoundingMode, NormalFormId, Option<NormalFormId>),
380    UnitConversion(NormalFormId, SemanticConversionTarget),
381    Veto(VetoExpression),
382    DateRelative(DateRelativeKind, NormalFormId),
383    DateCalendar(DateCalendarKind, CalendarPeriodUnit, NormalFormId),
384    RangeLiteral(NormalFormId, NormalFormId),
385    PastFutureRange(DateRelativeKind, NormalFormId),
386    RangeContainment(NormalFormId, NormalFormId),
387    RangeBound(RangeBound, NormalFormId),
388    ResultIsVeto(NormalFormId),
389    Now,
390    Piecewise(Vec<(NormalFormId, NormalFormId)>),
391    /// A [`Self::Piecewise`] over one scrutinee, decided by binary search instead
392    /// of a linear arm scan. Built by [`ordered_dispatch`], which always records
393    /// the pre-image as `origin`, so explanation keeps reading the Piecewise.
394    OrderedDispatch {
395        scrutinee: NormalFormId,
396        /// Sorted, deduplicated, all of one [`DispatchKey`] class.
397        /// Shared so plan extract / rebuild clones are a refcount bump.
398        boundaries: Arc<[DispatchKey]>,
399        /// Exactly [`region_count`] entries; see [`crate::planning::ordered_dispatch`].
400        regions: Vec<DispatchRegion>,
401    },
402    /// Read member `path.name` of the row `base` yields, in that row's context.
403    SpecMember {
404        base: NormalFormId,
405        path: Vec<String>,
406        name: String,
407        row: RowRead,
408    },
409    /// Field `end` of the current row of the instance-list command at `level`
410    /// (0 = outermost), read through the template plan of `identity`.
411    RowField {
412        level: u32,
413        identity: SpecIdentity,
414        end: RowEnd,
415        /// Written list qualifier, preserved for explanation display. `None` for bare refs.
416        qualifier: Option<Vec<String>>,
417    },
418    /// `row_level` is set when argument cells read the current list row at that
419    /// level, through [`LeafKind::RowValue`] or [`Self::RowField`].
420    ListCommand {
421        list: NormalFormId,
422        op: NormalFormListOp,
423        args: Vec<NormalFormId>,
424        row_level: Option<u32>,
425    },
426}
427
428#[derive(Clone, Debug, PartialEq, Eq, Hash)]
429pub enum NormalFormListOp {
430    Find,
431    First,
432    Last,
433    Count,
434    Sum,
435    Avg,
436    Product,
437    Min,
438    Max,
439    /// `args[0]` is the boolean expression evaluated once per row.
440    Predicate {
441        kind: crate::planning::semantics::ListPredKind,
442        form: ListPredicateForm,
443    },
444    Sort {
445        descending: bool,
446    },
447    Top,
448    With,
449    /// Append one row of spec `identity`; `args` are the values of `fields`, in order.
450    WithRow {
451        identity: Arc<SpecIdentity>,
452        fields: Vec<DataPath>,
453    },
454    /// Set field key(s) on a Spec-element list; evaluation returns the list unchanged.
455    Key {
456        fields: Vec<DataPath>,
457    },
458}
459
460impl NormalFormKind {
461    /// Calls `f` on each child cell id in evaluation order. Piecewise yields `cond, body`
462    /// per arm from index 0; OrderedDispatch yields the scrutinee then every region body.
463    pub fn for_each_child(&self, mut f: impl FnMut(NormalFormId)) {
464        match self {
465            NormalFormKind::Leaf(_) | NormalFormKind::Veto(_) | NormalFormKind::Now => {}
466            NormalFormKind::Sum(terms) | NormalFormKind::Product(terms) => {
467                terms.iter().for_each(|term| f(term.operand));
468            }
469            NormalFormKind::Logical(_, children) => children.iter().copied().for_each(f),
470            NormalFormKind::Subtract(a, b)
471            | NormalFormKind::Divide(a, b)
472            | NormalFormKind::Power(a, b)
473            | NormalFormKind::Modulo(a, b)
474            | NormalFormKind::Comparison(a, _, b)
475            | NormalFormKind::RangeLiteral(a, b)
476            | NormalFormKind::RangeContainment(a, b) => {
477                f(*a);
478                f(*b);
479            }
480            NormalFormKind::StepRounding(_, a, step) => {
481                f(*a);
482                if let Some(step) = step {
483                    f(*step);
484                }
485            }
486            NormalFormKind::Negate(x)
487            | NormalFormKind::Reciprocal(x)
488            | NormalFormKind::Not(x)
489            | NormalFormKind::MathOp(_, x)
490            | NormalFormKind::UnitConversion(x, _)
491            | NormalFormKind::DateRelative(_, x)
492            | NormalFormKind::DateCalendar(_, _, x)
493            | NormalFormKind::PastFutureRange(_, x)
494            | NormalFormKind::RangeBound(_, x)
495            | NormalFormKind::ResultIsVeto(x) => f(*x),
496            NormalFormKind::Piecewise(arms) => arms.iter().for_each(|(condition, body)| {
497                f(*condition);
498                f(*body);
499            }),
500            NormalFormKind::OrderedDispatch {
501                scrutinee, regions, ..
502            } => {
503                f(*scrutinee);
504                regions.iter().for_each(|region| f(region.cell()));
505            }
506            NormalFormKind::SpecMember { base, .. } => f(*base),
507            NormalFormKind::RowField { .. } => {}
508            NormalFormKind::ListCommand { list, args, .. } => {
509                f(*list);
510                args.iter().copied().for_each(f);
511            }
512        }
513    }
514
515    /// Child cell ids in [`Self::for_each_child`] order.
516    pub fn children(&self) -> Vec<NormalFormId> {
517        let mut children = Vec::new();
518        self.for_each_child(|child| children.push(child));
519        children
520    }
521
522    /// Same Kind with every child id passed through `map`. Payloads that are not
523    /// child ids (operators, literals, targets, boundaries) are cloned.
524    pub(crate) fn map_children(&self, mut map: impl FnMut(NormalFormId) -> NormalFormId) -> Self {
525        match self {
526            NormalFormKind::Leaf(leaf) => NormalFormKind::Leaf(leaf.clone()),
527            NormalFormKind::Sum(terms) => NormalFormKind::Sum(
528                terms
529                    .iter()
530                    .map(|term| ArithmeticTerm {
531                        operand: map(term.operand),
532                        inverse: term.inverse,
533                    })
534                    .collect(),
535            ),
536            NormalFormKind::Product(terms) => NormalFormKind::Product(
537                terms
538                    .iter()
539                    .map(|term| ArithmeticTerm {
540                        operand: map(term.operand),
541                        inverse: term.inverse,
542                    })
543                    .collect(),
544            ),
545            NormalFormKind::Logical(connective, children) => {
546                NormalFormKind::Logical(*connective, children.iter().copied().map(map).collect())
547            }
548            NormalFormKind::Subtract(a, b) => NormalFormKind::Subtract(map(*a), map(*b)),
549            NormalFormKind::Divide(a, b) => NormalFormKind::Divide(map(*a), map(*b)),
550            NormalFormKind::Power(a, b) => NormalFormKind::Power(map(*a), map(*b)),
551            NormalFormKind::Modulo(a, b) => NormalFormKind::Modulo(map(*a), map(*b)),
552            NormalFormKind::Comparison(a, op, b) => {
553                NormalFormKind::Comparison(map(*a), op.clone(), map(*b))
554            }
555            NormalFormKind::RangeLiteral(a, b) => NormalFormKind::RangeLiteral(map(*a), map(*b)),
556            NormalFormKind::RangeContainment(a, b) => {
557                NormalFormKind::RangeContainment(map(*a), map(*b))
558            }
559            NormalFormKind::Negate(x) => NormalFormKind::Negate(map(*x)),
560            NormalFormKind::Reciprocal(x) => NormalFormKind::Reciprocal(map(*x)),
561            NormalFormKind::Not(x) => NormalFormKind::Not(map(*x)),
562            NormalFormKind::MathOp(op, x) => NormalFormKind::MathOp(op.clone(), map(*x)),
563            NormalFormKind::StepRounding(mode, x, step) => {
564                NormalFormKind::StepRounding(*mode, map(*x), step.map(map))
565            }
566            NormalFormKind::UnitConversion(x, target) => {
567                NormalFormKind::UnitConversion(map(*x), target.clone())
568            }
569            NormalFormKind::Veto(v) => NormalFormKind::Veto(v.clone()),
570            NormalFormKind::DateRelative(kind, x) => NormalFormKind::DateRelative(*kind, map(*x)),
571            NormalFormKind::DateCalendar(kind, unit, x) => {
572                NormalFormKind::DateCalendar(*kind, *unit, map(*x))
573            }
574            NormalFormKind::PastFutureRange(kind, x) => {
575                NormalFormKind::PastFutureRange(*kind, map(*x))
576            }
577            NormalFormKind::RangeBound(bound, x) => NormalFormKind::RangeBound(*bound, map(*x)),
578            NormalFormKind::ResultIsVeto(x) => NormalFormKind::ResultIsVeto(map(*x)),
579            NormalFormKind::Now => NormalFormKind::Now,
580            NormalFormKind::Piecewise(arms) => NormalFormKind::Piecewise(
581                arms.iter()
582                    .map(|(condition, body)| (map(*condition), map(*body)))
583                    .collect(),
584            ),
585            NormalFormKind::OrderedDispatch {
586                scrutinee,
587                boundaries,
588                regions,
589            } => NormalFormKind::OrderedDispatch {
590                scrutinee: map(*scrutinee),
591                boundaries: boundaries.clone(),
592                regions: regions
593                    .iter()
594                    .map(|region| region.map_cell(&mut map))
595                    .collect(),
596            },
597            NormalFormKind::SpecMember {
598                base,
599                path,
600                name,
601                row,
602            } => NormalFormKind::SpecMember {
603                base: map(*base),
604                path: path.clone(),
605                name: name.clone(),
606                row: row.clone(),
607            },
608            NormalFormKind::RowField { .. } => self.clone(),
609            NormalFormKind::ListCommand {
610                list,
611                op,
612                args,
613                row_level,
614            } => NormalFormKind::ListCommand {
615                list: map(*list),
616                op: op.clone(),
617                args: args.iter().copied().map(map).collect(),
618                row_level: *row_level,
619            },
620        }
621    }
622}
623
624/// One node in THE DAG: algebraic Kind, stamped result type, optional conversion
625/// span, optional fold origin, optional rule use-site identity.
626///
627/// A rule use-site shares the completed body's Kind (same child ids) and sets
628/// `rule_ref`. Type is the graph rule type (`RulePath`) or data resolved type
629/// (rule-target DataPath) — not the post-rewrite body cell type. Cons keys on
630/// `(kind, rule_ref, result_type)` so a rule reference never collapses into the
631/// bare body cell, and DataPath leaves that differ only in resolved type stay
632/// distinct.
633#[derive(Clone, Debug)]
634pub struct NormalForm {
635    pub kind: NormalFormKind,
636    /// Type of the value this cell evaluates to. Stamped at planning.
637    pub result_type: Arc<LemmaType>,
638    /// For Sum/Product: type after folding `children[..=k]`. Empty for other kinds.
639    /// `fold_types.last()` equals `result_type` for n-ary arithmetic.
640    pub fold_types: Vec<Arc<LemmaType>>,
641    /// Span for unit-conversion nodes; does not affect sharing.
642    pub source: Option<Source>,
643    /// Pre-image destroyed by a fold; blocks sharing when present.
644    pub origin: Option<NormalFormId>,
645    /// Use-site of a named rule (`None` on ordinary cells).
646    pub rule_ref: Option<RulePath>,
647}
648
649/// Result of [`Cells::type_of`]: stamped result type plus progressive fold types
650/// for n-ary Sum/Product (empty for every other kind).
651pub(crate) struct Typed {
652    pub(crate) result_type: Arc<LemmaType>,
653    pub(crate) fold_types: Vec<Arc<LemmaType>>,
654}
655
656/// Cons key: algebraic Kind, optional rule-ref identity, and stamped result
657/// type. Type is part of the key so a shared interner across temporal slices
658/// never collapses DataPath leaves that differ only in resolved type (Literal
659/// leaves already distinguish via payload identity inside Kind; range endpoints
660/// carry [`crate::planning::semantics::TypedLiteral`] types on [`crate::planning::semantics::ValueKind::Range`]).
661type ConsKey = (NormalFormKind, Option<RulePath>, Arc<LemmaType>);
662
663/// Build-local table + origin-free cons. Ship via [`Self::extract_reachable`].
664pub(crate) struct NormalFormInterner {
665    forms: Vec<NormalForm>,
666    cons: HashMap<ConsKey, NormalFormId>,
667}
668
669impl NormalFormInterner {
670    pub(crate) fn new() -> Self {
671        Self {
672            forms: Vec::new(),
673            cons: HashMap::new(),
674        }
675    }
676
677    pub(crate) fn intern(
678        &mut self,
679        kind: NormalFormKind,
680        source: Option<Source>,
681        origin: Option<NormalFormId>,
682        rule_ref: Option<RulePath>,
683        typed: Typed,
684    ) -> NormalFormId {
685        let Typed {
686            result_type,
687            fold_types,
688        } = typed;
689        let key = (kind, rule_ref, result_type);
690        if origin.is_none() {
691            if let Some(id) = self.cons.get(&key) {
692                return *id;
693            }
694        }
695        let id = NormalFormId(
696            u32::try_from(self.forms.len()).expect("BUG: normal_forms table exceeds u32::MAX"),
697        );
698        if origin.is_none() {
699            self.cons.insert(key.clone(), id);
700        }
701        self.forms.push(NormalForm {
702            kind: key.0,
703            result_type: key.2,
704            fold_types,
705            source,
706            origin,
707            rule_ref: key.1,
708        });
709        id
710    }
711
712    pub(crate) fn get(&self, id: NormalFormId) -> &NormalForm {
713        self.forms.get(id.index()).unwrap_or_else(|| {
714            panic!(
715                "BUG: NormalFormId {} out of range (table len {})",
716                id.0,
717                self.forms.len()
718            )
719        })
720    }
721
722    pub(crate) fn result_type(&self, id: NormalFormId) -> &Arc<LemmaType> {
723        &self.get(id).result_type
724    }
725
726    /// Number of cells interned so far; ids at or past it do not exist yet.
727    pub(crate) fn len(&self) -> usize {
728        self.forms.len()
729    }
730
731    /// Ship only cells reachable from `roots`. Dense-remap ids in ascending
732    /// old-index order. Walks shape children and origin. Remaps both.
733    ///
734    /// Keeps the interner so one planning pass can share it across every
735    /// temporal slice of a spec set.
736    pub(crate) fn extract_reachable(
737        &self,
738        roots: &[NormalFormId],
739    ) -> (Vec<NormalForm>, Vec<NormalFormId>) {
740        let mut reachable: HashSet<u32> = HashSet::new();
741        let mut worklist: Vec<NormalFormId> = roots.to_vec();
742        while let Some(id) = worklist.pop() {
743            if id.index() >= self.forms.len() {
744                panic!(
745                    "BUG: NormalFormId {} out of range during reachable extract (table len {})",
746                    id.0,
747                    self.forms.len()
748                );
749            }
750            if !reachable.insert(id.0) {
751                continue;
752            }
753            let cell = &self.forms[id.index()];
754            cell.kind.for_each_child(|child| worklist.push(child));
755            if let Some(origin) = cell.origin {
756                worklist.push(origin);
757            }
758        }
759        let mut old_ids: Vec<u32> = reachable.into_iter().collect();
760        old_ids.sort_unstable();
761        let mut remap: HashMap<u32, NormalFormId> = HashMap::with_capacity(old_ids.len());
762        for (new_index, old) in old_ids.iter().copied().enumerate() {
763            remap.insert(
764                old,
765                NormalFormId(
766                    u32::try_from(new_index).expect("BUG: normal_forms table exceeds u32::MAX"),
767                ),
768            );
769        }
770        let mut table = Vec::with_capacity(old_ids.len());
771        for old in &old_ids {
772            let cell = &self.forms[*old as usize];
773            table.push(NormalForm {
774                kind: cell.kind.map_children(|child| {
775                    *remap.get(&child.0).unwrap_or_else(|| {
776                        panic!("BUG: child NormalFormId {} missing from remap", child.0)
777                    })
778                }),
779                result_type: Arc::clone(&cell.result_type),
780                fold_types: cell.fold_types.clone(),
781                source: cell.source.clone(),
782                origin: cell.origin.map(|o| {
783                    *remap.get(&o.0).unwrap_or_else(|| {
784                        panic!("BUG: origin NormalFormId {} missing from remap", o.0)
785                    })
786                }),
787                rule_ref: cell.rule_ref.clone(),
788            });
789        }
790        let remapped_roots = roots
791            .iter()
792            .map(|r| {
793                *remap.get(&r.0).unwrap_or_else(|| {
794                    panic!("BUG: root NormalFormId {} not in reachable set", r.0)
795                })
796            })
797            .collect();
798        (table, remapped_roots)
799    }
800}
801
802/// Result type of a data leaf, from its definition in the flattened data map.
803/// Written units from the stored value are cleared: a path expression does not
804/// name a unit (same invariant as graph expression inference).
805pub(crate) fn data_path_result_type(
806    data: &IndexMap<DataPath, DataDefinition>,
807    path: &DataPath,
808) -> Arc<LemmaType> {
809    match data.get(path) {
810        Some(DataDefinition::Value { resolved_type, .. })
811        | Some(DataDefinition::TypeDeclaration { resolved_type, .. })
812        | Some(DataDefinition::Reference { resolved_type, .. })
813        | Some(DataDefinition::InstanceList { resolved_type, .. }) => {
814            Arc::new(resolved_type.as_ref().without_written_unit())
815        }
816        Some(DataDefinition::Import { row, .. }) => Arc::new(LemmaType::primitive(
817            crate::planning::semantics::TypeSpecification::Spec {
818                spec: row.identity.as_ref().clone(),
819            },
820        )),
821        // Absent: normalize unit tests with an empty data scope, as graph
822        // `infer_data_type`.
823        None => Arc::new(LemmaType::undetermined_type()),
824    }
825}
826
827/// The interner together with the measure scope every new cell is typed in.
828/// Lowering and every rewrite pass build cells through this, so a cell's type
829/// is fixed once, at creation, by [`Cells::type_of`].
830pub(crate) struct Cells<'a> {
831    interner: &'a mut NormalFormInterner,
832    measure_scope: MeasureScope<'a>,
833}
834
835/// Owned row-field end plus its optional written list qualifier
836/// (`children` in `children.is_leaf`). Bundled so [`Cells::intern_row_field`]
837/// stays under the argument-count lint.
838pub(crate) struct RowFieldEnd {
839    pub(crate) end: RowEnd,
840    pub(crate) qualifier: Option<Vec<String>>,
841}
842
843impl<'a> Cells<'a> {
844    pub(crate) fn new(
845        interner: &'a mut NormalFormInterner,
846        measure_scope: MeasureScope<'a>,
847    ) -> Self {
848        Self {
849            interner,
850            measure_scope,
851        }
852    }
853
854    pub(crate) fn get(&self, id: NormalFormId) -> &NormalForm {
855        self.interner.get(id)
856    }
857
858    pub(crate) fn result_type(&self, id: NormalFormId) -> &Arc<LemmaType> {
859        self.interner.result_type(id)
860    }
861
862    /// Number of cells interned so far; ids at or past it do not exist yet.
863    pub(crate) fn len(&self) -> usize {
864        self.interner.len()
865    }
866
867    pub(crate) fn intern_empty(&mut self, kind: NormalFormKind) -> NormalFormId {
868        let typed = self.type_of(&kind, None);
869        self.interner.intern(kind, None, None, None, typed)
870    }
871
872    pub(crate) fn intern_literal_leaf(
873        &mut self,
874        literal: LiteralValue,
875        result_type: Arc<LemmaType>,
876    ) -> NormalFormId {
877        self.interner.intern(
878            NormalFormKind::Leaf(LeafKind::Literal(literal)),
879            None,
880            None,
881            None,
882            Typed {
883                result_type,
884                fold_types: Vec::new(),
885            },
886        )
887    }
888
889    /// Row field cell, typed from the row spec (the type is the same on every row).
890    fn intern_row_field(
891        &mut self,
892        level: u32,
893        identity: SpecIdentity,
894        row_end: RowFieldEnd,
895        result_type: Arc<LemmaType>,
896    ) -> NormalFormId {
897        let RowFieldEnd { end, qualifier } = row_end;
898        self.interner.intern(
899            NormalFormKind::RowField {
900                level,
901                identity,
902                end,
903                qualifier,
904            },
905            None,
906            None,
907            None,
908            Typed {
909                result_type,
910                fold_types: Vec::new(),
911            },
912        )
913    }
914
915    fn intern_row_value_leaf(&mut self, level: u32, result_type: Arc<LemmaType>) -> NormalFormId {
916        self.interner.intern(
917            NormalFormKind::Leaf(LeafKind::RowValue { level }),
918            None,
919            None,
920            None,
921            Typed {
922                result_type,
923                fold_types: Vec::new(),
924            },
925        )
926    }
927
928    fn intern_data_leaf(&mut self, path: DataPath, result_type: Arc<LemmaType>) -> NormalFormId {
929        self.interner.intern(
930            NormalFormKind::Leaf(LeafKind::DataPath(path)),
931            None,
932            None,
933            None,
934            Typed {
935                result_type,
936                fold_types: Vec::new(),
937            },
938        )
939    }
940
941    /// Use-site of a named rule: share the completed body's Kind, set rule_ref,
942    /// stamp the caller-supplied type (graph rule type or data resolved type).
943    /// Empty fold_types: eval never folds through a rule_ref cut.
944    fn intern_rule_ref(
945        &mut self,
946        body: NormalFormId,
947        rule_path: RulePath,
948        result_type: Arc<LemmaType>,
949    ) -> NormalFormId {
950        let kind = self.interner.get(body).kind.clone();
951        self.interner.intern(
952            kind,
953            None,
954            None,
955            Some(rule_path),
956            Typed {
957                result_type,
958                fold_types: Vec::new(),
959            },
960        )
961    }
962
963    pub(crate) fn rebuild(
964        &mut self,
965        kind: NormalFormKind,
966        source: Option<Source>,
967        origin: Option<NormalFormId>,
968    ) -> NormalFormId {
969        let typed = self.type_of(&kind, origin);
970        self.interner.intern(kind, source, origin, None, typed)
971    }
972
973    pub(crate) fn fold_into(
974        &mut self,
975        kind: NormalFormKind,
976        destroyed_id: NormalFormId,
977    ) -> NormalFormId {
978        let typed = self.type_of(&kind, Some(destroyed_id));
979        self.interner
980            .intern(kind, None, Some(destroyed_id), None, typed)
981    }
982
983    pub(crate) fn fold_into_survivor(
984        &mut self,
985        survivor_id: NormalFormId,
986        destroyed_id: NormalFormId,
987    ) -> NormalFormId {
988        let survivor = self.interner.get(survivor_id);
989        // Identity-elim unwrap onto a node that already records a fold must not
990        // erase that fold (e.g. `(exp(log(5))) + 0` → keep exp/log origin).
991        if survivor.origin.is_some() {
992            return survivor_id;
993        }
994        self.interner.intern(
995            survivor.kind.clone(),
996            survivor.source.clone(),
997            Some(destroyed_id),
998            survivor.rule_ref.clone(),
999            Typed {
1000                result_type: Arc::clone(&survivor.result_type),
1001                fold_types: survivor.fold_types.clone(),
1002            },
1003        )
1004    }
1005
1006    /// Always re-intern survivor Kind with `origin = destroyed`, even when the
1007    /// survivor already records an origin (Piecewise collapse must keep the
1008    /// recorded arms as the explanation pre-image).
1009    pub(crate) fn replace_with_survivor(
1010        &mut self,
1011        survivor_id: NormalFormId,
1012        destroyed_id: NormalFormId,
1013    ) -> NormalFormId {
1014        let survivor = self.interner.get(survivor_id);
1015        self.interner.intern(
1016            survivor.kind.clone(),
1017            survivor.source.clone(),
1018            Some(destroyed_id),
1019            survivor.rule_ref.clone(),
1020            Typed {
1021                result_type: Arc::clone(&survivor.result_type),
1022                fold_types: survivor.fold_types.clone(),
1023            },
1024        )
1025    }
1026
1027    fn fold_arithmetic_child_types(
1028        &self,
1029        terms: &[ArithmeticTerm],
1030        direct: ArithmeticComputation,
1031        inverted: ArithmeticComputation,
1032    ) -> Vec<Arc<LemmaType>> {
1033        let (first, rest) = terms
1034            .split_first()
1035            .expect("BUG: arithmetic NormalForm with zero children");
1036        assert!(
1037            !first.inverse,
1038            "BUG: arithmetic NormalForm starts with an inverse term"
1039        );
1040        let mut fold_types = Vec::with_capacity(terms.len());
1041        let mut acc = Arc::clone(self.result_type(first.operand));
1042        fold_types.push(Arc::clone(&acc));
1043        for term in rest {
1044            let op = if term.inverse { &inverted } else { &direct };
1045            acc = compute_arithmetic_result_type(
1046                acc,
1047                op,
1048                Arc::clone(self.result_type(term.operand)),
1049                &self.measure_scope,
1050            );
1051            fold_types.push(Arc::clone(&acc));
1052        }
1053        fold_types
1054    }
1055
1056    fn binary_arithmetic_type(
1057        &self,
1058        left: NormalFormId,
1059        op: ArithmeticComputation,
1060        right: NormalFormId,
1061    ) -> Arc<LemmaType> {
1062        let left_type = self.result_type(left);
1063        if op == ArithmeticComputation::Power && left_type.is_measure() {
1064            if let Some(exponent) = self.integer_exponent_leaf(right) {
1065                return quantity_integer_power_type(left_type, exponent, &self.measure_scope);
1066            }
1067        }
1068        compute_arithmetic_result_type(
1069            Arc::clone(left_type),
1070            &op,
1071            Arc::clone(self.result_type(right)),
1072            &self.measure_scope,
1073        )
1074    }
1075
1076    fn integer_exponent_leaf(&self, id: NormalFormId) -> Option<i32> {
1077        let NormalFormKind::Leaf(LeafKind::Literal(literal)) = &self.get(id).kind else {
1078            return None;
1079        };
1080        let ValueKind::Number(number) = &literal.value else {
1081            return None;
1082        };
1083        if !number.is_integer() {
1084            return None;
1085        }
1086        number.numer_to_i32()
1087    }
1088}
1089
1090/// Default primitive type for a folded/constructed literal when no richer type is available.
1091/// Named measure types must be stamped explicitly via [`Cells::fold_into`] /
1092/// [`Cells::intern_literal_leaf`].
1093fn literal_value_default_result_type(value: &ValueKind) -> Arc<LemmaType> {
1094    match value {
1095        ValueKind::Number(_) => primitive_number_arc().clone(),
1096        ValueKind::Boolean(_) => primitive_boolean_arc().clone(),
1097        ValueKind::Text(_) => primitive_text_arc().clone(),
1098        ValueKind::Date(_) => primitive_date_arc().clone(),
1099        ValueKind::Time(_) => primitive_time_arc().clone(),
1100        ValueKind::Ratio(_) => primitive_ratio_arc().clone(),
1101        ValueKind::Measure(_) | ValueKind::UnscaledMeasure { .. } => {
1102            panic!(
1103                "BUG: Measure literal result_type must be stamped from expression/fold survivor type"
1104            )
1105        }
1106        ValueKind::Range(_, _) => {
1107            panic!("BUG: Range literal result_type must be stamped from expression/endpoint types")
1108        }
1109        ValueKind::Spec(_) => {
1110            panic!("BUG: Spec literal result_type must be stamped from the spec identity")
1111        }
1112        ValueKind::List(_) => {
1113            panic!("BUG: List literal result_type must be stamped from the list type")
1114        }
1115    }
1116}
1117
1118impl Cells<'_> {
1119    /// Type of a cell from its children's stamped types. Same rules as
1120    /// `infer_expression_type`, so the lowered root of a rule has the rule's
1121    /// inferred type (asserted when the plan is built). Rewrites preserve the
1122    /// value type when arms were `same_value_type` (validation invariant): base
1123    /// kind and measure dimensions stay; a named type may swap for its
1124    /// primitive or back (`x + 0` → `x`, a collapsed `unless`).
1125    ///
1126    /// `pre_image` is the cell this one replaces (a fold's destroyed cell, a
1127    /// rebuild's origin). Two kinds carry no typing information of their own
1128    /// and keep the pre-image's type: a folded literal (the fold survivor's
1129    /// measure family lives only on `result_type`) and an OrderedDispatch (its
1130    /// regions are in key order, not the source order the rule type follows).
1131    fn type_of(&self, kind: &NormalFormKind, pre_image: Option<NormalFormId>) -> Typed {
1132        let (result_type, fold_types) = match kind {
1133            NormalFormKind::Leaf(LeafKind::Literal(literal)) => (
1134                match pre_image {
1135                    Some(destroyed) => Arc::clone(self.result_type(destroyed)),
1136                    None => literal_value_default_result_type(&literal.value),
1137                },
1138                Vec::new(),
1139            ),
1140            NormalFormKind::Leaf(LeafKind::DataPath(_)) => {
1141                panic!("BUG: DataPath leaf result_type must be stamped from plan.data")
1142            }
1143            NormalFormKind::RowField { .. } => {
1144                panic!("BUG: row field result_type must be stamped from the row spec")
1145            }
1146            NormalFormKind::Leaf(LeafKind::RowValue { .. }) => {
1147                panic!("BUG: row value leaf result_type must be stamped from its list")
1148            }
1149            NormalFormKind::Now => (primitive_date_arc().clone(), Vec::new()),
1150            NormalFormKind::SpecMember { row, .. } => (Arc::clone(&row.result_type), Vec::new()),
1151            NormalFormKind::ListCommand { list, op, args, .. } => (
1152                list_command_result_type(
1153                    self.result_type(*list),
1154                    op,
1155                    args.first().map(|argument| self.result_type(*argument)),
1156                    &self.measure_scope,
1157                ),
1158                Vec::new(),
1159            ),
1160            NormalFormKind::Veto(_) => (Arc::new(LemmaType::veto_type()), Vec::new()),
1161            NormalFormKind::Sum(terms) => {
1162                let fold_types = self.fold_arithmetic_child_types(
1163                    terms,
1164                    ArithmeticComputation::Add,
1165                    ArithmeticComputation::Subtract,
1166                );
1167                let result_type = Arc::clone(
1168                    fold_types
1169                        .last()
1170                        .expect("BUG: Sum fold_types empty after fold_arithmetic_child_types"),
1171                );
1172                (result_type, fold_types)
1173            }
1174            NormalFormKind::Product(terms) => {
1175                let fold_types = self.fold_arithmetic_child_types(
1176                    terms,
1177                    ArithmeticComputation::Multiply,
1178                    ArithmeticComputation::Divide,
1179                );
1180                let result_type = Arc::clone(
1181                    fold_types
1182                        .last()
1183                        .expect("BUG: Product fold_types empty after fold_arithmetic_child_types"),
1184                );
1185                (result_type, fold_types)
1186            }
1187            NormalFormKind::Subtract(a, b) => (
1188                self.binary_arithmetic_type(*a, ArithmeticComputation::Subtract, *b),
1189                Vec::new(),
1190            ),
1191            NormalFormKind::Divide(a, b) => (
1192                self.binary_arithmetic_type(*a, ArithmeticComputation::Divide, *b),
1193                Vec::new(),
1194            ),
1195            NormalFormKind::Power(a, b) => (
1196                self.binary_arithmetic_type(*a, ArithmeticComputation::Power, *b),
1197                Vec::new(),
1198            ),
1199            NormalFormKind::Modulo(a, b) => (
1200                self.binary_arithmetic_type(*a, ArithmeticComputation::Modulo, *b),
1201                Vec::new(),
1202            ),
1203            // `-x` is `-1 * x`; `1/x` is `1 / x`. Typed by the kernel like the
1204            // source arithmetic they were rewritten from.
1205            NormalFormKind::Negate(x) => (
1206                compute_arithmetic_result_type(
1207                    primitive_number_arc().clone(),
1208                    &ArithmeticComputation::Multiply,
1209                    Arc::clone(self.result_type(*x)),
1210                    &self.measure_scope,
1211                ),
1212                Vec::new(),
1213            ),
1214            NormalFormKind::Reciprocal(x) => (
1215                compute_arithmetic_result_type(
1216                    primitive_number_arc().clone(),
1217                    &ArithmeticComputation::Divide,
1218                    Arc::clone(self.result_type(*x)),
1219                    &self.measure_scope,
1220                ),
1221                Vec::new(),
1222            ),
1223            NormalFormKind::Logical(_, children) => {
1224                let mut types = children.iter().map(|id| self.result_type(*id));
1225                let first = types.next().expect("BUG: And has at least two operands");
1226                let ty = types.fold(Arc::clone(first), |acc, next| {
1227                    logical_connective_type(&acc, next)
1228                });
1229                (ty, Vec::new())
1230            }
1231            NormalFormKind::Comparison(a, op, b) => (
1232                comparison_type(self.result_type(*a), op, self.result_type(*b)),
1233                Vec::new(),
1234            ),
1235            NormalFormKind::RangeContainment(a, b) => (
1236                range_containment_type(self.result_type(*a), self.result_type(*b)),
1237                Vec::new(),
1238            ),
1239            NormalFormKind::Not(x) => (logical_not_type(self.result_type(*x)), Vec::new()),
1240            NormalFormKind::DateRelative(_, x) | NormalFormKind::DateCalendar(_, _, x) => {
1241                (date_predicate_type(self.result_type(*x)), Vec::new())
1242            }
1243            NormalFormKind::ResultIsVeto(_) => (result_is_veto_type(), Vec::new()),
1244            NormalFormKind::MathOp(op, x) => (
1245                math_op_type(op, self.result_type(*x), &self.measure_scope),
1246                Vec::new(),
1247            ),
1248            NormalFormKind::StepRounding(_, x, step) => (
1249                step_rounding_type(
1250                    self.result_type(*x),
1251                    step.map(|step| self.result_type(step)),
1252                ),
1253                Vec::new(),
1254            ),
1255            NormalFormKind::UnitConversion(x, target) => (
1256                unit_conversion_type(self.result_type(*x), target),
1257                Vec::new(),
1258            ),
1259            NormalFormKind::RangeLiteral(a, b) => {
1260                let (a, b) = (self.result_type(*a), self.result_type(*b));
1261                let ty = if a.vetoed() || b.vetoed() {
1262                    Arc::new(LemmaType::veto_type())
1263                } else {
1264                    infer_range_type_from_endpoint_types(a.as_ref(), b.as_ref())
1265                };
1266                (ty, Vec::new())
1267            }
1268            NormalFormKind::PastFutureRange(_, offset) => (
1269                past_future_range_type(self.result_type(*offset)),
1270                Vec::new(),
1271            ),
1272            NormalFormKind::RangeBound(_, operand) => (
1273                range_bound_result_type(self.result_type(*operand), &self.measure_scope),
1274                Vec::new(),
1275            ),
1276            NormalFormKind::Piecewise(arms) => (
1277                piecewise_type(
1278                    arms.iter()
1279                        .map(|(_, body)| Arc::clone(self.result_type(*body))),
1280                    &self.measure_scope,
1281                ),
1282                Vec::new(),
1283            ),
1284            NormalFormKind::OrderedDispatch { .. } => (
1285                Arc::clone(self.result_type(pre_image.expect(
1286                    "BUG: OrderedDispatch is only built over the Piecewise it re-indexes",
1287                ))),
1288                Vec::new(),
1289            ),
1290        };
1291        // After planning validation every operand is determined, so an undetermined
1292        // stamp here is a validation gap. Unit tests may still lower untyped paths;
1293        // undetermined is then only legal when an operand already was.
1294        if result_type.is_undetermined() {
1295            let input_undetermined = match kind {
1296                NormalFormKind::Leaf(_) | NormalFormKind::Now | NormalFormKind::Veto(_) => false,
1297                NormalFormKind::OrderedDispatch { .. } => {
1298                    pre_image.is_some_and(|id| self.result_type(id).is_undetermined())
1299                }
1300                NormalFormKind::Sum(terms) | NormalFormKind::Product(terms) => terms
1301                    .iter()
1302                    .any(|term| self.result_type(term.operand).is_undetermined()),
1303                NormalFormKind::Logical(_, children) => children
1304                    .iter()
1305                    .any(|id| self.result_type(*id).is_undetermined()),
1306                NormalFormKind::Subtract(a, b)
1307                | NormalFormKind::Divide(a, b)
1308                | NormalFormKind::Power(a, b)
1309                | NormalFormKind::Modulo(a, b)
1310                | NormalFormKind::RangeLiteral(a, b) => {
1311                    self.result_type(*a).is_undetermined() || self.result_type(*b).is_undetermined()
1312                }
1313                NormalFormKind::StepRounding(_, a, step) => {
1314                    self.result_type(*a).is_undetermined()
1315                        || step.is_some_and(|step| self.result_type(step).is_undetermined())
1316                }
1317                // Infer returns Undetermined for invalid operand pairs; validation
1318                // rejects those before a plan is built. Normalize unit tests may
1319                // lower without validation, so determined operands → Undetermined
1320                // is allowed here.
1321                NormalFormKind::Comparison(_, _, _) | NormalFormKind::RangeContainment(_, _) => {
1322                    true
1323                }
1324                NormalFormKind::Negate(x)
1325                | NormalFormKind::Reciprocal(x)
1326                | NormalFormKind::UnitConversion(x, _)
1327                | NormalFormKind::RangeBound(_, x)
1328                | NormalFormKind::ResultIsVeto(x) => self.result_type(*x).is_undetermined(),
1329                NormalFormKind::Not(_)
1330                | NormalFormKind::DateRelative(_, _)
1331                | NormalFormKind::DateCalendar(_, _, _)
1332                | NormalFormKind::MathOp(_, _)
1333                | NormalFormKind::PastFutureRange(_, _) => true,
1334                NormalFormKind::Piecewise(arms) => arms.iter().any(|(condition, body)| {
1335                    self.result_type(*condition).is_undetermined()
1336                        || self.result_type(*body).is_undetermined()
1337                }),
1338                NormalFormKind::SpecMember { base, .. } => {
1339                    self.result_type(*base).is_undetermined()
1340                }
1341                NormalFormKind::RowField { .. } => false,
1342                NormalFormKind::ListCommand { list, args, .. } => {
1343                    self.result_type(*list).is_undetermined()
1344                        || args
1345                            .iter()
1346                            .any(|id| self.result_type(*id).is_undetermined())
1347                }
1348            };
1349            assert!(
1350                input_undetermined,
1351                "BUG: NormalForm cell stamped undetermined type after validation"
1352            );
1353        }
1354        Typed {
1355            result_type,
1356            fold_types,
1357        }
1358    }
1359}
1360
1361impl LowerCtx<'_> {
1362    /// Root of an already planned rule. Every rule a cell reads precedes it in topo order.
1363    fn completed_rule(&self, path: &RulePath) -> NormalFormId {
1364        *self.completed_rules.get(path).unwrap_or_else(|| {
1365            panic!(
1366                "BUG: rule '{}' read before it was planned (missing dependency edge)",
1367                path.input_key()
1368            )
1369        })
1370    }
1371
1372    fn rule_type(&self, path: &RulePath) -> Arc<LemmaType> {
1373        Arc::clone(self.rule_types.get(path).unwrap_or_else(|| {
1374            panic!("BUG: rule '{}' has no RuleNode.rule_type", path.input_key())
1375        }))
1376    }
1377}
1378
1379/// Lower the tree into `cells`, children before parents.
1380fn to_normal_form(tree: &Expression, cells: &mut Cells<'_>, lower: &LowerCtx<'_>) -> NormalFormId {
1381    let mut lowered: Vec<NormalFormId> = Vec::with_capacity(tree.table.len());
1382    let mut children: Vec<NormalFormId> = Vec::new();
1383    for id in tree.table.ids() {
1384        let node = tree.at(id);
1385        children.clear();
1386        node.kind()
1387            .for_each_child(|child| children.push(lowered[child.index()]));
1388        lowered.push(lower_node(node, &children, &lowered, cells, lower));
1389    }
1390    *lowered.last().expect("BUG: expression tree has a root")
1391}
1392
1393/// Operator group whose consecutive chain steps lower to one n-ary cell.
1394#[derive(Clone, Copy, PartialEq, Eq)]
1395enum ArithmeticGroup {
1396    Sum,
1397    Product,
1398}
1399
1400fn arithmetic_group(op: &ArithmeticComputation) -> Option<ArithmeticGroup> {
1401    match op {
1402        ArithmeticComputation::Add | ArithmeticComputation::Subtract => Some(ArithmeticGroup::Sum),
1403        ArithmeticComputation::Multiply | ArithmeticComputation::Divide => {
1404            Some(ArithmeticGroup::Product)
1405        }
1406        ArithmeticComputation::Power | ArithmeticComputation::Modulo => None,
1407    }
1408}
1409
1410/// Lower the left fold of `first` over `steps` whose operands are lowered.
1411///
1412/// Each run of consecutive steps in one group (`a + b - c`, `a * b / c`) interns one
1413/// n-ary cell. Subtract and divide steps become negated and reciprocal operands only
1414/// when every operand of the run is numeric, as normalization expands them; otherwise
1415/// the run is lowered one binary step at a time.
1416fn lower_arithmetic_chain(
1417    first: NormalFormId,
1418    steps: &[(ArithmeticComputation, NormalFormId)],
1419    cells: &mut Cells<'_>,
1420) -> NormalFormId {
1421    let mut current = first;
1422    let mut rest = steps;
1423    while let Some((op, operand)) = rest.first() {
1424        let Some(group) = arithmetic_group(op) else {
1425            current = lower_binary_arithmetic(op, current, *operand, cells);
1426            rest = &rest[1..];
1427            continue;
1428        };
1429        let run_len = rest
1430            .iter()
1431            .take_while(|(op, _)| arithmetic_group(op) == Some(group))
1432            .count();
1433        current = lower_arithmetic_run(current, &rest[..run_len], group, cells);
1434        rest = &rest[run_len..];
1435    }
1436    current
1437}
1438
1439fn lower_arithmetic_run(
1440    first: NormalFormId,
1441    operands: &[(ArithmeticComputation, NormalFormId)],
1442    group: ArithmeticGroup,
1443    cells: &mut Cells<'_>,
1444) -> NormalFormId {
1445    let has_subtract_or_divide = operands.iter().any(|(op, _)| {
1446        matches!(
1447            op,
1448            ArithmeticComputation::Subtract | ArithmeticComputation::Divide
1449        )
1450    });
1451    let all_numeric = {
1452        let mut memo = HashMap::new();
1453        std::iter::once(first)
1454            .chain(operands.iter().map(|(_, id)| *id))
1455            .all(|id| rewrite::numeric_only(cells, &mut memo, id))
1456    };
1457    if !has_subtract_or_divide || (all_numeric && operands.len() > 1) {
1458        let terms: Vec<ArithmeticTerm> = std::iter::once(ArithmeticTerm::direct(first))
1459            .chain(operands.iter().map(|(op, id)| ArithmeticTerm {
1460                operand: *id,
1461                inverse: matches!(
1462                    op,
1463                    ArithmeticComputation::Subtract | ArithmeticComputation::Divide
1464                ),
1465            }))
1466            .collect();
1467        let kind = match group {
1468            ArithmeticGroup::Sum => NormalFormKind::Sum(terms),
1469            ArithmeticGroup::Product => NormalFormKind::Product(terms),
1470        };
1471        return cells.intern_empty(kind);
1472    }
1473    operands.iter().fold(first, |current, (op, operand)| {
1474        lower_binary_arithmetic(op, current, *operand, cells)
1475    })
1476}
1477
1478fn lower_binary_arithmetic(
1479    op: &ArithmeticComputation,
1480    left: NormalFormId,
1481    right: NormalFormId,
1482    cells: &mut Cells<'_>,
1483) -> NormalFormId {
1484    let kind = match op {
1485        ArithmeticComputation::Subtract => NormalFormKind::Subtract(left, right),
1486        ArithmeticComputation::Divide => NormalFormKind::Divide(left, right),
1487        ArithmeticComputation::Add => NormalFormKind::Sum(vec![
1488            ArithmeticTerm::direct(left),
1489            ArithmeticTerm::direct(right),
1490        ]),
1491        ArithmeticComputation::Multiply => NormalFormKind::Product(vec![
1492            ArithmeticTerm::direct(left),
1493            ArithmeticTerm::direct(right),
1494        ]),
1495        ArithmeticComputation::Power => NormalFormKind::Power(left, right),
1496        ArithmeticComputation::Modulo => NormalFormKind::Modulo(left, right),
1497    };
1498    cells.intern_empty(kind)
1499}
1500
1501/// Member `path.name` read on the row `base` yields, through the template plans of
1502/// its spec and of every `uses` hop.
1503fn lower_spec_member(
1504    base: NormalFormId,
1505    path: &[String],
1506    name: &str,
1507    cells: &mut Cells<'_>,
1508    lower: &LowerCtx<'_>,
1509) -> NormalFormId {
1510    let crate::planning::semantics::TypeSpecification::Spec { spec } =
1511        &cells.result_type(base).specifications
1512    else {
1513        panic!(
1514            "BUG: member '{}' read on a base that is not spec-typed after typing",
1515            crate::planning::semantics::spec_member_input_key(path, name)
1516        );
1517    };
1518    let chain = lower.row_member_chain(spec, path, name).unwrap_or_else(|| {
1519        panic!(
1520            "BUG: row member '{}' of '{spec}' passed typing",
1521            crate::planning::semantics::spec_member_input_key(path, name)
1522        )
1523    });
1524    let last = chain.last().expect("BUG: a row end chain has a last hop");
1525    let mut result_type = lower.row_end_type(&last.identity, &last.end);
1526    if let NormalFormKind::Leaf(LeafKind::DataPath(data_path)) = &cells.get(base).kind {
1527        result_type = reanchor_lowered_member_type(lower, data_path, path, result_type);
1528    }
1529    result_type = Arc::new(result_type.as_ref().without_written_unit());
1530    cells.intern_empty(NormalFormKind::SpecMember {
1531        base,
1532        path: path.to_vec(),
1533        name: name.to_string(),
1534        row: RowRead {
1535            ends: chain.into_iter().map(|hop| hop.end).collect(),
1536            result_type,
1537        },
1538    })
1539}
1540
1541/// Prefix `uses` hops from a SpecMember's import base onto the field type's
1542/// dynamic scale references (normalize twin of graph typing's reanchor).
1543fn reanchor_lowered_member_type(
1544    lower: &LowerCtx<'_>,
1545    base_path: &DataPath,
1546    member_path: &[String],
1547    lemma_type: Arc<LemmaType>,
1548) -> Arc<LemmaType> {
1549    use crate::planning::semantics::{DataDefinition, LemmaType, SpecIdentity};
1550    let Some(DataDefinition::Import { row, .. }) = lower.data.get(base_path) else {
1551        return lemma_type;
1552    };
1553    LemmaType::reanchor_through_import_path(
1554        lemma_type,
1555        &base_path.data,
1556        row.identity.as_ref(),
1557        member_path,
1558        |identity: &SpecIdentity, segment: &str| {
1559            let template = lower.template(identity);
1560            let slot = DataPath::local(segment.to_string());
1561            match template.data.get(&slot) {
1562                Some(DataDefinition::Import { row, .. }) => Some(row.identity.as_ref().clone()),
1563                _ => None,
1564            }
1565        },
1566    )
1567}
1568
1569/// Lower one node whose children are already lowered to `children`, in `children()` order.
1570fn lower_node(
1571    node: ExprRef<'_, ExpressionKind>,
1572    children: &[NormalFormId],
1573    lowered: &[NormalFormId],
1574    cells: &mut Cells<'_>,
1575    lower: &LowerCtx<'_>,
1576) -> NormalFormId {
1577    match node.kind() {
1578        ExpressionKind::Literal(lit) => {
1579            cells.intern_literal_leaf(lit.to_literal(), Arc::clone(&lit.lemma_type))
1580        }
1581        // A data slot bound to a rule (`-> with slot: big`) stays a data leaf: the read
1582        // takes the rule's value and checks it against the slot's declared type.
1583        ExpressionKind::DataPath(p) => {
1584            cells.intern_data_leaf(p.clone(), data_path_result_type(lower.data, p))
1585        }
1586        ExpressionKind::SpecMember { path, name, .. } => {
1587            lower_spec_member(children[0], path, name, cells, lower)
1588        }
1589        ExpressionKind::RulePath(path) => {
1590            let body = lower.completed_rule(path);
1591            cells.intern_rule_ref(
1592                body,
1593                path.clone(),
1594                Arc::new(lower.rule_type(path).as_ref().without_written_unit()),
1595            )
1596        }
1597        ExpressionKind::Logical { connective, .. } => {
1598            cells.intern_empty(NormalFormKind::Logical(*connective, children.to_vec()))
1599        }
1600        ExpressionKind::Arithmetic { steps, .. } => {
1601            let (first, operands) = children
1602                .split_first()
1603                .expect("BUG: arithmetic chain has a first operand");
1604            assert_eq!(
1605                operands.len(),
1606                steps.len(),
1607                "BUG: arithmetic chain lowered to one operand per step"
1608            );
1609            let steps: Vec<(ArithmeticComputation, NormalFormId)> = steps
1610                .iter()
1611                .zip(operands)
1612                .map(|(step, operand)| (step.op.clone(), *operand))
1613                .collect();
1614            lower_arithmetic_chain(*first, &steps, cells)
1615        }
1616        ExpressionKind::Comparison(_, op, _) => {
1617            let &[left_id, right_id] = children else {
1618                unreachable!("BUG: comparison has two operands")
1619            };
1620            cells.intern_empty(NormalFormKind::Comparison(left_id, op.clone(), right_id))
1621        }
1622        ExpressionKind::UnitConversion(_, target) => {
1623            let &[inner_id] = children else {
1624                unreachable!("BUG: unit conversion has one operand")
1625            };
1626            cells.rebuild(
1627                NormalFormKind::UnitConversion(inner_id, target.clone()),
1628                node.source().cloned(),
1629                None,
1630            )
1631        }
1632        ExpressionKind::LogicalNegation(_, _) => {
1633            let &[inner_id] = children else {
1634                unreachable!("BUG: negation has one operand")
1635            };
1636            cells.intern_empty(NormalFormKind::Not(inner_id))
1637        }
1638        ExpressionKind::MathematicalComputation(op, _) => {
1639            let &[inner_id] = children else {
1640                unreachable!("BUG: math operator has one operand")
1641            };
1642            cells.intern_empty(NormalFormKind::MathOp(op.clone(), inner_id))
1643        }
1644        ExpressionKind::StepRounding { mode, .. } => match *children {
1645            [operand_id] => {
1646                cells.intern_empty(NormalFormKind::StepRounding(*mode, operand_id, None))
1647            }
1648            [operand_id, step_id] => cells.intern_empty(NormalFormKind::StepRounding(
1649                *mode,
1650                operand_id,
1651                Some(step_id),
1652            )),
1653            _ => unreachable!("BUG: step rounding has an operand and an optional step"),
1654        },
1655        ExpressionKind::Veto(v) => cells.intern_empty(NormalFormKind::Veto(v.clone())),
1656        ExpressionKind::Now => cells.intern_empty(NormalFormKind::Now),
1657        ExpressionKind::DateRelative(kind, _) => {
1658            let &[inner_id] = children else {
1659                unreachable!("BUG: date relative has one operand")
1660            };
1661            cells.intern_empty(NormalFormKind::DateRelative(*kind, inner_id))
1662        }
1663        ExpressionKind::DateCalendar(kind, unit, _) => {
1664            let &[inner_id] = children else {
1665                unreachable!("BUG: date calendar has one operand")
1666            };
1667            cells.intern_empty(NormalFormKind::DateCalendar(*kind, *unit, inner_id))
1668        }
1669        ExpressionKind::RangeLiteral(_, _) => {
1670            let &[left_id, right_id] = children else {
1671                unreachable!("BUG: range literal has two operands")
1672            };
1673            cells.intern_empty(NormalFormKind::RangeLiteral(left_id, right_id))
1674        }
1675        ExpressionKind::PastFutureRange(kind, _) => {
1676            let &[inner_id] = children else {
1677                unreachable!("BUG: past/future range has one operand")
1678            };
1679            cells.intern_empty(NormalFormKind::PastFutureRange(*kind, inner_id))
1680        }
1681        ExpressionKind::RangeBound(bound, _) => {
1682            let &[inner_id] = children else {
1683                unreachable!("BUG: range bound has one operand")
1684            };
1685            cells.intern_empty(NormalFormKind::RangeBound(*bound, inner_id))
1686        }
1687        ExpressionKind::RangeContainment(_, _) => {
1688            let &[left_id, right_id] = children else {
1689                unreachable!("BUG: range containment has two operands")
1690            };
1691            cells.intern_empty(NormalFormKind::RangeContainment(left_id, right_id))
1692        }
1693        ExpressionKind::ResultIsVeto(_) => {
1694            let &[operand_id] = children else {
1695                unreachable!("BUG: result-is-veto has one operand")
1696            };
1697            cells.intern_empty(NormalFormKind::ResultIsVeto(operand_id))
1698        }
1699        ExpressionKind::Piecewise(_) => {
1700            let mapped = children
1701                .chunks_exact(2)
1702                .map(|pair| (pair[0], pair[1]))
1703                .collect();
1704            cells.intern_empty(NormalFormKind::Piecewise(mapped))
1705        }
1706        ExpressionKind::ListCommand { op, row_level, .. } => {
1707            let (list_id, args) = children
1708                .split_first()
1709                .expect("BUG: list command has a list operand");
1710            cells.intern_empty(NormalFormKind::ListCommand {
1711                list: *list_id,
1712                op: list_op_kind(op),
1713                args: args.to_vec(),
1714                row_level: *row_level,
1715            })
1716        }
1717        ExpressionKind::RowField {
1718            level,
1719            identity,
1720            end,
1721            qualifier,
1722        } => cells.intern_row_field(
1723            *level,
1724            identity.clone(),
1725            RowFieldEnd {
1726                end: end.clone(),
1727                qualifier: qualifier.clone(),
1728            },
1729            Arc::new(
1730                lower
1731                    .row_end_type(identity, end)
1732                    .as_ref()
1733                    .without_written_unit(),
1734            ),
1735        ),
1736        ExpressionKind::RowValue { level, list } => {
1737            let list_cell = *lowered.get(list.index()).expect(
1738                "BUG: row value type-only list reference must point to an earlier expression",
1739            );
1740            let element_type = cells.result_type(list_cell).list_element_lemma_type();
1741            cells.intern_row_value_leaf(
1742                *level,
1743                Arc::new(element_type.as_ref().without_written_unit()),
1744            )
1745        }
1746    }
1747}
1748
1749/// The normal-form operation of a list op. Its operands are the list command's children
1750/// in `children()` order.
1751fn list_op_kind(op: &crate::planning::semantics::ListOp) -> NormalFormListOp {
1752    use crate::planning::semantics::{ListOp, ListReduce};
1753    match op {
1754        ListOp::Find { .. } => NormalFormListOp::Find,
1755        ListOp::First => NormalFormListOp::First,
1756        ListOp::Last => NormalFormListOp::Last,
1757        ListOp::Count => NormalFormListOp::Count,
1758        ListOp::Reduce { kind, .. } => match kind {
1759            ListReduce::Sum => NormalFormListOp::Sum,
1760            ListReduce::Avg => NormalFormListOp::Avg,
1761            ListReduce::Product => NormalFormListOp::Product,
1762            ListReduce::Min => NormalFormListOp::Min,
1763            ListReduce::Max => NormalFormListOp::Max,
1764        },
1765        ListOp::Predicate { kind, form, .. } => NormalFormListOp::Predicate {
1766            kind: *kind,
1767            form: *form,
1768        },
1769        ListOp::Sort { descending, .. } => NormalFormListOp::Sort {
1770            descending: *descending,
1771        },
1772        ListOp::Top { .. } => NormalFormListOp::Top,
1773        ListOp::With { .. } => NormalFormListOp::With,
1774        ListOp::WithRow { identity, fields } => NormalFormListOp::WithRow {
1775            identity: Arc::new(identity.clone()),
1776            fields: fields.iter().map(|(path, _)| path.clone()).collect(),
1777        },
1778        ListOp::Key { fields } => NormalFormListOp::Key {
1779            fields: fields.clone(),
1780        },
1781    }
1782}
1783
1784/// `first_argument` is the type of the first argument cell; a reduce over instance
1785/// rows takes the type of its row expression.
1786fn list_command_result_type(
1787    list_type: &Arc<LemmaType>,
1788    op: &NormalFormListOp,
1789    first_argument: Option<&Arc<LemmaType>>,
1790    measure_scope: &crate::planning::typing::MeasureScope<'_>,
1791) -> Arc<LemmaType> {
1792    use crate::planning::typing::{
1793        list_command_result_type as stamp, list_has_instance_rows, ListCommandResultKind,
1794    };
1795    let instance_rows = list_has_instance_rows(list_type);
1796    let kind = match op {
1797        NormalFormListOp::Count => ListCommandResultKind::Count,
1798        NormalFormListOp::Predicate {
1799            kind: ListPredKind::Any | ListPredKind::All,
1800            ..
1801        } => ListCommandResultKind::BooleanPredicate,
1802        NormalFormListOp::Predicate {
1803            kind: ListPredKind::Where,
1804            ..
1805        }
1806        | NormalFormListOp::Sort { .. }
1807        | NormalFormListOp::Top
1808        | NormalFormListOp::With
1809        | NormalFormListOp::WithRow { .. }
1810        | NormalFormListOp::Key { .. } => ListCommandResultKind::KeepList,
1811        NormalFormListOp::Sum
1812        | NormalFormListOp::Avg
1813        | NormalFormListOp::Product
1814        | NormalFormListOp::Min
1815        | NormalFormListOp::Max
1816            if instance_rows =>
1817        {
1818            ListCommandResultKind::InstanceReduce
1819        }
1820        NormalFormListOp::Find
1821        | NormalFormListOp::First
1822        | NormalFormListOp::Last
1823        | NormalFormListOp::Sum
1824        | NormalFormListOp::Avg
1825        | NormalFormListOp::Product
1826        | NormalFormListOp::Min
1827        | NormalFormListOp::Max => ListCommandResultKind::Element,
1828    };
1829    let result = stamp(list_type, kind, first_argument, measure_scope);
1830    match op {
1831        NormalFormListOp::Key { fields } => {
1832            crate::planning::typing::refine_list_key_type(&result, fields)
1833        }
1834        _ => result,
1835    }
1836}
1837
1838fn cell(forms: &[NormalForm], id: NormalFormId) -> &NormalForm {
1839    forms.get(id.index()).unwrap_or_else(|| {
1840        panic!(
1841            "BUG: NormalFormId {} out of range (table len {})",
1842            id.0,
1843            forms.len()
1844        )
1845    })
1846}
1847
1848/// Expression text for a DAG node. Follows origin when present so folded
1849/// nodes display their pre-image.
1850/// Display a cell as Lemma syntax. Data paths are qualified by the written row
1851/// whose context the cell narrates in (`people.1.age`), bare at the root.
1852pub(crate) fn explanation_display(
1853    forms: &[NormalForm],
1854    id: NormalFormId,
1855    qualifier: Option<&NameScope>,
1856) -> String {
1857    Syntax(NfNode {
1858        forms,
1859        id,
1860        qualifier,
1861    })
1862    .to_string()
1863}
1864
1865/// Cell whose spelling a node displays: the pre-image of folded cells.
1866fn displayed_cell(forms: &[NormalForm], mut id: NormalFormId) -> &NormalForm {
1867    loop {
1868        let nf = cell(forms, id);
1869        match nf.origin {
1870            Some(origin) => id = origin,
1871            None => return nf,
1872        }
1873    }
1874}
1875
1876#[derive(Clone, Copy, PartialEq, Eq)]
1877enum SpineLevel {
1878    Additive,
1879    Multiplicative,
1880}
1881
1882/// One same-level, left-associative arithmetic chain as its first operand and the steps
1883/// after it, in source order. Walks the left spine iteratively, through origins, so a
1884/// long chain never recurses. A child at another level or a named rule ends the spine.
1885fn arithmetic_chain(
1886    forms: &[NormalForm],
1887    top: NormalFormId,
1888) -> (
1889    NormalFormId,
1890    Vec<(&'static ArithmeticComputation, NormalFormId)>,
1891) {
1892    let level = match &displayed_cell(forms, top).kind {
1893        NormalFormKind::Sum(_) | NormalFormKind::Subtract(..) => SpineLevel::Additive,
1894        NormalFormKind::Product(_) | NormalFormKind::Divide(..) | NormalFormKind::Modulo(..) => {
1895            SpineLevel::Multiplicative
1896        }
1897        _ => unreachable!("BUG: arithmetic_chain on a non-arithmetic cell"),
1898    };
1899    let mut steps = Vec::new();
1900    let mut current = top;
1901    loop {
1902        let shown = displayed_cell(forms, current);
1903        if shown.rule_ref.is_some() {
1904            break;
1905        }
1906        match (&shown.kind, level) {
1907            (NormalFormKind::Sum(terms), SpineLevel::Additive) => {
1908                let (first, rest) = terms
1909                    .split_first()
1910                    .expect("BUG: Sum cell has at least one operand");
1911                assert!(!first.inverse, "BUG: Sum starts with an inverse term");
1912                steps.extend(rest.iter().rev().map(|term| {
1913                    let op = if term.inverse {
1914                        &ArithmeticComputation::Subtract
1915                    } else {
1916                        &ArithmeticComputation::Add
1917                    };
1918                    (op, term.operand)
1919                }));
1920                current = first.operand;
1921            }
1922            (NormalFormKind::Subtract(left, right), SpineLevel::Additive) => {
1923                steps.push((&ArithmeticComputation::Subtract, *right));
1924                current = *left;
1925            }
1926            (NormalFormKind::Product(terms), SpineLevel::Multiplicative) => {
1927                let (first, rest) = terms
1928                    .split_first()
1929                    .expect("BUG: Product cell has at least one operand");
1930                assert!(!first.inverse, "BUG: Product starts with an inverse term");
1931                steps.extend(rest.iter().rev().map(|term| {
1932                    let op = if term.inverse {
1933                        &ArithmeticComputation::Divide
1934                    } else {
1935                        &ArithmeticComputation::Multiply
1936                    };
1937                    (op, term.operand)
1938                }));
1939                current = first.operand;
1940            }
1941            (NormalFormKind::Divide(left, right), SpineLevel::Multiplicative) => {
1942                steps.push((&ArithmeticComputation::Divide, *right));
1943                current = *left;
1944            }
1945            (NormalFormKind::Modulo(left, right), SpineLevel::Multiplicative) => {
1946                steps.push((&ArithmeticComputation::Modulo, *right));
1947                current = *left;
1948            }
1949            _ => break,
1950        }
1951    }
1952    steps.reverse();
1953    (current, steps)
1954}
1955
1956/// How an explanation names the data and rules of the instance context a cell narrates
1957/// in: as the plan holding the instance names them. Data display under the instance's
1958/// hops (`people.1.age`, `i.x`); rules under the `uses` hops above the nearest row or
1959/// the root (`i.total`), so a row's own rules stay bare.
1960pub(crate) struct NameScope {
1961    pub(crate) data: Vec<PathSegment>,
1962    pub(crate) rules: Vec<PathSegment>,
1963}
1964
1965/// A normal-form cell printed as Lemma syntax.
1966#[derive(Clone, Copy)]
1967pub(crate) struct NfNode<'a> {
1968    pub(crate) forms: &'a [NormalForm],
1969    pub(crate) id: NormalFormId,
1970    /// Instance whose context the cell narrates in; `None` for the root plan.
1971    pub(crate) qualifier: Option<&'a NameScope>,
1972}
1973
1974impl<'a> NfNode<'a> {
1975    fn at(self, id: NormalFormId) -> Self {
1976        Self {
1977            forms: self.forms,
1978            id,
1979            qualifier: self.qualifier,
1980        }
1981    }
1982}
1983
1984/// Leaf of a normal-form cell.
1985#[derive(Clone, Copy)]
1986pub(crate) enum NfAtom<'a> {
1987    Literal(&'a LiteralValue, &'a LemmaType),
1988    DataPath(&'a DataPath),
1989    /// Data path qualified by the instance whose context it narrates in.
1990    QualifiedDataPath {
1991        prefix: &'a [PathSegment],
1992        path: &'a DataPath,
1993    },
1994    Rule(&'a RulePath),
1995    /// Rule qualified by the `uses` hops of the instance whose context it narrates in.
1996    QualifiedRule {
1997        prefix: &'a [PathSegment],
1998        path: &'a RulePath,
1999    },
2000    Row(&'a RowEnd),
2001    /// Row field with its written list qualifier: `children.is_leaf`.
2002    QualifiedRow {
2003        qualifier: &'a [String],
2004        end: &'a RowEnd,
2005    },
2006    SpecMemberField {
2007        path: &'a [String],
2008        name: &'a str,
2009    },
2010    Now,
2011    Veto(&'a VetoExpression),
2012}
2013
2014impl std::fmt::Display for NfAtom<'_> {
2015    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2016        match self {
2017            NfAtom::Literal(literal, lemma_type) => {
2018                write!(f, "{}", literal.display_value_with_type(lemma_type))
2019            }
2020            NfAtom::DataPath(path) => write!(f, "{}", path.input_key()),
2021            NfAtom::QualifiedDataPath { prefix, path } => {
2022                for segment in prefix.iter() {
2023                    write!(f, "{}.", segment.input_key())?;
2024                }
2025                write!(f, "{}", path.input_key())
2026            }
2027            NfAtom::Rule(path) => write!(f, "{}", path.input_key()),
2028            NfAtom::QualifiedRule { prefix, path } => {
2029                for segment in prefix.iter() {
2030                    write!(f, "{}.", segment.input_key())?;
2031                }
2032                write!(f, "{}", path.input_key())
2033            }
2034            NfAtom::Row(end) => write!(f, "{end}"),
2035            NfAtom::QualifiedRow { qualifier, end } => {
2036                for (i, segment) in qualifier.iter().enumerate() {
2037                    if i > 0 {
2038                        f.write_str(".")?;
2039                    }
2040                    f.write_str(segment)?;
2041                }
2042                f.write_str(".")?;
2043                write!(f, "{end}")
2044            }
2045            NfAtom::SpecMemberField { path, name } => {
2046                write!(
2047                    f,
2048                    "{}",
2049                    crate::planning::semantics::spec_member_input_key(path, name)
2050                )
2051            }
2052            NfAtom::Now => write!(f, "now"),
2053            NfAtom::Veto(veto) => write!(f, "{veto}"),
2054        }
2055    }
2056}
2057
2058fn value_starts_with_sign(value: &ValueKind) -> bool {
2059    let mut current = value;
2060    loop {
2061        match current {
2062            ValueKind::Number(n) | ValueKind::Measure(n) | ValueKind::Ratio(n) => {
2063                return n.numer_is_negative()
2064            }
2065            ValueKind::Range(left, _) => current = &left.value,
2066            _ => return false,
2067        }
2068    }
2069}
2070
2071fn single_argument(args: &[NormalFormId], command: &str) -> NormalFormId {
2072    match args {
2073        [argument] => *argument,
2074        other => panic!(
2075            "BUG: `{command}` cell must carry one argument, got {}",
2076            other.len()
2077        ),
2078    }
2079}
2080
2081fn optional_argument(args: &[NormalFormId], command: &str) -> Option<NormalFormId> {
2082    match args {
2083        [] => None,
2084        [argument] => Some(*argument),
2085        other => panic!(
2086            "BUG: `{command}` cell carries at most one argument, got {}",
2087            other.len()
2088        ),
2089    }
2090}
2091
2092impl<'a> SyntaxNode<'a> for NfNode<'a> {
2093    type Atom = NfAtom<'a>;
2094
2095    fn view(self) -> SyntaxView<'a, Self> {
2096        let nf = displayed_cell(self.forms, self.id);
2097        let atom = |atom| SyntaxView::Atom {
2098            atom,
2099            starts_with_sign: false,
2100        };
2101        if let Some(path) = &nf.rule_ref {
2102            return match self.qualifier {
2103                Some(scope) if !scope.rules.is_empty() => atom(NfAtom::QualifiedRule {
2104                    prefix: &scope.rules,
2105                    path,
2106                }),
2107                Some(_) | None => atom(NfAtom::Rule(path)),
2108            };
2109        }
2110        let node = |id: &NormalFormId| self.at(*id);
2111        let nodes = |ids: &[NormalFormId]| ids.iter().map(node).collect::<Vec<_>>();
2112        match &nf.kind {
2113            NormalFormKind::Leaf(LeafKind::Literal(literal)) => SyntaxView::Atom {
2114                atom: NfAtom::Literal(literal, nf.result_type.as_ref()),
2115                starts_with_sign: value_starts_with_sign(&literal.value),
2116            },
2117            NormalFormKind::Leaf(LeafKind::DataPath(path)) => match self.qualifier {
2118                Some(scope) if !scope.data.is_empty() => atom(NfAtom::QualifiedDataPath {
2119                    prefix: &scope.data,
2120                    path,
2121                }),
2122                Some(_) | None => atom(NfAtom::DataPath(path)),
2123            },
2124            NormalFormKind::RowField {
2125                end,
2126                qualifier: Some(segments),
2127                ..
2128            } => atom(NfAtom::QualifiedRow {
2129                qualifier: segments,
2130                end,
2131            }),
2132            NormalFormKind::RowField {
2133                end,
2134                qualifier: None,
2135                ..
2136            } => atom(NfAtom::Row(end)),
2137            NormalFormKind::Leaf(LeafKind::RowValue { .. }) => SyntaxView::ImplicitRowValue,
2138            NormalFormKind::Now => atom(NfAtom::Now),
2139            NormalFormKind::Veto(veto) => atom(NfAtom::Veto(veto)),
2140            NormalFormKind::Comparison(left, op, right) => {
2141                SyntaxView::Comparison(node(left), op, node(right))
2142            }
2143            NormalFormKind::Logical(connective, children) => SyntaxView::Logical {
2144                connective: *connective,
2145                operands: nodes(children),
2146            },
2147            NormalFormKind::Not(operand) => {
2148                let shown = displayed_cell(self.forms, *operand);
2149                match &shown.kind {
2150                    NormalFormKind::ResultIsVeto(inner) if shown.rule_ref.is_none() => {
2151                        SyntaxView::IsNotVeto(node(inner))
2152                    }
2153                    _ => SyntaxView::Not(node(operand)),
2154                }
2155            }
2156            NormalFormKind::ResultIsVeto(operand) => SyntaxView::IsVeto(node(operand)),
2157            NormalFormKind::Sum(_)
2158            | NormalFormKind::Product(_)
2159            | NormalFormKind::Subtract(..)
2160            | NormalFormKind::Divide(..)
2161            | NormalFormKind::Modulo(..) => {
2162                let (first, steps) = arithmetic_chain(self.forms, self.id);
2163                let level = match &nf.kind {
2164                    NormalFormKind::Sum(_) | NormalFormKind::Subtract(..) => {
2165                        &ArithmeticComputation::Add
2166                    }
2167                    _ => &ArithmeticComputation::Multiply,
2168                };
2169                SyntaxView::Chain {
2170                    level,
2171                    operands: std::iter::once(node(&first))
2172                        .chain(steps.iter().map(|(_, operand)| node(operand)))
2173                        .collect(),
2174                    ops: steps.iter().map(|(op, _)| *op).collect(),
2175                }
2176            }
2177            NormalFormKind::Power(left, right) => SyntaxView::Chain {
2178                level: &ArithmeticComputation::Power,
2179                operands: vec![node(left), node(right)],
2180                ops: vec![&ArithmeticComputation::Power],
2181            },
2182            NormalFormKind::Negate(operand) => SyntaxView::Negate(node(operand)),
2183            NormalFormKind::Reciprocal(operand) => SyntaxView::Reciprocal(node(operand)),
2184            NormalFormKind::MathOp(op, operand) => SyntaxView::Prefix(op, node(operand)),
2185            NormalFormKind::StepRounding(mode, operand, step) => SyntaxView::ListCommand {
2186                list: node(operand),
2187                command: CommandView::Step(mode.keyword(), step.as_ref().map(node)),
2188            },
2189            NormalFormKind::RangeBound(bound, operand) => SyntaxView::Prefix(bound, node(operand)),
2190            NormalFormKind::UnitConversion(value, target) => {
2191                SyntaxView::UnitConversion(node(value), target)
2192            }
2193            NormalFormKind::DateRelative(kind, date) => SyntaxView::DateRelative(kind, node(date)),
2194            NormalFormKind::DateCalendar(kind, unit, date) => {
2195                SyntaxView::DateCalendar(kind, unit, node(date))
2196            }
2197            NormalFormKind::RangeLiteral(left, right) => {
2198                SyntaxView::RangeLiteral(node(left), node(right))
2199            }
2200            NormalFormKind::PastFutureRange(kind, offset) => {
2201                SyntaxView::PastFutureRange(kind, node(offset))
2202            }
2203            NormalFormKind::RangeContainment(value, range) => {
2204                SyntaxView::RangeContainment(node(value), node(range))
2205            }
2206            NormalFormKind::Piecewise(arms) => SyntaxView::Piecewise(
2207                arms.iter()
2208                    .map(|(condition, result)| (node(condition), node(result)))
2209                    .collect(),
2210            ),
2211            NormalFormKind::SpecMember {
2212                base, path, name, ..
2213            } => {
2214                let base_cell = cell(self.forms, *base);
2215                let named = base_cell.rule_ref.is_some()
2216                    || matches!(
2217                        base_cell.kind,
2218                        NormalFormKind::Leaf(_) | NormalFormKind::SpecMember { .. }
2219                    );
2220                SyntaxView::Member {
2221                    base: node(base),
2222                    path: NfAtom::SpecMemberField {
2223                        path,
2224                        name: name.as_str(),
2225                    },
2226                    parenthesize_base: !named,
2227                }
2228            }
2229            NormalFormKind::OrderedDispatch { .. } => unreachable!(
2230                "BUG: OrderedDispatch always carries its Piecewise pre-image as origin, \
2231                 so displayed_cell never stops at it"
2232            ),
2233            NormalFormKind::ListCommand { list, op, args, .. } => {
2234                let reduce = |name: &'static str| {
2235                    CommandView::Reduce(name, optional_argument(args, name).map(|id| node(&id)))
2236                };
2237                let command = match op {
2238                    NormalFormListOp::Find => CommandView::Find(nodes(args)),
2239                    NormalFormListOp::First => CommandView::First,
2240                    NormalFormListOp::Last => CommandView::Last,
2241                    NormalFormListOp::Count => CommandView::Count,
2242                    NormalFormListOp::Sum => reduce("sum"),
2243                    NormalFormListOp::Avg => reduce("avg"),
2244                    NormalFormListOp::Product => reduce("product"),
2245                    NormalFormListOp::Min => reduce("min"),
2246                    NormalFormListOp::Max => reduce("max"),
2247                    NormalFormListOp::Predicate { kind, form } => {
2248                        let name = kind.keyword();
2249                        let expr = node(&single_argument(args, name));
2250                        let view = match form {
2251                            ListPredicateForm::Value => PredicateView::Value(expr),
2252                            ListPredicateForm::Instance => PredicateView::Instance(expr),
2253                        };
2254                        CommandView::Predicate(name, view)
2255                    }
2256                    NormalFormListOp::Sort { descending } => CommandView::Sort {
2257                        descending: *descending,
2258                        argument: optional_argument(args, "sort").map(|id| node(&id)),
2259                    },
2260                    NormalFormListOp::Top => CommandView::Top(node(&single_argument(args, "top"))),
2261                    NormalFormListOp::With => {
2262                        CommandView::With(args.iter().map(|id| (None, node(id))).collect())
2263                    }
2264                    NormalFormListOp::WithRow { fields, .. } => {
2265                        assert_eq!(
2266                            fields.len(),
2267                            args.len(),
2268                            "BUG: `with` row cell must carry one value per field"
2269                        );
2270                        CommandView::With(
2271                            fields
2272                                .iter()
2273                                .zip(args)
2274                                .map(|(field, value)| (Some(NfAtom::DataPath(field)), node(value)))
2275                                .collect(),
2276                        )
2277                    }
2278                    NormalFormListOp::Key { fields } => {
2279                        CommandView::Key(fields.iter().map(NfAtom::DataPath).collect())
2280                    }
2281                };
2282                SyntaxView::ListCommand {
2283                    list: node(list),
2284                    command,
2285                }
2286            }
2287        }
2288    }
2289}
2290
2291#[cfg(test)]
2292mod tests {
2293    use super::*;
2294
2295    static EMPTY_ROW_TEMPLATES: BTreeMap<SpecIdentity, Arc<ExecutionPlan>> = BTreeMap::new();
2296    static TEST_SELF_IDENTITY: std::sync::LazyLock<SpecIdentity> =
2297        std::sync::LazyLock::new(|| SpecIdentity {
2298            repository: None,
2299            spec: "test".to_string(),
2300            effective: crate::parsing::ast::EffectiveDate::Origin,
2301            slice: crate::parsing::ast::EffectiveDate::Origin,
2302        });
2303    use crate::computation::arithmetic::SignatureIndex;
2304    use crate::computation::rational::{rational_new, rational_one, rational_zero};
2305    use crate::parsing::arena::{ExprId, ExprTable, ExprTree};
2306    use crate::parsing::ast::{ChainStep, ConversionTypeKind};
2307    use crate::planning::semantics::TypedLiteral;
2308    use crate::planning::semantics::{
2309        ComparisonComputation, DataPath, ExpressionKind, NegationType, SemanticConversionTarget,
2310        ValueKind,
2311    };
2312    use crate::planning::unit_index::UnitIndex;
2313    use std::rc::Rc;
2314
2315    fn interner_forms(interner: &NormalFormInterner) -> &[NormalForm] {
2316        &interner.forms
2317    }
2318
2319    fn empty_named_types() -> &'static IndexMap<String, Arc<LemmaType>> {
2320        use std::sync::OnceLock;
2321        static EMPTY: OnceLock<IndexMap<String, Arc<LemmaType>>> = OnceLock::new();
2322        EMPTY.get_or_init(IndexMap::new)
2323    }
2324
2325    fn empty_measure_scope<'a>(
2326        unit_index: &'a UnitIndex,
2327        signature_index: &'a SignatureIndex,
2328    ) -> MeasureScope<'a> {
2329        MeasureScope {
2330            unit_index,
2331            signature_index,
2332            named_types: empty_named_types(),
2333        }
2334    }
2335
2336    /// Typed data map for algebraic normalize unit tests: mirrors a post-validation
2337    /// plan so [`Cells::type_of`] never stamps undetermined.
2338    fn algebraic_scope() -> IndexMap<DataPath, DataDefinition> {
2339        use crate::parsing::source::{Source as ParseSource, SourceType};
2340        use crate::Span;
2341        let source = ParseSource::new(
2342            SourceType::Volatile,
2343            Span {
2344                start: 0,
2345                end: 0,
2346                line: 1,
2347                col: 1,
2348            },
2349        );
2350        let mut data = IndexMap::new();
2351        for (name, resolved_type) in [
2352            ("x", primitive_number_arc().clone()),
2353            ("y", primitive_number_arc().clone()),
2354            ("flag", primitive_boolean_arc().clone()),
2355            ("flag2", primitive_boolean_arc().clone()),
2356        ] {
2357            data.insert(
2358                DataPath::new(vec![], name.into()),
2359                DataDefinition::TypeDeclaration {
2360                    resolved_type,
2361                    declared_suggestion: None,
2362                    declared_value: None,
2363                    source: source.clone(),
2364                },
2365            );
2366        }
2367        data
2368    }
2369
2370    /// Normalize through the production pipeline with typed algebraic data in scope.
2371    fn normalize_expression(expr: &Ex) -> Result<Expression, Error> {
2372        let data = algebraic_scope();
2373        normalize_expression_with_data(expr, &data).map(|(expr, _)| expr)
2374    }
2375
2376    /// Normalize with a resolved data map in scope, returning the normalized
2377    /// expression alongside the interner so tests can inspect the shipped shape.
2378    fn normalize_expression_with_data(
2379        expr: &Ex,
2380        data: &IndexMap<DataPath, DataDefinition>,
2381    ) -> Result<(Expression, NormalFormInterner), Error> {
2382        let tree = expr.tree();
2383        let source = tree.root_source().cloned();
2384        let mut interner = NormalFormInterner::new();
2385        let root = lower_with_data(&tree, &mut interner, data);
2386        let unit_index = UnitIndex::new();
2387        let signature_index = SignatureIndex::new();
2388        let measure_scope = empty_measure_scope(&unit_index, &signature_index);
2389        let nf = rewrite::normalize(
2390            root,
2391            Cells::new(&mut interner, measure_scope),
2392            source.clone(),
2393        )?;
2394        let expression = to_expression(interner_forms(&interner), nf, source);
2395        Ok((expression, interner))
2396    }
2397
2398    fn to_normal_form_empty(expr: &Ex, interner: &mut NormalFormInterner) -> NormalFormId {
2399        let data = algebraic_scope();
2400        to_normal_form_with_data(expr, interner, &data)
2401    }
2402
2403    fn to_normal_form_with_data(
2404        expr: &Ex,
2405        interner: &mut NormalFormInterner,
2406        data: &IndexMap<DataPath, DataDefinition>,
2407    ) -> NormalFormId {
2408        lower_with_data(&expr.tree(), interner, data)
2409    }
2410
2411    fn lower_with_data(
2412        expr: &Expression,
2413        interner: &mut NormalFormInterner,
2414        data: &IndexMap<DataPath, DataDefinition>,
2415    ) -> NormalFormId {
2416        let completed_rules = HashMap::new();
2417        let rule_types = HashMap::new();
2418        let lower = LowerCtx {
2419            completed_rules: &completed_rules,
2420            rule_types: &rule_types,
2421            data,
2422            row_templates: &EMPTY_ROW_TEMPLATES,
2423            self_identity: &TEST_SELF_IDENTITY,
2424            rule_order: &[],
2425        };
2426        let unit_index = UnitIndex::new();
2427        let signature_index = SignatureIndex::new();
2428        let mut cells = Cells::new(interner, empty_measure_scope(&unit_index, &signature_index));
2429        to_normal_form(expr, &mut cells, &lower)
2430    }
2431
2432    /// Lowers normal forms into one expression table; every node carries `source`.
2433    struct Lowering<'f> {
2434        forms: &'f [NormalForm],
2435        source: Option<Source>,
2436        table: ExprTable<ExpressionKind>,
2437    }
2438
2439    fn to_expression(forms: &[NormalForm], id: NormalFormId, source: Option<Source>) -> Expression {
2440        let mut lowering = Lowering {
2441            forms,
2442            source,
2443            table: ExprTable::default(),
2444        };
2445        let root = lowering.child(id);
2446        ExprTree::new(lowering.table, root)
2447    }
2448
2449    impl Lowering<'_> {
2450        fn child(&mut self, id: NormalFormId) -> ExprId {
2451            let kind = self.kind(id);
2452            self.table.push(kind, self.source.clone())
2453        }
2454
2455        fn kind(&mut self, id: NormalFormId) -> ExpressionKind {
2456            let forms = self.forms;
2457            let source = self.source.clone();
2458            let nf = cell(forms, id);
2459            if let Some(path) = &nf.rule_ref {
2460                return ExpressionKind::RulePath(path.clone());
2461            }
2462            match &nf.kind {
2463                NormalFormKind::Leaf(LeafKind::Literal(lit)) => {
2464                    ExpressionKind::Literal(Box::new(TypedLiteral {
2465                        value: lit.value.clone(),
2466                        lemma_type: Arc::clone(&nf.result_type),
2467                    }))
2468                }
2469                NormalFormKind::Leaf(LeafKind::DataPath(p)) => ExpressionKind::DataPath(p.clone()),
2470                NormalFormKind::Sum(terms) => self.sum_kind(terms),
2471                NormalFormKind::Product(terms) => self.product_kind(terms),
2472                NormalFormKind::Subtract(a, b) => {
2473                    self.binary_kind(*a, ArithmeticComputation::Subtract, *b)
2474                }
2475                NormalFormKind::Divide(a, b) => {
2476                    self.binary_kind(*a, ArithmeticComputation::Divide, *b)
2477                }
2478                NormalFormKind::Power(base, exp) => {
2479                    self.binary_kind(*base, ArithmeticComputation::Power, *exp)
2480                }
2481                NormalFormKind::Modulo(a, b) => {
2482                    self.binary_kind(*a, ArithmeticComputation::Modulo, *b)
2483                }
2484                NormalFormKind::Negate(x) => {
2485                    let zero = self.table.push(
2486                        ExpressionKind::Literal(Box::new(TypedLiteral::number(rational_zero()))),
2487                        source.clone(),
2488                    );
2489                    let operand = self.child(*x);
2490                    arithmetic_kind(zero, vec![(ArithmeticComputation::Subtract, operand)])
2491                }
2492                NormalFormKind::Reciprocal(x) => {
2493                    let one = self.table.push(
2494                        ExpressionKind::Literal(Box::new(TypedLiteral::number(rational_one()))),
2495                        source.clone(),
2496                    );
2497                    let operand = self.child(*x);
2498                    arithmetic_kind(one, vec![(ArithmeticComputation::Divide, operand)])
2499                }
2500                NormalFormKind::Comparison(a, op, b) => {
2501                    ExpressionKind::Comparison(self.child(*a), op.clone(), self.child(*b))
2502                }
2503                NormalFormKind::Logical(connective, children) => {
2504                    self.logical_kind(*connective, children)
2505                }
2506                NormalFormKind::Not(x) => {
2507                    ExpressionKind::LogicalNegation(self.child(*x), NegationType::Not)
2508                }
2509                NormalFormKind::MathOp(op, x) => {
2510                    ExpressionKind::MathematicalComputation(op.clone(), self.child(*x))
2511                }
2512                NormalFormKind::StepRounding(mode, x, step) => ExpressionKind::StepRounding {
2513                    operand: self.child(*x),
2514                    mode: *mode,
2515                    step: step.map(|step| self.child(step)),
2516                },
2517                NormalFormKind::UnitConversion(x, target) => {
2518                    ExpressionKind::UnitConversion(self.child(*x), target.clone())
2519                }
2520                NormalFormKind::Veto(v) => ExpressionKind::Veto(v.clone()),
2521                NormalFormKind::Now => ExpressionKind::Now,
2522                NormalFormKind::DateRelative(kind, x) => {
2523                    ExpressionKind::DateRelative(*kind, self.child(*x))
2524                }
2525                NormalFormKind::DateCalendar(kind, unit, x) => {
2526                    ExpressionKind::DateCalendar(*kind, *unit, self.child(*x))
2527                }
2528                NormalFormKind::RangeLiteral(a, b) => {
2529                    ExpressionKind::RangeLiteral(self.child(*a), self.child(*b))
2530                }
2531                NormalFormKind::PastFutureRange(kind, x) => {
2532                    ExpressionKind::PastFutureRange(*kind, self.child(*x))
2533                }
2534                NormalFormKind::RangeBound(bound, x) => {
2535                    ExpressionKind::RangeBound(*bound, self.child(*x))
2536                }
2537                NormalFormKind::RangeContainment(a, b) => {
2538                    ExpressionKind::RangeContainment(self.child(*a), self.child(*b))
2539                }
2540                NormalFormKind::ResultIsVeto(operand) => {
2541                    ExpressionKind::ResultIsVeto(self.child(*operand))
2542                }
2543                NormalFormKind::Piecewise(arms) => ExpressionKind::Piecewise(
2544                    arms.iter()
2545                        .map(|(condition, result)| (self.child(*condition), self.child(*result)))
2546                        .collect(),
2547                ),
2548                NormalFormKind::SpecMember {
2549                    base, path, name, ..
2550                } => ExpressionKind::SpecMember {
2551                    base: self.child(*base),
2552                    path: path.clone(),
2553                    name: name.clone(),
2554                },
2555                NormalFormKind::RowField {
2556                    level,
2557                    identity,
2558                    end,
2559                    qualifier,
2560                    ..
2561                } => ExpressionKind::RowField {
2562                    level: *level,
2563                    identity: identity.clone(),
2564                    end: end.clone(),
2565                    qualifier: qualifier.clone(),
2566                },
2567                NormalFormKind::Leaf(LeafKind::RowValue { .. }) => {
2568                    panic!("BUG: normal-form RowValue cannot be rebuilt without its list context")
2569                }
2570                // A dispatch table has no Expression spelling; its pre-image does.
2571                NormalFormKind::OrderedDispatch { .. } => self.kind(
2572                    nf.origin
2573                        .expect("BUG: OrderedDispatch without its Piecewise pre-image"),
2574                ),
2575                NormalFormKind::ListCommand {
2576                    list,
2577                    op,
2578                    args,
2579                    row_level,
2580                } => {
2581                    let list_expr = self.child(*list);
2582                    let arg_exprs: Vec<ExprId> = args.iter().map(|id| self.child(*id)).collect();
2583                    use crate::planning::semantics::{ListOp, ListReduce};
2584                    let planned = match op {
2585                        NormalFormListOp::Find => ListOp::Find { keys: arg_exprs },
2586                        NormalFormListOp::First => ListOp::First,
2587                        NormalFormListOp::Last => ListOp::Last,
2588                        NormalFormListOp::Count => ListOp::Count,
2589                        NormalFormListOp::Sum => ListOp::Reduce {
2590                            kind: ListReduce::Sum,
2591                            argument: arg_exprs.into_iter().next(),
2592                        },
2593                        NormalFormListOp::Avg => ListOp::Reduce {
2594                            kind: ListReduce::Avg,
2595                            argument: arg_exprs.into_iter().next(),
2596                        },
2597                        NormalFormListOp::Product => ListOp::Reduce {
2598                            kind: ListReduce::Product,
2599                            argument: arg_exprs.into_iter().next(),
2600                        },
2601                        NormalFormListOp::Min => ListOp::Reduce {
2602                            kind: ListReduce::Min,
2603                            argument: arg_exprs.into_iter().next(),
2604                        },
2605                        NormalFormListOp::Max => ListOp::Reduce {
2606                            kind: ListReduce::Max,
2607                            argument: arg_exprs.into_iter().next(),
2608                        },
2609                        NormalFormListOp::Predicate { kind, form } => ListOp::Predicate {
2610                            kind: *kind,
2611                            form: *form,
2612                            expr: arg_exprs
2613                                .into_iter()
2614                                .next()
2615                                .expect("BUG: predicate needs a row expression"),
2616                        },
2617                        NormalFormListOp::Sort { descending } => ListOp::Sort {
2618                            descending: *descending,
2619                            argument: arg_exprs.into_iter().next(),
2620                        },
2621                        NormalFormListOp::Top => ListOp::Top {
2622                            n: arg_exprs.into_iter().next().expect("BUG: top needs n"),
2623                        },
2624                        NormalFormListOp::With => ListOp::With { rows: arg_exprs },
2625                        NormalFormListOp::WithRow { identity, fields } => ListOp::WithRow {
2626                            identity: SpecIdentity::clone(identity),
2627                            fields: fields.iter().cloned().zip(arg_exprs).collect(),
2628                        },
2629                        NormalFormListOp::Key { fields } => ListOp::Key {
2630                            fields: fields.clone(),
2631                        },
2632                    };
2633                    ExpressionKind::ListCommand {
2634                        list: list_expr,
2635                        op: planned,
2636                        row_level: *row_level,
2637                    }
2638                }
2639            }
2640        }
2641
2642        fn binary_kind(
2643            &mut self,
2644            left: NormalFormId,
2645            op: ArithmeticComputation,
2646            right: NormalFormId,
2647        ) -> ExpressionKind {
2648            let left = self.child(left);
2649            let right = self.child(right);
2650            arithmetic_kind(left, vec![(op, right)])
2651        }
2652
2653        fn sum_kind(&mut self, terms: &[ArithmeticTerm]) -> ExpressionKind {
2654            match terms {
2655                [] => ExpressionKind::Literal(Box::new(TypedLiteral::number(rational_zero()))),
2656                [one] => self.kind(one.operand),
2657                [first, rest @ ..] => self.terms_kind(
2658                    first.operand,
2659                    rest,
2660                    ArithmeticComputation::Add,
2661                    ArithmeticComputation::Subtract,
2662                ),
2663            }
2664        }
2665
2666        fn product_kind(&mut self, terms: &[ArithmeticTerm]) -> ExpressionKind {
2667            match terms {
2668                [] => ExpressionKind::Literal(Box::new(TypedLiteral::number(rational_one()))),
2669                [one] => self.kind(one.operand),
2670                [first, rest @ ..] => self.terms_kind(
2671                    first.operand,
2672                    rest,
2673                    ArithmeticComputation::Multiply,
2674                    ArithmeticComputation::Divide,
2675                ),
2676            }
2677        }
2678
2679        fn terms_kind(
2680            &mut self,
2681            first: NormalFormId,
2682            rest: &[ArithmeticTerm],
2683            direct: ArithmeticComputation,
2684            inverse: ArithmeticComputation,
2685        ) -> ExpressionKind {
2686            let first = self.child(first);
2687            let steps = rest
2688                .iter()
2689                .map(|term| {
2690                    let op = if term.inverse {
2691                        inverse.clone()
2692                    } else {
2693                        direct.clone()
2694                    };
2695                    (op, self.child(term.operand))
2696                })
2697                .collect();
2698            arithmetic_kind(first, steps)
2699        }
2700
2701        fn logical_kind(
2702            &mut self,
2703            connective: LogicalConnective,
2704            children: &[NormalFormId],
2705        ) -> ExpressionKind {
2706            match children {
2707                [] => unreachable!("BUG: Logical has at least one operand"),
2708                [one] => self.kind(*one),
2709                [first, rest @ ..] => {
2710                    let first = self.child(*first);
2711                    let operands = rest.iter().map(|id| self.child(*id)).collect();
2712                    logical_kind(connective, first, operands)
2713                }
2714            }
2715        }
2716    }
2717
2718    /// Test expressions carry no spans; chain steps require one.
2719    fn step_source() -> Source {
2720        Source::new(
2721            crate::parsing::source::SourceType::Volatile,
2722            crate::Span {
2723                start: 0,
2724                end: 0,
2725                line: 1,
2726                col: 1,
2727            },
2728        )
2729    }
2730
2731    fn arithmetic_kind(
2732        first: ExprId,
2733        steps: Vec<(ArithmeticComputation, ExprId)>,
2734    ) -> ExpressionKind {
2735        ExpressionKind::Arithmetic {
2736            first,
2737            steps: steps
2738                .into_iter()
2739                .map(|(op, operand)| ChainStep {
2740                    op,
2741                    operand,
2742                    source_location: step_source(),
2743                })
2744                .collect(),
2745        }
2746    }
2747
2748    /// `left op right` of a one-step arithmetic chain.
2749    fn binary_arithmetic(
2750        kind: &ExpressionKind,
2751    ) -> Option<(&ExprId, &ArithmeticComputation, &ExprId)> {
2752        match kind {
2753            ExpressionKind::Arithmetic { first, steps } => match steps.as_slice() {
2754                [step] => Some((first, &step.op, &step.operand)),
2755                _ => None,
2756            },
2757            _ => None,
2758        }
2759    }
2760
2761    fn logical_kind(
2762        connective: LogicalConnective,
2763        first: ExprId,
2764        operands: Vec<ExprId>,
2765    ) -> ExpressionKind {
2766        ExpressionKind::Logical {
2767            connective,
2768            first,
2769            steps: operands
2770                .into_iter()
2771                .map(|operand| ChainStep {
2772                    op: (),
2773                    operand,
2774                    source_location: step_source(),
2775                })
2776                .collect(),
2777        }
2778    }
2779
2780    fn num_expr(n: i64) -> Ex {
2781        Ex::leaf(
2782            ExpressionKind::Literal(Box::new(TypedLiteral::number(rational_new(n, 1)))),
2783            None,
2784        )
2785    }
2786
2787    fn bool_expr(b: bool) -> Ex {
2788        Ex::leaf(
2789            ExpressionKind::Literal(Box::new(TypedLiteral::from_bool(b))),
2790            None,
2791        )
2792    }
2793
2794    fn dx() -> Ex {
2795        Ex::leaf(
2796            ExpressionKind::DataPath(DataPath::new(vec![], "x".into())),
2797            None,
2798        )
2799    }
2800
2801    fn dflag() -> Ex {
2802        Ex::leaf(
2803            ExpressionKind::DataPath(DataPath::new(vec![], "flag".into())),
2804            None,
2805        )
2806    }
2807
2808    fn dflag2() -> Ex {
2809        Ex::leaf(
2810            ExpressionKind::DataPath(DataPath::new(vec![], "flag2".into())),
2811            None,
2812        )
2813    }
2814
2815    /// Pushes a test expression's nodes into a table and returns its root.
2816    type PushNodes = dyn Fn(&mut ExprTable<ExpressionKind>) -> ExprId;
2817
2818    /// Test expression; [`Ex::tree`] pushes every node straight into one table.
2819    #[derive(Clone)]
2820    struct Ex(Rc<PushNodes>);
2821
2822    impl Ex {
2823        fn leaf(kind: ExpressionKind, source: Option<Source>) -> Self {
2824            Self(Rc::new(move |table| {
2825                table.push(kind.clone(), source.clone())
2826            }))
2827        }
2828
2829        fn push(&self, table: &mut ExprTable<ExpressionKind>) -> ExprId {
2830            (self.0)(table)
2831        }
2832
2833        fn tree(&self) -> Expression {
2834            let mut table = ExprTable::default();
2835            let root = self.push(&mut table);
2836            ExprTree::new(table, root)
2837        }
2838    }
2839
2840    fn compose_expr(children: &[&Ex], make: impl Fn(&[ExprId]) -> ExpressionKind + 'static) -> Ex {
2841        let children: Vec<Ex> = children.iter().map(|child| (*child).clone()).collect();
2842        Ex(Rc::new(move |table| {
2843            let ids: Vec<ExprId> = children.iter().map(|child| child.push(table)).collect();
2844            table.push(make(&ids), None)
2845        }))
2846    }
2847
2848    /// A rule as planning converts it: `default`, or a `Piecewise` over `default` (condition
2849    /// literal `true`) and each `unless <condition> then <result>` arm.
2850    fn unless_chain(default: Ex, arms: Vec<(Ex, Ex)>) -> Ex {
2851        if arms.is_empty() {
2852            return default;
2853        }
2854        Ex(Rc::new(move |table| {
2855            let default_result = default.push(table);
2856            let default_condition = table.push(
2857                ExpressionKind::Literal(Box::new(TypedLiteral::from_bool(true))),
2858                None,
2859            );
2860            let mut pieces = vec![(default_condition, default_result)];
2861            for (condition, result) in &arms {
2862                let condition = condition.push(table);
2863                let result = result.push(table);
2864                pieces.push((condition, result));
2865            }
2866            table.push(ExpressionKind::Piecewise(pieces), None)
2867        }))
2868    }
2869
2870    fn add_expr(a: Ex, b: Ex) -> Ex {
2871        compose_expr(&[&a, &b], |ids| {
2872            arithmetic_kind(ids[0], vec![(ArithmeticComputation::Add, ids[1])])
2873        })
2874    }
2875
2876    fn mul_expr(a: Ex, b: Ex) -> Ex {
2877        compose_expr(&[&a, &b], |ids| {
2878            arithmetic_kind(ids[0], vec![(ArithmeticComputation::Multiply, ids[1])])
2879        })
2880    }
2881
2882    fn div_expr(a: Ex, b: Ex) -> Ex {
2883        compose_expr(&[&a, &b], |ids| {
2884            arithmetic_kind(ids[0], vec![(ArithmeticComputation::Divide, ids[1])])
2885        })
2886    }
2887
2888    fn pow_expr(a: Ex, b: Ex) -> Ex {
2889        compose_expr(&[&a, &b], |ids| {
2890            arithmetic_kind(ids[0], vec![(ArithmeticComputation::Power, ids[1])])
2891        })
2892    }
2893
2894    fn and_expr(a: Ex, b: Ex) -> Ex {
2895        compose_expr(&[&a, &b], |ids| {
2896            logical_kind(LogicalConnective::And, ids[0], vec![ids[1]])
2897        })
2898    }
2899
2900    fn or_expr(a: Ex, b: Ex) -> Ex {
2901        compose_expr(&[&a, &b], |ids| {
2902            logical_kind(LogicalConnective::Or, ids[0], vec![ids[1]])
2903        })
2904    }
2905
2906    fn not_expr(inner: Ex) -> Ex {
2907        compose_expr(&[&inner], |ids| {
2908            ExpressionKind::LogicalNegation(ids[0], NegationType::Not)
2909        })
2910    }
2911
2912    fn lt_expr(a: Ex, b: Ex) -> Ex {
2913        compose_expr(&[&a, &b], |ids| {
2914            ExpressionKind::Comparison(ids[0], ComparisonComputation::LessThan, ids[1])
2915        })
2916    }
2917
2918    fn sub_expr(a: Ex, b: Ex) -> Ex {
2919        compose_expr(&[&a, &b], |ids| {
2920            arithmetic_kind(ids[0], vec![(ArithmeticComputation::Subtract, ids[1])])
2921        })
2922    }
2923
2924    fn math_expr(op: MathematicalComputation, operand: Ex) -> Ex {
2925        compose_expr(&[&operand], move |ids| {
2926            ExpressionKind::MathematicalComputation(op.clone(), ids[0])
2927        })
2928    }
2929
2930    mod ordered_dispatch_fold {
2931        use super::*;
2932        use crate::parsing::source::{Source as ParseSource, SourceType};
2933        use crate::planning::ordered_dispatch::{region_count, DispatchKey};
2934        use crate::planning::semantics::{
2935            primitive_boolean_arc, primitive_date_arc, primitive_number_arc, primitive_text_arc,
2936            LemmaType, TypeSpecification,
2937        };
2938        use crate::Span;
2939
2940        fn test_source() -> ParseSource {
2941            ParseSource::new(
2942                SourceType::Volatile,
2943                Span {
2944                    start: 0,
2945                    end: 0,
2946                    line: 1,
2947                    col: 1,
2948                },
2949            )
2950        }
2951
2952        fn data_path(name: &str) -> DataPath {
2953            DataPath::new(vec![], name.into())
2954        }
2955
2956        fn declared(name: &str, resolved_type: Arc<LemmaType>) -> (DataPath, DataDefinition) {
2957            (
2958                data_path(name),
2959                DataDefinition::TypeDeclaration {
2960                    resolved_type,
2961                    declared_suggestion: None,
2962                    declared_value: None,
2963                    source: test_source(),
2964                },
2965            )
2966        }
2967
2968        /// `x: number`, `code: text`, `flag: boolean`, `day: date`, `rate: ratio`.
2969        fn scope() -> IndexMap<DataPath, DataDefinition> {
2970            let mut data = IndexMap::new();
2971            for entry in [
2972                declared("x", primitive_number_arc().clone()),
2973                declared("y", primitive_number_arc().clone()),
2974                declared("code", primitive_text_arc().clone()),
2975                declared("flag", primitive_boolean_arc().clone()),
2976                declared("day", primitive_date_arc().clone()),
2977                declared(
2978                    "rate",
2979                    Arc::new(LemmaType::primitive(TypeSpecification::ratio())),
2980                ),
2981            ] {
2982                data.insert(entry.0, entry.1);
2983            }
2984            data
2985        }
2986
2987        fn path_expr(name: &str) -> Ex {
2988            Ex::leaf(ExpressionKind::DataPath(data_path(name)), None)
2989        }
2990
2991        fn text_expr(text: &str) -> Ex {
2992            Ex::leaf(
2993                ExpressionKind::Literal(Box::new(TypedLiteral::text(text.to_string()))),
2994                None,
2995            )
2996        }
2997
2998        fn compare(left: Ex, op: ComparisonComputation, right: Ex) -> Ex {
2999            compose_expr(&[&left, &right], move |ids| {
3000                ExpressionKind::Comparison(ids[0], op.clone(), ids[1])
3001            })
3002        }
3003
3004        /// `default` followed by `unless <condition> then <result>` arms.
3005        fn chain(default: Ex, arms: Vec<(Ex, Ex)>) -> Ex {
3006            unless_chain(default, arms)
3007        }
3008
3009        /// Normalize `expr` against `scope()` and return the root cell.
3010        fn fold(expr: &Ex) -> (NormalFormId, NormalFormInterner) {
3011            let data = scope();
3012            let mut interner = NormalFormInterner::new();
3013            let root = to_normal_form_with_data(expr, &mut interner, &data);
3014            let unit_index = UnitIndex::new();
3015            let signature_index = SignatureIndex::new();
3016            let measure_scope = empty_measure_scope(&unit_index, &signature_index);
3017            let nf = rewrite::normalize(root, Cells::new(&mut interner, measure_scope), None)
3018                .expect("normalize");
3019            (nf, interner)
3020        }
3021
3022        /// Boundaries and the result-per-region table, as literal display strings.
3023        fn table(expr: &Ex) -> (Vec<DispatchKey>, Vec<String>) {
3024            let (nf, interner) = fold(expr);
3025            let NormalFormKind::OrderedDispatch {
3026                boundaries,
3027                regions,
3028                ..
3029            } = &interner.get(nf).kind
3030            else {
3031                panic!("expected OrderedDispatch, got {:?}", interner.get(nf).kind);
3032            };
3033            let rendered = regions
3034                .iter()
3035                .map(|region| explanation_display(interner_forms(&interner), region.cell(), None))
3036                .collect();
3037            (boundaries.to_vec(), rendered)
3038        }
3039
3040        fn assert_declines(expr: &Ex, reason: &str) {
3041            let (nf, interner) = fold(expr);
3042            assert!(
3043                !matches!(
3044                    interner.get(nf).kind,
3045                    NormalFormKind::OrderedDispatch { .. }
3046                ),
3047                "must not fold ({reason}), got {:?}",
3048                interner.get(nf).kind
3049            );
3050        }
3051
3052        #[test]
3053        fn equality_chain_on_text_folds_to_point_regions() {
3054            let expr = chain(
3055                num_expr(0),
3056                vec![
3057                    (
3058                        compare(
3059                            path_expr("code"),
3060                            ComparisonComputation::Is,
3061                            text_expr("NL"),
3062                        ),
3063                        num_expr(1),
3064                    ),
3065                    (
3066                        compare(
3067                            path_expr("code"),
3068                            ComparisonComputation::Is,
3069                            text_expr("BE"),
3070                        ),
3071                        num_expr(2),
3072                    ),
3073                ],
3074            );
3075            let (boundaries, regions) = table(&expr);
3076            assert_eq!(
3077                boundaries,
3078                vec![
3079                    DispatchKey::Text(std::sync::Arc::from("BE")),
3080                    DispatchKey::Text(std::sync::Arc::from("NL"))
3081                ]
3082            );
3083            // Every interval holds the default; only the two points carry results.
3084            assert_eq!(regions, vec!["0", "2", "0", "1", "0"]);
3085        }
3086
3087        #[test]
3088        fn later_arm_wins_when_two_arms_share_a_constant() {
3089            let expr = chain(
3090                num_expr(0),
3091                vec![
3092                    (
3093                        compare(path_expr("x"), ComparisonComputation::Is, num_expr(5)),
3094                        num_expr(1),
3095                    ),
3096                    (
3097                        compare(path_expr("x"), ComparisonComputation::Is, num_expr(5)),
3098                        num_expr(2),
3099                    ),
3100                ],
3101            );
3102            let (boundaries, regions) = table(&expr);
3103            assert_eq!(boundaries.len(), 1, "duplicates collapse to one breakpoint");
3104            assert_eq!(regions, vec!["0", "2", "0"]);
3105        }
3106
3107        #[test]
3108        fn greater_than_and_greater_or_equal_differ_at_the_boundary() {
3109            let strict = chain(
3110                num_expr(0),
3111                vec![(
3112                    compare(
3113                        path_expr("x"),
3114                        ComparisonComputation::GreaterThan,
3115                        num_expr(5),
3116                    ),
3117                    num_expr(1),
3118                )],
3119            );
3120            assert_eq!(table(&strict).1, vec!["0", "0", "1"]);
3121
3122            let inclusive = chain(
3123                num_expr(0),
3124                vec![(
3125                    compare(
3126                        path_expr("x"),
3127                        ComparisonComputation::GreaterThanOrEqual,
3128                        num_expr(5),
3129                    ),
3130                    num_expr(1),
3131                )],
3132            );
3133            assert_eq!(table(&inclusive).1, vec!["0", "1", "1"]);
3134        }
3135
3136        #[test]
3137        fn is_not_claims_every_region_but_its_own_point() {
3138            let expr = chain(
3139                num_expr(0),
3140                vec![(
3141                    compare(path_expr("x"), ComparisonComputation::IsNot, num_expr(5)),
3142                    num_expr(1),
3143                )],
3144            );
3145            assert_eq!(table(&expr).1, vec!["1", "0", "1"]);
3146        }
3147
3148        #[test]
3149        fn overlapping_ordering_arms_resolve_by_arm_order() {
3150            // `x > 5 then 1` is shadowed above 10 by the later `x > 10 then 2`.
3151            let expr = chain(
3152                num_expr(0),
3153                vec![
3154                    (
3155                        compare(
3156                            path_expr("x"),
3157                            ComparisonComputation::GreaterThan,
3158                            num_expr(5),
3159                        ),
3160                        num_expr(1),
3161                    ),
3162                    (
3163                        compare(
3164                            path_expr("x"),
3165                            ComparisonComputation::GreaterThan,
3166                            num_expr(10),
3167                        ),
3168                        num_expr(2),
3169                    ),
3170                ],
3171            );
3172            let (boundaries, regions) = table(&expr);
3173            assert_eq!(boundaries.len(), 2);
3174            assert_eq!(regions.len(), region_count(2));
3175            // below 5 | at 5 | between | at 10 | above 10
3176            assert_eq!(regions, vec!["0", "0", "1", "1", "2"]);
3177        }
3178
3179        #[test]
3180        fn literal_on_the_left_mirrors_the_operator() {
3181            let expr = chain(
3182                num_expr(0),
3183                vec![(
3184                    compare(num_expr(5), ComparisonComputation::LessThan, path_expr("x")),
3185                    num_expr(1),
3186                )],
3187            );
3188            // `5 < x` is `x > 5`: only the region above the boundary.
3189            assert_eq!(table(&expr).1, vec!["0", "0", "1"]);
3190        }
3191
3192        #[test]
3193        fn dates_fold_with_ordering_operators() {
3194            let day = |year: i32| {
3195                Ex::leaf(
3196                    ExpressionKind::Literal(Box::new(TypedLiteral::date(
3197                        crate::planning::semantics::SemanticDateTime {
3198                            year,
3199                            month: 1,
3200                            day: 1,
3201                            hour: 0,
3202                            minute: 0,
3203                            second: 0,
3204                            microsecond: 0,
3205                            timezone: None,
3206                        },
3207                    ))),
3208                    None,
3209                )
3210            };
3211            let expr = chain(
3212                num_expr(0),
3213                vec![(
3214                    compare(
3215                        path_expr("day"),
3216                        ComparisonComputation::GreaterThanOrEqual,
3217                        day(2020),
3218                    ),
3219                    num_expr(1),
3220                )],
3221            );
3222            let (boundaries, regions) = table(&expr);
3223            assert!(matches!(boundaries.as_slice(), [DispatchKey::Date(_)]));
3224            assert_eq!(regions, vec!["0", "1", "1"]);
3225        }
3226
3227        #[test]
3228        fn calendar_failure_literal_is_a_planning_error() {
3229            let invalid_day = Ex::leaf(
3230                ExpressionKind::Literal(Box::new(TypedLiteral::date(
3231                    crate::planning::semantics::SemanticDateTime {
3232                        year: 2020,
3233                        month: 13,
3234                        day: 1,
3235                        hour: 0,
3236                        minute: 0,
3237                        second: 0,
3238                        microsecond: 0,
3239                        timezone: None,
3240                    },
3241                ))),
3242                None,
3243            );
3244            let expr = chain(
3245                num_expr(0),
3246                vec![(
3247                    compare(path_expr("day"), ComparisonComputation::Is, invalid_day),
3248                    num_expr(1),
3249                )],
3250            );
3251            let data = scope();
3252            let mut interner = NormalFormInterner::new();
3253            let root = to_normal_form_with_data(&expr, &mut interner, &data);
3254            let unit_index = UnitIndex::new();
3255            let signature_index = SignatureIndex::new();
3256            let measure_scope = empty_measure_scope(&unit_index, &signature_index);
3257            let err = rewrite::normalize(root, Cells::new(&mut interner, measure_scope), None)
3258                .expect_err(
3259                    "invalid calendar literal must fail planning, not silently stay Piecewise",
3260                );
3261            let message = err.to_string();
3262            assert!(
3263                message.contains("ordered dispatch") && message.contains("Invalid date"),
3264                "unexpected error: {message}"
3265            );
3266        }
3267
3268        #[test]
3269        fn the_pre_image_is_kept_as_the_fold_origin() {
3270            let expr = chain(
3271                num_expr(0),
3272                vec![(
3273                    compare(path_expr("x"), ComparisonComputation::Is, num_expr(5)),
3274                    num_expr(1),
3275                )],
3276            );
3277            let (nf, interner) = fold(&expr);
3278            let origin = interner
3279                .get(nf)
3280                .origin
3281                .expect("dispatch must record its pre-image");
3282            assert!(
3283                matches!(interner.get(origin).kind, NormalFormKind::Piecewise(_)),
3284                "origin must be the Piecewise, got {:?}",
3285                interner.get(origin).kind
3286            );
3287        }
3288
3289        #[test]
3290        fn a_collapsed_arm_keeps_the_whole_origin_chain() {
3291            // The `1 < 0` arm is statically false, so `collapse_piecewise` folds first
3292            // and hands this pass a cell that already carries an origin.
3293            let expr = chain(
3294                num_expr(0),
3295                vec![
3296                    (lt_expr(num_expr(1), num_expr(0)), num_expr(9)),
3297                    (
3298                        compare(path_expr("x"), ComparisonComputation::Is, num_expr(5)),
3299                        num_expr(1),
3300                    ),
3301                ],
3302            );
3303            let (nf, interner) = fold(&expr);
3304            let origin = interner
3305                .get(nf)
3306                .origin
3307                .expect("dispatch must record its pre-image");
3308            assert!(
3309                matches!(interner.get(origin).kind, NormalFormKind::Piecewise(_)),
3310                "origin must be the Piecewise, got {:?}",
3311                interner.get(origin).kind
3312            );
3313            let recorded = interner
3314                .get(origin)
3315                .origin
3316                .expect("collapse origin must survive the second fold");
3317            let NormalFormKind::Piecewise(arms) = &interner.get(recorded).kind else {
3318                panic!("recorded pre-image must be a Piecewise");
3319            };
3320            assert_eq!(arms.len(), 3, "the dropped arm must still be recorded");
3321        }
3322
3323        #[test]
3324        fn text_declines_ordering_operators() {
3325            assert_declines(
3326                &chain(
3327                    num_expr(0),
3328                    vec![(
3329                        compare(
3330                            path_expr("code"),
3331                            ComparisonComputation::GreaterThan,
3332                            text_expr("NL"),
3333                        ),
3334                        num_expr(1),
3335                    )],
3336                ),
3337                "text has no ordering comparison",
3338            );
3339        }
3340
3341        #[test]
3342        fn arms_on_different_scrutinees_decline() {
3343            assert_declines(
3344                &chain(
3345                    num_expr(0),
3346                    vec![
3347                        (
3348                            compare(path_expr("x"), ComparisonComputation::Is, num_expr(1)),
3349                            num_expr(1),
3350                        ),
3351                        (
3352                            compare(path_expr("y"), ComparisonComputation::Is, num_expr(2)),
3353                            num_expr(2),
3354                        ),
3355                    ],
3356                ),
3357                "two scrutinees",
3358            );
3359        }
3360
3361        #[test]
3362        fn equality_or_of_is_comparisons_declines() {
3363            assert_declines(
3364                &chain(
3365                    num_expr(0),
3366                    vec![(
3367                        or_expr(
3368                            compare(path_expr("x"), ComparisonComputation::Is, num_expr(1)),
3369                            compare(path_expr("y"), ComparisonComputation::Is, num_expr(2)),
3370                        ),
3371                        num_expr(1),
3372                    )],
3373                ),
3374                "or of equality comparisons is not an And peel",
3375            );
3376        }
3377
3378        #[test]
3379        fn boolean_scrutinee_declines() {
3380            assert_declines(
3381                &chain(
3382                    num_expr(0),
3383                    vec![(
3384                        compare(
3385                            path_expr("flag"),
3386                            ComparisonComputation::Is,
3387                            bool_expr(true),
3388                        ),
3389                        num_expr(1),
3390                    )],
3391                ),
3392                "boolean has at most two keys",
3393            );
3394        }
3395
3396        #[test]
3397        fn mismatched_key_and_scrutinee_types_decline() {
3398            assert_declines(
3399                &chain(
3400                    num_expr(0),
3401                    vec![(
3402                        compare(path_expr("code"), ComparisonComputation::Is, num_expr(1)),
3403                        num_expr(1),
3404                    )],
3405                ),
3406                "text scrutinee against a number key",
3407            );
3408            assert_declines(
3409                &chain(
3410                    num_expr(0),
3411                    vec![(
3412                        compare(path_expr("rate"), ComparisonComputation::Is, num_expr(1)),
3413                        num_expr(1),
3414                    )],
3415                ),
3416                "ratio scrutinee against a plain number key",
3417            );
3418        }
3419
3420        #[test]
3421        fn a_determined_expression_scrutinee_folds() {
3422            // Stamped result_type on the Sum is Number; that is enough to classify.
3423            let expr = chain(
3424                num_expr(0),
3425                vec![(
3426                    compare(
3427                        add_expr(path_expr("x"), num_expr(1)),
3428                        ComparisonComputation::Is,
3429                        num_expr(5),
3430                    ),
3431                    num_expr(1),
3432                )],
3433            );
3434            let (boundaries, regions) = table(&expr);
3435            assert_eq!(boundaries.len(), 1);
3436            assert_eq!(regions, vec!["0", "1", "0"]);
3437        }
3438
3439        #[test]
3440        fn conjunctive_key_lut_folds_to_nested_dispatch() {
3441            let expr = chain(
3442                num_expr(0),
3443                vec![
3444                    (
3445                        and_expr(
3446                            compare(path_expr("x"), ComparisonComputation::Is, num_expr(2)),
3447                            compare(
3448                                path_expr("y"),
3449                                ComparisonComputation::GreaterThanOrEqual,
3450                                num_expr(1),
3451                            ),
3452                        ),
3453                        num_expr(10),
3454                    ),
3455                    (
3456                        and_expr(
3457                            compare(path_expr("x"), ComparisonComputation::Is, num_expr(2)),
3458                            compare(
3459                                path_expr("y"),
3460                                ComparisonComputation::GreaterThanOrEqual,
3461                                num_expr(5),
3462                            ),
3463                        ),
3464                        num_expr(20),
3465                    ),
3466                    (
3467                        and_expr(
3468                            compare(path_expr("x"), ComparisonComputation::Is, num_expr(3)),
3469                            compare(
3470                                path_expr("y"),
3471                                ComparisonComputation::GreaterThanOrEqual,
3472                                num_expr(1),
3473                            ),
3474                        ),
3475                        num_expr(30),
3476                    ),
3477                ],
3478            );
3479            let (nf, interner) = fold(&expr);
3480            let NormalFormKind::OrderedDispatch {
3481                boundaries,
3482                regions,
3483                ..
3484            } = &interner.get(nf).kind
3485            else {
3486                panic!(
3487                    "expected outer OrderedDispatch, got {:?}",
3488                    interner.get(nf).kind
3489                );
3490            };
3491            assert_eq!(boundaries.len(), 2, "outer keys are the two zone values");
3492            // Point regions are nested dispatches; interval regions keep the default.
3493            let point_bodies: Vec<_> = regions
3494                .iter()
3495                .enumerate()
3496                .filter(|(index, _)| index % 2 == 1)
3497                .map(|(_, region)| &interner.get(region.cell()).kind)
3498                .collect();
3499            assert_eq!(point_bodies.len(), 2);
3500            for kind in &point_bodies {
3501                assert!(
3502                    matches!(kind, NormalFormKind::OrderedDispatch { .. }),
3503                    "point region must be an inner OrderedDispatch, got {kind:?}"
3504                );
3505            }
3506            for (index, region) in regions.iter().enumerate() {
3507                if index % 2 == 0 {
3508                    assert_eq!(
3509                        explanation_display(interner_forms(&interner), region.cell(), None),
3510                        "0",
3511                        "interval region must stay the default"
3512                    );
3513                }
3514            }
3515        }
3516
3517        #[test]
3518        fn conjunctive_key_without_shared_is_scrutinee_declines() {
3519            // Residuals only; no shared Is key across arms.
3520            assert_declines(
3521                &chain(
3522                    num_expr(0),
3523                    vec![
3524                        (
3525                            and_expr(
3526                                compare(
3527                                    path_expr("x"),
3528                                    ComparisonComputation::GreaterThan,
3529                                    num_expr(1),
3530                                ),
3531                                compare(
3532                                    path_expr("y"),
3533                                    ComparisonComputation::GreaterThan,
3534                                    num_expr(1),
3535                                ),
3536                            ),
3537                            num_expr(1),
3538                        ),
3539                        (
3540                            and_expr(
3541                                compare(
3542                                    path_expr("x"),
3543                                    ComparisonComputation::GreaterThan,
3544                                    num_expr(2),
3545                                ),
3546                                compare(
3547                                    path_expr("y"),
3548                                    ComparisonComputation::GreaterThan,
3549                                    num_expr(2),
3550                                ),
3551                            ),
3552                            num_expr(2),
3553                        ),
3554                    ],
3555                ),
3556                "no Is key conjunct shared across arms",
3557            );
3558        }
3559
3560        #[test]
3561        fn an_unknown_data_path_declines() {
3562            assert_declines(
3563                &chain(
3564                    num_expr(0),
3565                    vec![(
3566                        compare(path_expr("absent"), ComparisonComputation::Is, num_expr(5)),
3567                        num_expr(1),
3568                    )],
3569                ),
3570                "the scrutinee has no resolved type in scope",
3571            );
3572        }
3573
3574        #[test]
3575        fn a_non_comparison_condition_declines() {
3576            assert_declines(
3577                &chain(num_expr(0), vec![(path_expr("flag"), num_expr(1))]),
3578                "a bare boolean condition is not a comparison",
3579            );
3580        }
3581
3582        #[test]
3583        fn a_comparison_between_two_data_paths_declines() {
3584            assert_declines(
3585                &chain(
3586                    num_expr(0),
3587                    vec![(
3588                        compare(path_expr("x"), ComparisonComputation::Is, path_expr("y")),
3589                        num_expr(1),
3590                    )],
3591                ),
3592                "neither side is a constant",
3593            );
3594        }
3595
3596        /// Every folded cell must agree with its pre-image for every input, which is
3597        /// the property the whole region model exists to preserve.
3598        #[test]
3599        fn folded_table_agrees_with_the_piecewise_it_replaced() {
3600            let operators = [
3601                ComparisonComputation::Is,
3602                ComparisonComputation::IsNot,
3603                ComparisonComputation::GreaterThan,
3604                ComparisonComputation::GreaterThanOrEqual,
3605                ComparisonComputation::LessThan,
3606                ComparisonComputation::LessThanOrEqual,
3607            ];
3608            // Duplicated constants and out-of-order constants are both covered.
3609            let constants = [5i64, 2, 5, 8];
3610            let mut checked = 0usize;
3611
3612            for first in &operators {
3613                for second in &operators {
3614                    for third in &operators {
3615                        let arm_operators = [first, second, third];
3616                        let arms: Vec<(Ex, Ex)> = arm_operators
3617                            .iter()
3618                            .enumerate()
3619                            .map(|(index, operator)| {
3620                                (
3621                                    compare(
3622                                        path_expr("x"),
3623                                        (*operator).clone(),
3624                                        num_expr(constants[index]),
3625                                    ),
3626                                    num_expr(index as i64 + 1),
3627                                )
3628                            })
3629                            .collect();
3630                        let expr = chain(num_expr(0), arms);
3631                        let (boundaries, regions) = table(&expr);
3632
3633                        // Sample below, at and above every constant, plus far outside.
3634                        for probe in [-100i64, 1, 2, 3, 5, 6, 8, 9, 100] {
3635                            let expected = arm_operators
3636                                .iter()
3637                                .enumerate()
3638                                .rev()
3639                                .find_map(|(index, operator)| {
3640                                    predicate_holds(probe, operator, constants[index])
3641                                        .then_some(index as i64 + 1)
3642                                })
3643                                .unwrap_or(0);
3644                            let region = crate::planning::ordered_dispatch::region_for_value(
3645                                &boundaries,
3646                                &DispatchKey::Rational(rational_new(probe, 1)).as_probe(),
3647                            )
3648                            .expect("integers compare");
3649                            assert_eq!(
3650                                regions[region],
3651                                expected.to_string(),
3652                                "operators {first} / {second} / {third} disagree at x = {probe}"
3653                            );
3654                            checked += 1;
3655                        }
3656                    }
3657                }
3658            }
3659            assert_eq!(checked, 6 * 6 * 6 * 9);
3660        }
3661
3662        fn predicate_holds(value: i64, operator: &ComparisonComputation, constant: i64) -> bool {
3663            match operator {
3664                ComparisonComputation::Is => value == constant,
3665                ComparisonComputation::IsNot => value != constant,
3666                ComparisonComputation::GreaterThan => value > constant,
3667                ComparisonComputation::GreaterThanOrEqual => value >= constant,
3668                ComparisonComputation::LessThan => value < constant,
3669                ComparisonComputation::LessThanOrEqual => value <= constant,
3670            }
3671        }
3672    }
3673
3674    #[test]
3675    fn flatten_associative_sum_nested() {
3676        let inner = add_expr(num_expr(1), num_expr(2));
3677        let expr = add_expr(inner, num_expr(3));
3678        let norm = normalize_expression(&expr).expect("normalize");
3679        assert!(matches!(
3680            norm.root_kind(),
3681            ExpressionKind::Literal(ref l)
3682                if matches!(l.value, crate::planning::semantics::ValueKind::Number(ref n) if n == &rational_new(6, 1))
3683        ));
3684    }
3685
3686    #[test]
3687    fn flatten_associative_product_nested() {
3688        let inner = mul_expr(num_expr(2), num_expr(3));
3689        let expr = mul_expr(inner, num_expr(4));
3690        let norm = normalize_expression(&expr).expect("normalize");
3691        assert!(matches!(
3692            norm.root_kind(),
3693            ExpressionKind::Literal(ref l)
3694                if matches!(l.value, crate::planning::semantics::ValueKind::Number(ref n) if n == &rational_new(24, 1))
3695        ));
3696    }
3697
3698    #[test]
3699    fn and_chain_lowers_to_one_flat_cell() {
3700        let chain = compose_expr(
3701            &[&bool_expr(true), &bool_expr(true), &bool_expr(false)],
3702            |ids| logical_kind(LogicalConnective::And, ids[0], vec![ids[1], ids[2]]),
3703        );
3704        let mut interner = NormalFormInterner::new();
3705        let id = to_normal_form_empty(&chain, &mut interner);
3706        let NormalFormKind::Logical(LogicalConnective::And, children) = &interner.get(id).kind
3707        else {
3708            panic!("expected And(..), got {:?}", interner.get(id).kind);
3709        };
3710        let [first, second, third] = children.as_slice() else {
3711            panic!("expected three flat operands, got {children:?}");
3712        };
3713        for operand in [first, second, third] {
3714            assert!(matches!(
3715                &interner.get(*operand).kind,
3716                NormalFormKind::Leaf(LeafKind::Literal(_))
3717            ));
3718        }
3719    }
3720
3721    #[test]
3722    fn parenthesized_sum_flattens_into_parent() {
3723        let y = Ex::leaf(
3724            ExpressionKind::DataPath(DataPath::new(vec![], "y".into())),
3725            None,
3726        );
3727        for expr in [
3728            add_expr(dx(), add_expr(y.clone(), num_expr(2))),
3729            add_expr(add_expr(dx(), y.clone()), num_expr(2)),
3730        ] {
3731            let norm = normalize_expression(&expr).expect("normalize");
3732            let ExpressionKind::Arithmetic { steps, .. } = norm.root_kind() else {
3733                panic!("expected one sum, got {:?}", norm.root_kind());
3734            };
3735            assert_eq!(steps.len(), 2, "expected three flat terms, got {norm:?}");
3736        }
3737    }
3738
3739    #[test]
3740    fn parenthesized_conjunction_flattens_into_parent() {
3741        let x_below_one = lt_expr(dx(), num_expr(1));
3742        for expr in [
3743            and_expr(dflag(), and_expr(dflag2(), x_below_one.clone())),
3744            and_expr(and_expr(dflag(), dflag2()), x_below_one.clone()),
3745        ] {
3746            let norm = normalize_expression(&expr).expect("normalize");
3747            let ExpressionKind::Logical {
3748                connective: LogicalConnective::And,
3749                steps,
3750                ..
3751            } = norm.root_kind()
3752            else {
3753                panic!("expected one conjunction, got {:?}", norm.root_kind());
3754            };
3755            assert_eq!(steps.len(), 2, "expected three flat operands, got {norm:?}");
3756        }
3757    }
3758
3759    #[test]
3760    fn or_chain_lowers_to_one_flat_cell() {
3761        let chain = compose_expr(
3762            &[&bool_expr(false), &bool_expr(false), &bool_expr(true)],
3763            |ids| logical_kind(LogicalConnective::Or, ids[0], vec![ids[1], ids[2]]),
3764        );
3765        let mut interner = NormalFormInterner::new();
3766        let id = to_normal_form_empty(&chain, &mut interner);
3767        let NormalFormKind::Logical(LogicalConnective::Or, children) = &interner.get(id).kind
3768        else {
3769            panic!("expected Or(..), got {:?}", interner.get(id).kind);
3770        };
3771        let [first, second, third] = children.as_slice() else {
3772            panic!("expected three flat operands, got {children:?}");
3773        };
3774        for operand in [first, second, third] {
3775            assert!(matches!(
3776                &interner.get(*operand).kind,
3777                NormalFormKind::Leaf(LeafKind::Literal(_))
3778            ));
3779        }
3780    }
3781
3782    #[test]
3783    fn parenthesized_disjunction_flattens_into_parent() {
3784        let x_below_one = lt_expr(dx(), num_expr(1));
3785        for expr in [
3786            or_expr(dflag(), or_expr(dflag2(), x_below_one.clone())),
3787            or_expr(or_expr(dflag(), dflag2()), x_below_one.clone()),
3788        ] {
3789            let norm = normalize_expression(&expr).expect("normalize");
3790            let ExpressionKind::Logical {
3791                connective: LogicalConnective::Or,
3792                steps,
3793                ..
3794            } = norm.root_kind()
3795            else {
3796                panic!("expected one disjunction, got {:?}", norm.root_kind());
3797            };
3798            assert_eq!(steps.len(), 2, "expected three flat operands, got {norm:?}");
3799        }
3800    }
3801
3802    #[test]
3803    fn mixed_or_and_does_not_flatten() {
3804        let expr = or_expr(dflag(), and_expr(dflag2(), lt_expr(dx(), num_expr(1))));
3805        let norm = normalize_expression(&expr).expect("normalize");
3806        let ExpressionKind::Logical {
3807            connective: LogicalConnective::Or,
3808            first,
3809            steps,
3810        } = norm.root_kind()
3811        else {
3812            panic!("expected or root, got {:?}", norm.root_kind());
3813        };
3814        assert_eq!(steps.len(), 1);
3815        assert!(matches!(
3816            norm.at(*first).kind(),
3817            ExpressionKind::DataPath(ref p) if p.data == "flag"
3818        ));
3819        assert!(matches!(
3820            norm.at(steps[0].operand).kind(),
3821            ExpressionKind::Logical {
3822                connective: LogicalConnective::And,
3823                ..
3824            }
3825        ));
3826    }
3827
3828    #[test]
3829    fn true_or_data_absorbs_to_true() {
3830        let norm = normalize_expression(&or_expr(bool_expr(true), dflag())).expect("normalize");
3831        assert!(matches!(
3832            norm.root_kind(),
3833            ExpressionKind::Literal(ref literal)
3834                if matches!(literal.value, crate::planning::semantics::ValueKind::Boolean(true))
3835        ));
3836    }
3837
3838    #[test]
3839    fn false_or_data_folds_to_data() {
3840        let norm = normalize_expression(&or_expr(bool_expr(false), dflag())).expect("normalize");
3841        assert!(matches!(
3842            norm.root_kind(),
3843            ExpressionKind::DataPath(ref path) if path.data == "flag"
3844        ));
3845    }
3846
3847    #[test]
3848    fn identity_add_zero() {
3849        let expr = add_expr(dx(), num_expr(0));
3850        let norm = normalize_expression(&expr).expect("normalize");
3851        assert!(matches!(norm.root_kind(), ExpressionKind::DataPath(ref p) if p.data == "x"));
3852    }
3853
3854    #[test]
3855    fn identity_mul_one() {
3856        let expr = mul_expr(dx(), num_expr(1));
3857        let norm = normalize_expression(&expr).expect("normalize");
3858        assert!(matches!(norm.root_kind(), ExpressionKind::DataPath(ref p) if p.data == "x"));
3859    }
3860
3861    #[test]
3862    fn identity_mul_zero_with_data_path_is_not_folded() {
3863        // `x * 0` must stay a runtime multiply: folding it to `0` would erase
3864        // a missing-data veto for `x` and drop `x` from the data manifest.
3865        let expr = mul_expr(dx(), num_expr(0));
3866        let norm = normalize_expression(&expr).expect("normalize");
3867        let Some((left, ArithmeticComputation::Multiply, right)) =
3868            binary_arithmetic(norm.root_kind())
3869        else {
3870            panic!("expected preserved multiply, got {:?}", norm.root_kind());
3871        };
3872        assert!(matches!(norm.at(*left).kind(), ExpressionKind::DataPath(ref p) if p.data == "x"));
3873        assert!(matches!(
3874            norm.at(*right).kind(),
3875            ExpressionKind::Literal(ref l)
3876                if matches!(l.value, crate::planning::semantics::ValueKind::Number(ref n) if n == &rational_new(0, 1))
3877        ));
3878    }
3879
3880    #[test]
3881    fn identity_mul_zero_with_literal_folds() {
3882        // All-literal products still fold: `7 * 0 → 0`.
3883        let expr = mul_expr(num_expr(7), num_expr(0));
3884        let norm = normalize_expression(&expr).expect("normalize");
3885        assert!(matches!(
3886            norm.root_kind(),
3887            ExpressionKind::Literal(ref l)
3888                if matches!(l.value, crate::planning::semantics::ValueKind::Number(ref n) if n == &rational_new(0, 1))
3889        ));
3890    }
3891
3892    #[test]
3893    fn identity_pow_one_and_zero() {
3894        let p1 = normalize_expression(&pow_expr(dx(), num_expr(1))).expect("normalize");
3895        assert!(matches!(p1.root_kind(), ExpressionKind::DataPath(ref p) if p.data == "x"));
3896        // `x ^ 0` must stay a runtime power: folding it to `1` would erase a
3897        // missing-data veto for `x`.
3898        let p0 = normalize_expression(&pow_expr(dx(), num_expr(0))).expect("normalize");
3899        let Some((base, ArithmeticComputation::Power, exp)) = binary_arithmetic(p0.root_kind())
3900        else {
3901            panic!("expected preserved power, got {:?}", p0.root_kind());
3902        };
3903        assert!(matches!(p0.at(*base).kind(), ExpressionKind::DataPath(ref p) if p.data == "x"));
3904        assert!(matches!(
3905            p0.at(*exp).kind(),
3906            ExpressionKind::Literal(ref l)
3907                if matches!(l.value, crate::planning::semantics::ValueKind::Number(ref n) if n == &rational_new(0, 1))
3908        ));
3909        // A total, nonzero literal base still folds: `7 ^ 0 → 1`.
3910        let literal_pow_zero =
3911            normalize_expression(&pow_expr(num_expr(7), num_expr(0))).expect("normalize");
3912        assert!(matches!(
3913            literal_pow_zero.root_kind(),
3914            ExpressionKind::Literal(ref l)
3915                if matches!(l.value, crate::planning::semantics::ValueKind::Number(ref n) if n == &rational_new(1, 1))
3916        ));
3917    }
3918
3919    #[test]
3920    fn double_logical_negation() {
3921        let norm = normalize_expression(&not_expr(not_expr(bool_expr(true)))).expect("normalize");
3922        assert!(matches!(
3923            norm.root_kind(),
3924            ExpressionKind::Literal(ref l) if matches!(l.value, crate::planning::semantics::ValueKind::Boolean(true))
3925        ));
3926    }
3927
3928    #[test]
3929    fn double_numeric_negation() {
3930        let zero = num_expr(0);
3931        let inner = sub_expr(zero.clone(), dx());
3932        let expr = sub_expr(zero, inner);
3933        let norm = normalize_expression(&expr).expect("normalize");
3934        assert!(matches!(norm.root_kind(), ExpressionKind::DataPath(ref p) if p.data == "x"));
3935    }
3936
3937    #[test]
3938    fn double_reciprocal_with_data_path_is_not_collapsed() {
3939        // `1 / (1 / x)` must stay nested: collapsing it to `x` would erase
3940        // the inner division-by-zero veto for `x = 0`.
3941        let one = num_expr(1);
3942        let inner = div_expr(one.clone(), dx());
3943        let expr = div_expr(one, inner);
3944        let norm = normalize_expression(&expr).expect("normalize");
3945        let Some((_, ArithmeticComputation::Divide, outer_denominator)) =
3946            binary_arithmetic(norm.root_kind())
3947        else {
3948            panic!(
3949                "expected preserved outer division, got {:?}",
3950                norm.root_kind()
3951            );
3952        };
3953        let Some((_, ArithmeticComputation::Divide, inner_denominator)) =
3954            binary_arithmetic(norm.at(*outer_denominator).kind())
3955        else {
3956            panic!(
3957                "expected preserved inner division, got {:?}",
3958                norm.at(*outer_denominator).kind()
3959            );
3960        };
3961        assert!(
3962            matches!(norm.at(*outer_denominator).child(*inner_denominator).kind(), ExpressionKind::DataPath(ref p) if p.data == "x")
3963        );
3964    }
3965
3966    #[test]
3967    fn sqrt_squared_numeric_folds_to_exact_rational() {
3968        let sqrt2 = math_expr(MathematicalComputation::Sqrt, num_expr(2));
3969        let expr = pow_expr(sqrt2, num_expr(2));
3970        let norm = normalize_expression(&expr).expect("normalize");
3971        assert!(matches!(
3972            norm.root_kind(),
3973            ExpressionKind::Literal(ref l)
3974                if matches!(l.value, crate::planning::semantics::ValueKind::Number(ref n) if n == &rational_new(2, 1))
3975        ));
3976    }
3977
3978    /// `2^(1/2)` has no exact rational result: constant_fold must not substitute a decimal.
3979    #[test]
3980    fn literal_power_with_irrational_result_stays_power() {
3981        let half_lit = literal_from_folded_rational(rational_new(1, 2));
3982        let half = Ex::leaf(
3983            ExpressionKind::Literal(Box::new(TypedLiteral {
3984                value: half_lit.value,
3985                lemma_type: primitive_number_arc().clone(),
3986            })),
3987            None,
3988        );
3989        let expr = pow_expr(num_expr(2), half);
3990        let norm = normalize_expression(&expr).expect("normalize");
3991        assert!(
3992            matches!(
3993                binary_arithmetic(norm.root_kind()),
3994                Some((_, ArithmeticComputation::Power, _))
3995            ),
3996            "expected symbolic Power, got {:?}",
3997            norm.root_kind()
3998        );
3999    }
4000
4001    #[test]
4002    fn negated_conjunction_preserved_for_data_paths() {
4003        // Non-total conjunctions under negation must not be rewritten:
4004        // AND propagates every conjunct's veto, so algebraic rewrites change
4005        // veto observability.
4006        let expr = not_expr(and_expr(dflag(), dflag2()));
4007        let norm = normalize_expression(&expr).expect("normalize");
4008        let ExpressionKind::LogicalNegation(inner, _) = norm.root_kind() else {
4009            panic!("expected preserved negation, got {:?}", norm.root_kind());
4010        };
4011        assert!(
4012            matches!(
4013                norm.at(*inner).kind(),
4014                ExpressionKind::Logical {
4015                    connective: LogicalConnective::And,
4016                    ..
4017                }
4018            ),
4019            "expected preserved conjunction under the negation, got {:?}",
4020            norm.at(*inner).kind()
4021        );
4022    }
4023
4024    #[test]
4025    fn power_law_nested_power() {
4026        let expr = pow_expr(pow_expr(dx(), num_expr(2)), num_expr(3));
4027        let norm = normalize_expression(&expr).expect("normalize");
4028        assert!(matches!(
4029            binary_arithmetic(norm.root_kind()),
4030            Some((_, ArithmeticComputation::Power, _))
4031        ));
4032        let Some((base, ArithmeticComputation::Power, exp)) = binary_arithmetic(norm.root_kind())
4033        else {
4034            unreachable!();
4035        };
4036        assert!(matches!(
4037            norm.at(*base).kind(),
4038            ExpressionKind::DataPath(ref p) if p.data == "x"
4039        ));
4040        assert!(matches!(
4041            norm.at(*exp).kind(),
4042            ExpressionKind::Literal(ref l)
4043                if matches!(l.value, crate::planning::semantics::ValueKind::Number(ref n) if n == &rational_new(6, 1))
4044        ));
4045    }
4046
4047    #[test]
4048    fn power_law_like_base_product() {
4049        let expr = mul_expr(pow_expr(dx(), num_expr(2)), pow_expr(dx(), num_expr(3)));
4050        let norm = normalize_expression(&expr).expect("normalize");
4051        let Some((base, ArithmeticComputation::Power, exp)) = binary_arithmetic(norm.root_kind())
4052        else {
4053            panic!("expected single power, got {:?}", norm.root_kind());
4054        };
4055        assert!(matches!(norm.at(*base).kind(), ExpressionKind::DataPath(ref p) if p.data == "x"));
4056        assert!(matches!(
4057            norm.at(*exp).kind(),
4058            ExpressionKind::Literal(ref l)
4059                if matches!(l.value, crate::planning::semantics::ValueKind::Number(ref n) if n == &rational_new(5, 1))
4060        ));
4061    }
4062
4063    #[test]
4064    fn negated_comparison_not_less_than() {
4065        let cmp = lt_expr(dx(), num_expr(0));
4066        let norm = normalize_expression(&not_expr(cmp)).expect("normalize");
4067        let ExpressionKind::Comparison(_, op, _) = norm.root_kind() else {
4068            panic!("expected Comparison, got {:?}", norm.root_kind());
4069        };
4070        assert_eq!(*op, ComparisonComputation::GreaterThanOrEqual);
4071    }
4072
4073    #[test]
4074    fn logical_short_circuit_and_false_with_data_path_folds() {
4075        // A `false` operand decides `and` whatever the other operands are, so
4076        // `false and flag` folds to `false`.
4077        let expr = and_expr(bool_expr(false), dflag());
4078        let norm = normalize_expression(&expr).expect("normalize");
4079        assert!(matches!(
4080            norm.root_kind(),
4081            ExpressionKind::Literal(ref l) if matches!(l.value, crate::planning::semantics::ValueKind::Boolean(false))
4082        ));
4083    }
4084
4085    #[test]
4086    fn logical_short_circuit_and_false_all_literal_folds() {
4087        let expr = and_expr(bool_expr(false), bool_expr(true));
4088        let norm = normalize_expression(&expr).expect("normalize");
4089        assert!(matches!(
4090            norm.root_kind(),
4091            ExpressionKind::Literal(ref l) if matches!(l.value, crate::planning::semantics::ValueKind::Boolean(false))
4092        ));
4093    }
4094
4095    #[test]
4096    fn logical_and_false_on_right_with_data_path_folds() {
4097        let expr = and_expr(dflag(), bool_expr(false));
4098        let norm = normalize_expression(&expr).expect("normalize");
4099        assert!(matches!(
4100            norm.root_kind(),
4101            ExpressionKind::Literal(ref l) if matches!(l.value, crate::planning::semantics::ValueKind::Boolean(false))
4102        ));
4103    }
4104
4105    #[test]
4106    fn logical_idempotency_and_duplicate_paths() {
4107        let a = dflag();
4108        let expr = and_expr(a.clone(), a);
4109        let norm = normalize_expression(&expr).expect("normalize");
4110        assert!(matches!(norm.root_kind(), ExpressionKind::DataPath(ref p) if p.data == "flag"));
4111    }
4112
4113    #[test]
4114    fn unit_conversion_fold_number_to_number() {
4115        let inner = num_expr(42);
4116        let expr = compose_expr(&[&inner], |ids| {
4117            ExpressionKind::UnitConversion(
4118                ids[0],
4119                SemanticConversionTarget::Type(ConversionTypeKind::Number),
4120            )
4121        });
4122        let norm = normalize_expression(&expr).expect("normalize");
4123        assert!(matches!(
4124            norm.root_kind(),
4125            ExpressionKind::Literal(ref l)
4126                if matches!(l.value, crate::planning::semantics::ValueKind::Number(ref n) if n == &rational_new(42, 1))
4127        ));
4128    }
4129
4130    #[test]
4131    fn piecewise_collapse_last_true_unless_keeps_only_that_result() {
4132        let piecewise = unless_chain(num_expr(0), vec![(bool_expr(true), num_expr(42))]);
4133        let norm = normalize_expression(&piecewise).expect("normalize");
4134        assert!(matches!(
4135            norm.root_kind(),
4136            ExpressionKind::Literal(ref l)
4137                if matches!(l.value, ValueKind::Number(ref n) if n == &rational_new(42, 1))
4138        ));
4139    }
4140
4141    #[test]
4142    fn collapsed_unless_false_retains_piecewise_origin() {
4143        let piecewise = unless_chain(num_expr(1), vec![(bool_expr(false), num_expr(2))]).tree();
4144        let mut interner = NormalFormInterner::new();
4145        let completed_rules = HashMap::new();
4146        let rule_types = HashMap::new();
4147        let data = IndexMap::new();
4148        let unit_index = UnitIndex::new();
4149        let signature_index = SignatureIndex::new();
4150        let measure_scope = empty_measure_scope(&unit_index, &signature_index);
4151        let lower = LowerCtx {
4152            completed_rules: &completed_rules,
4153            rule_types: &rule_types,
4154            data: &data,
4155            row_templates: &EMPTY_ROW_TEMPLATES,
4156            self_identity: &TEST_SELF_IDENTITY,
4157            rule_order: &[],
4158        };
4159        let root = to_normal_form(
4160            &piecewise,
4161            &mut Cells::new(&mut interner, measure_scope),
4162            &lower,
4163        );
4164        let nf = rewrite::normalize(root, Cells::new(&mut interner, measure_scope), None)
4165            .expect("normalize");
4166        let cell = interner.get(nf);
4167        assert!(
4168            cell.origin.is_some(),
4169            "collapsed unless must keep piecewise origin, kind={:?}",
4170            cell.kind
4171        );
4172        let origin = cell.origin.expect("origin");
4173        assert!(
4174            matches!(interner.get(origin).kind, NormalFormKind::Piecewise(_)),
4175            "origin must be piecewise, got {:?}",
4176            interner.get(origin).kind
4177        );
4178    }
4179
4180    #[test]
4181    fn piecewise_collapse_drops_arm_with_statically_false_condition() {
4182        let piecewise = unless_chain(
4183            num_expr(7),
4184            vec![(lt_expr(num_expr(1), num_expr(0)), num_expr(99))],
4185        );
4186        let norm = normalize_expression(&piecewise).expect("normalize");
4187        assert!(matches!(
4188            norm.root_kind(),
4189            ExpressionKind::Literal(ref l)
4190                if matches!(l.value, ValueKind::Number(ref n) if n == &rational_new(7, 1))
4191        ));
4192    }
4193
4194    #[test]
4195    fn constant_fold_divide_of_literals_to_number() {
4196        let expr = div_expr(num_expr(5), num_expr(2));
4197        let norm = normalize_expression(&expr).expect("normalize");
4198        assert!(matches!(
4199            norm.root_kind(),
4200            ExpressionKind::Literal(ref l)
4201                if matches!(l.value, ValueKind::Number(ref n) if n == &rational_new(5, 2))
4202        ));
4203    }
4204
4205    #[test]
4206    fn constant_fold_divide_comparison_to_true() {
4207        let expr = lt_expr(div_expr(num_expr(5), num_expr(2)), num_expr(4));
4208        let norm = normalize_expression(&expr).expect("normalize");
4209        assert!(matches!(
4210            norm.root_kind(),
4211            ExpressionKind::Literal(ref l) if matches!(l.value, ValueKind::Boolean(true))
4212        ));
4213    }
4214
4215    #[test]
4216    fn constant_fold_nested_arithmetic_comparison_to_true() {
4217        // (1 + 2) * 3 / 4 < 10
4218        let expr = lt_expr(
4219            div_expr(
4220                mul_expr(add_expr(num_expr(1), num_expr(2)), num_expr(3)),
4221                num_expr(4),
4222            ),
4223            num_expr(10),
4224        );
4225        let norm = normalize_expression(&expr).expect("normalize");
4226        assert!(matches!(
4227            norm.root_kind(),
4228            ExpressionKind::Literal(ref l) if matches!(l.value, ValueKind::Boolean(true))
4229        ));
4230    }
4231
4232    #[test]
4233    fn power_laws_sqrt_squared_is_not_merged_for_data_paths() {
4234        let x = Ex::leaf(
4235            ExpressionKind::DataPath(DataPath::new(vec![], "x".into())),
4236            None,
4237        );
4238        let expr = mul_expr(
4239            math_expr(MathematicalComputation::Sqrt, x.clone()),
4240            math_expr(MathematicalComputation::Sqrt, x),
4241        );
4242        let norm = normalize_expression(&expr).expect("normalize");
4243        let mut interner = NormalFormInterner::new();
4244        let id = lower_with_data(&norm, &mut interner, &algebraic_scope());
4245        assert!(
4246            matches!(&interner.get(id).kind, NormalFormKind::Product(ref children) if children.len() == 2),
4247            "sqrt(x)*sqrt(x) must stay a product of two powers, got {:?}",
4248            interner.get(id).kind
4249        );
4250    }
4251
4252    /// `sqrt_two * sqrt_two` (each factor inlined with `rule_origin = sqrt_two`)
4253    /// folds to `Leaf(2)` and records the full fold chain as provenance:
4254    #[test]
4255    fn exp_log_not_folded_for_data_path() {
4256        // `exp (log x)` must stay nested: folding it to `x` would erase the
4257        // `log` domain veto for `x <= 0`.
4258        let inner = math_expr(MathematicalComputation::Log, dx());
4259        let expr = math_expr(MathematicalComputation::Exp, inner);
4260        let norm = normalize_expression(&expr).expect("normalize");
4261        let ExpressionKind::MathematicalComputation(MathematicalComputation::Exp, inner) =
4262            norm.root_kind()
4263        else {
4264            panic!("expected preserved exp, got {:?}", norm.root_kind());
4265        };
4266        assert!(
4267            matches!(
4268                norm.at(*inner).kind(),
4269                ExpressionKind::MathematicalComputation(MathematicalComputation::Log, _)
4270            ),
4271            "expected preserved log under exp, got {:?}",
4272            norm.at(*inner).kind()
4273        );
4274    }
4275
4276    #[test]
4277
4278    fn constant_fold_add() {
4279        let expr = add_expr(num_expr(2), num_expr(3));
4280        let norm = normalize_expression(&expr).expect("normalize");
4281        assert!(matches!(
4282            norm.root_kind(),
4283            ExpressionKind::Literal(ref l) if matches!(l.value, crate::planning::semantics::ValueKind::Number(ref n) if n == &rational_new(5, 1))
4284        ));
4285    }
4286
4287    /// `i.sub.x` on a `uses` instance is one member read: the `uses` slot leaf, then a
4288    /// row end per hop through the templates, no cell of the dependency in this plan.
4289    #[test]
4290    fn member_read_on_import_lowers_to_row_ends() {
4291        let mut engine = crate::Engine::new();
4292        engine
4293            .load([(
4294                crate::SourceType::Path(Arc::new(std::path::PathBuf::from("member.lemma"))),
4295                r#"
4296spec leaf
4297data x: 3
4298
4299spec mid
4300uses sub: leaf
4301
4302spec outer
4303uses i: mid
4304rule r: i.sub.x
4305"#
4306                .to_string(),
4307            )])
4308            .expect("load member fixture");
4309        let plan = engine
4310            .plans
4311            .get_plans(None, "outer")
4312            .and_then(|plans| plans.values().next())
4313            .expect("outer plan");
4314        let rule = plan
4315            .rules
4316            .get(&RulePath::new(Vec::new(), "r".to_string()))
4317            .expect("rule r");
4318        let NormalFormKind::SpecMember { base, row, .. } = &plan.normal_form(rule.normal_form).kind
4319        else {
4320            panic!(
4321                "`i.sub.x` lowers to a member read, got {:?}",
4322                plan.normal_form(rule.normal_form).kind
4323            );
4324        };
4325        assert!(matches!(
4326            &plan.normal_form(*base).kind,
4327            NormalFormKind::Leaf(LeafKind::DataPath(path)) if path.input_key() == "i"
4328        ));
4329        assert_eq!(
4330            row.ends,
4331            vec![
4332                RowEnd::Data(DataPath::local("sub".to_string())),
4333                RowEnd::Data(DataPath::local("x".to_string())),
4334            ]
4335        );
4336        assert!(plan.data.keys().all(|path| path.segments.is_empty()));
4337    }
4338}