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