Skip to main content

lemma/planning/
show_expression.rs

1//! Show rule graph: resolved branch expressions projected for [`crate::Engine::show`].
2//!
3//! Built once from [`crate::planning::graph::RuleNode`] branches during plan build.
4//! Not NormalForm and not evaluation narration.
5
6use crate::parsing::arena::{ExprId, ExprKind, ExprRef};
7use crate::parsing::ast::{
8    ArithmeticComputation, CalendarPeriodUnit, ComparisonComputation, ConversionTypeKind,
9    DateCalendarKind, DateRelativeKind, LogicalConnective, MathematicalComputation, RangeBound,
10    RoundingMode, VetoExpression,
11};
12use crate::planning::execution_plan::ExecutionPlan;
13use crate::planning::instance_list::{row_end_chain, RowEndView};
14use crate::planning::semantics::{
15    DataDefinition, DataPath, Expression, ExpressionKind, ListOp, PathSegment, RowEnd, RulePath,
16    SemanticConversionTarget, SpecIdentity, TypedLiteral,
17};
18use indexmap::IndexMap;
19use serde::{Deserialize, Serialize};
20use std::collections::{BTreeMap, HashSet};
21use std::sync::Arc;
22
23/// What projecting the rules of one plan reads: the reachable Show rules, and the
24/// plan's data and row templates, which name a member read on a `uses` import.
25pub(crate) struct ShowScope<'a> {
26    pub(crate) show_keys: &'a HashSet<RulePath>,
27    pub(crate) data: &'a IndexMap<DataPath, DataDefinition>,
28    pub(crate) templates: &'a BTreeMap<SpecIdentity, Arc<ExecutionPlan>>,
29}
30
31/// What a member read on a `uses` import names: a rule or data of the instance it
32/// lands on, by path composed from the import hops.
33enum ImportMember {
34    Rule(RulePath),
35    Data(DataPath),
36}
37
38impl ShowScope<'_> {
39    /// The member `path.name` names when `base` is a `uses` slot of the plan.
40    fn import_member(
41        &self,
42        base: &ExpressionKind,
43        path: &[String],
44        name: &str,
45    ) -> Option<ImportMember> {
46        let ExpressionKind::DataPath(slot) = base else {
47            return None;
48        };
49        let Some(DataDefinition::Import { row, .. }) = self.data.get(slot) else {
50            return None;
51        };
52        let template = self.templates.get(&row.identity).unwrap_or_else(|| {
53            panic!(
54                "BUG: `uses` slot '{slot}' has no template of '{}'",
55                row.identity
56            )
57        });
58        let parts: Vec<&str> = path
59            .iter()
60            .map(String::as_str)
61            .chain(std::iter::once(name))
62            .collect();
63        let chain = row_end_chain(
64            &row.identity,
65            RowEndView::Plan(template),
66            self.templates,
67            &parts,
68        )
69        .unwrap_or_else(|| {
70            panic!(
71                "BUG: member '{}' of `uses` slot '{slot}' planned but unresolved",
72                parts.join(".")
73            )
74        });
75        let mut segments = vec![PathSegment::import(&slot.data, &row.identity)];
76        for (hop, next) in chain.iter().zip(chain.iter().skip(1)) {
77            let RowEnd::Data(alias) = &hop.end else {
78                panic!(
79                    "BUG: a member chain hop is a `uses` slot, got {:?}",
80                    hop.end
81                );
82            };
83            segments.push(PathSegment::import(&alias.data, &next.identity));
84        }
85        let last = chain.last().expect("BUG: a member chain has a last end");
86        Some(match &last.end {
87            RowEnd::Rule(rule) => ImportMember::Rule(RulePath::new(segments, rule.rule.clone())),
88            RowEnd::Data(data) => ImportMember::Data(DataPath {
89                segments,
90                data: data.data.clone(),
91            }),
92        })
93    }
94}
95
96/// One arm of a rule's flat last-match table on Show.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct ShowBranch {
99    /// Absent on the default arm; present on each `unless`.
100    pub condition: Option<ShowExpression>,
101    pub result: ShowExpression,
102}
103
104/// Local rule on Show: result type, authored branches, stored topo deps.
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct ShowRule {
107    pub lemma_type: crate::planning::semantics::LemmaType,
108    /// Import hops to this rule (`[]` = this spec). Catalog entries are always empty.
109    pub path: Vec<crate::planning::semantics::PathSegment>,
110    pub branches: Vec<ShowBranch>,
111    /// Other Show.rules keys this rule depends on (local catalog closure). Always present.
112    /// Branch Rule leaves may name reachable imports outside this set.
113    pub depends_on_rules: Vec<RulePath>,
114}
115
116/// Conversion target on a Show `as` expression (no owning type — inspection only).
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub enum ShowConversionTarget {
119    Type(ConversionTypeKind),
120    Unit { unit_name: String },
121}
122
123/// Resolved expression tree for Show branches (`tag = "type"` on the API mirror).
124///
125/// Literals stay [`TypedLiteral`]. [`RuleResultValue`] is built at the
126/// JSON/`lemma::api` boundary only.
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub enum ShowExpression {
129    Literal(Box<TypedLiteral>),
130    Data {
131        name: String,
132    },
133    Rule {
134        name: String,
135    },
136    /// `a and b and c`: one left fold, operands in source order.
137    And {
138        operands: Vec<ShowExpression>,
139    },
140    Or {
141        operands: Vec<ShowExpression>,
142    },
143    Not {
144        operand: Box<ShowExpression>,
145    },
146    /// `a + b - c`: one left fold on one precedence level, operands in source order.
147    Arithmetic {
148        first: Box<ShowExpression>,
149        steps: Vec<ShowArithmeticStep>,
150    },
151    Comparison {
152        op: ComparisonComputation,
153        left: Box<ShowExpression>,
154        right: Box<ShowExpression>,
155    },
156    /// `x as a as b`: one node over the innermost operand, `targets` in application order.
157    UnitConversion {
158        operand: Box<ShowExpression>,
159        targets: Vec<ShowConversionTarget>,
160    },
161    Math {
162        op: MathematicalComputation,
163        operand: Box<ShowExpression>,
164    },
165    /// `operand -> round` or `operand -> round step`.
166    StepRounding {
167        mode: RoundingMode,
168        operand: Box<ShowExpression>,
169        step: Option<Box<ShowExpression>>,
170    },
171    Veto {
172        message: Option<String>,
173    },
174    Now,
175    DateRelative {
176        kind: DateRelativeKind,
177        operand: Box<ShowExpression>,
178    },
179    DateCalendar {
180        kind: DateCalendarKind,
181        unit: CalendarPeriodUnit,
182        operand: Box<ShowExpression>,
183    },
184    RangeLiteral {
185        from: Box<ShowExpression>,
186        to: Box<ShowExpression>,
187    },
188    PastFutureRange {
189        kind: DateRelativeKind,
190        operand: Box<ShowExpression>,
191    },
192    RangeContainment {
193        value: Box<ShowExpression>,
194        range: Box<ShowExpression>,
195    },
196    RangeBound {
197        bound: RangeBound,
198        operand: Box<ShowExpression>,
199    },
200    IsVeto {
201        operand: Box<ShowExpression>,
202    },
203    /// `rule.field` on a spec-valued rule. `member` is `field` or `alias.field`.
204    SpecMember {
205        base: String,
206        member: String,
207    },
208    /// `(expression).field` on a spec value that is not a rule reference.
209    Member {
210        base: Box<ShowExpression>,
211        member: String,
212    },
213    /// Field of the current row inside an instance-list command argument.
214    RowField {
215        field: String,
216        /// Written list qualifier when the source used `list.field`; `None` for bare refs.
217        qualifier: Option<Vec<String>>,
218    },
219    /// The current row's value: the implicit left operand of a value-list condition.
220    RowValue,
221    /// `field: value` of a row appended by `-> with`.
222    RowFieldValue {
223        field: String,
224        value: Box<ShowExpression>,
225    },
226    /// `direction` is set exactly when `command` is [`ShowListCommand::Sort`].
227    ListCommand {
228        list: Box<ShowExpression>,
229        command: ShowListCommand,
230        direction: Option<SortDirection>,
231        args: Vec<ShowExpression>,
232    },
233}
234
235/// One operator step of a Show arithmetic chain.
236#[derive(Debug, Clone, PartialEq, Eq)]
237pub struct ShowArithmeticStep {
238    pub op: ArithmeticComputation,
239    pub operand: ShowExpression,
240}
241
242/// Postfix list command on Show.
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
244#[serde(rename_all = "snake_case")]
245pub enum ShowListCommand {
246    Find,
247    First,
248    Last,
249    Count,
250    Sum,
251    Avg,
252    Product,
253    Min,
254    Max,
255    Any,
256    All,
257    Where,
258    Sort,
259    Top,
260    With,
261    Key,
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
265#[serde(rename_all = "snake_case")]
266pub enum SortDirection {
267    Asc,
268    Desc,
269}
270
271impl ShowExpression {
272    /// Direct sub-expressions in the order [`Self::fold`] hands their results back.
273    pub(crate) fn operands(&self) -> Vec<&ShowExpression> {
274        match self {
275            Self::Literal(_)
276            | Self::Data { .. }
277            | Self::Rule { .. }
278            | Self::Veto { .. }
279            | Self::Now
280            | Self::SpecMember { .. }
281            | Self::RowField { .. }
282            | Self::RowValue => Vec::new(),
283            Self::And { operands } | Self::Or { operands } => operands.iter().collect(),
284            Self::Not { operand }
285            | Self::UnitConversion { operand, .. }
286            | Self::Math { operand, .. }
287            | Self::DateRelative { operand, .. }
288            | Self::DateCalendar { operand, .. }
289            | Self::PastFutureRange { operand, .. }
290            | Self::RangeBound { operand, .. }
291            | Self::IsVeto { operand } => vec![operand.as_ref()],
292            Self::Arithmetic { first, steps } => {
293                let mut out = vec![first.as_ref()];
294                out.extend(steps.iter().map(|step| &step.operand));
295                out
296            }
297            Self::Comparison { left, right, .. }
298            | Self::RangeLiteral {
299                from: left,
300                to: right,
301            }
302            | Self::RangeContainment {
303                value: left,
304                range: right,
305            } => vec![left.as_ref(), right.as_ref()],
306            Self::StepRounding { operand, step, .. } => {
307                let mut out = vec![operand.as_ref()];
308                if let Some(step) = step {
309                    out.push(step.as_ref());
310                }
311                out
312            }
313            Self::Member { base, .. } => vec![base.as_ref()],
314            Self::RowFieldValue { value, .. } => vec![value.as_ref()],
315            Self::ListCommand { list, args, .. } => {
316                let mut out = vec![list.as_ref()];
317                out.extend(args.iter());
318                out
319            }
320        }
321    }
322
323    /// Bottom-up fold with an explicit stack. `step` receives each node with its operands'
324    /// results in [`Self::operands`] order.
325    pub(crate) fn fold<U>(&self, mut step: impl FnMut(&ShowExpression, Vec<U>) -> U) -> U {
326        let mut values: Vec<U> = Vec::new();
327        let mut stack: Vec<(&ShowExpression, bool)> = vec![(self, false)];
328        while let Some((node, expanded)) = stack.pop() {
329            let operands = node.operands();
330            if !expanded {
331                stack.push((node, true));
332                stack.extend(operands.iter().rev().map(|&operand| (operand, false)));
333                continue;
334            }
335            let split = values
336                .len()
337                .checked_sub(operands.len())
338                .expect("BUG: operand folded before its parent");
339            let args = values.split_off(split);
340            values.push(step(node, args));
341        }
342        values
343            .pop()
344            .expect("BUG: fold produced no value for its root")
345    }
346}
347
348/// Next operand from a parent's operand list, in [`ShowExpression::operands`] order.
349pub(crate) fn take_operand<T, I: Iterator<Item = T>>(operands: &mut I) -> T {
350    operands
351        .next()
352        .expect("BUG: operand missing for its parent node")
353}
354
355/// Project the subtree at `root` into the Show tree.
356///
357/// `show_keys` are reachable Show rules. A `rule` leaf whose path is missing from that set is a bug.
358///
359/// Bottom-up projection with an explicit stack. Each node takes its operands' projections
360/// from the value stack in [`for_each_projection_operand`] order.
361fn project(
362    root: ExprRef<'_, ExpressionKind>,
363    scope: &ShowScope<'_>,
364    scratch: &mut ProjectScratch,
365) -> ShowExpression {
366    let ProjectScratch { values, stack } = scratch;
367    stack.push((root.id, false));
368    while let Some((id, expanded)) = stack.pop() {
369        let node = root.child(id);
370        if !expanded {
371            stack.push((id, true));
372            let start = stack.len();
373            for_each_projection_operand(node, scope, |operand| stack.push((operand, false)));
374            stack[start..].reverse();
375            continue;
376        }
377        let mut arity = 0;
378        for_each_projection_operand(node, scope, |_| arity += 1);
379        let split = values
380            .len()
381            .checked_sub(arity)
382            .expect("BUG: operand projected before its parent");
383        let shown = project_node(node, values.drain(split..), scope);
384        values.push(shown);
385    }
386    let shown = values
387        .pop()
388        .expect("BUG: projection produced no value for its root");
389    assert!(
390        values.is_empty(),
391        "BUG: projection left operands without a parent"
392    );
393    shown
394}
395
396/// Stacks of [`project`], reused across calls. Empty between calls.
397#[derive(Default)]
398struct ProjectScratch {
399    values: Vec<ShowExpression>,
400    stack: Vec<(ExprId, bool)>,
401}
402
403/// Calls `f` on the sub-expressions a Show node is built from, in [`project_node`] consumption order.
404/// An `as` run contributes only its innermost operand; a rule-path or `uses` member
405/// base, none.
406fn for_each_projection_operand(
407    node: ExprRef<'_, ExpressionKind>,
408    scope: &ShowScope<'_>,
409    mut f: impl FnMut(ExprId),
410) {
411    match node.kind() {
412        ExpressionKind::Piecewise(_) => {}
413        ExpressionKind::SpecMember { base, path, name } => {
414            let base_kind = node.child(*base).kind();
415            if !matches!(base_kind, ExpressionKind::RulePath(_))
416                && scope.import_member(base_kind, path, name).is_none()
417            {
418                f(*base);
419            }
420        }
421        ExpressionKind::UnitConversion(_, _) => f(conversion_operand(node)),
422        other => other.for_each_child(f),
423    }
424}
425
426/// Innermost operand of the `as` run ending at `node`.
427fn conversion_operand(node: ExprRef<'_, ExpressionKind>) -> ExprId {
428    let mut current = node;
429    while let ExpressionKind::UnitConversion(operand, _) = current.kind() {
430        current = current.child(*operand);
431    }
432    current.id
433}
434
435/// Innermost operand and targets (application order) of the `as` run ending at `node`.
436fn conversion_run(node: ExprRef<'_, ExpressionKind>) -> (ExprId, Vec<ShowConversionTarget>) {
437    let mut current = node;
438    let mut targets = Vec::new();
439    while let ExpressionKind::UnitConversion(operand, target) = current.kind() {
440        targets.push(conversion_target_from(target));
441        current = current.child(*operand);
442    }
443    targets.reverse();
444    (current.id, targets)
445}
446
447/// Build one Show node from its already-projected operands.
448fn project_node(
449    node: ExprRef<'_, ExpressionKind>,
450    mut operands: std::vec::Drain<'_, ShowExpression>,
451    scope: &ShowScope<'_>,
452) -> ShowExpression {
453    let show_keys = scope.show_keys;
454    match node.kind() {
455        ExpressionKind::Literal(typed) => ShowExpression::Literal(Box::new(typed.as_ref().clone())),
456        ExpressionKind::DataPath(path) => ShowExpression::Data {
457            name: path.input_key(),
458        },
459        ExpressionKind::RulePath(path) => {
460            if !show_keys.contains(path) {
461                panic!(
462                    "BUG: rule leaf '{}' not in Show.rules keys",
463                    path.input_key()
464                );
465            }
466            ShowExpression::Rule {
467                name: path.input_key(),
468            }
469        }
470        ExpressionKind::Logical { connective, .. } => match connective {
471            LogicalConnective::And => ShowExpression::And {
472                operands: operands.collect(),
473            },
474            LogicalConnective::Or => ShowExpression::Or {
475                operands: operands.collect(),
476            },
477        },
478        ExpressionKind::LogicalNegation(_, _) => ShowExpression::Not {
479            operand: Box::new(take_operand(&mut operands)),
480        },
481        ExpressionKind::Arithmetic { steps, .. } => {
482            let first = Box::new(take_operand(&mut operands));
483            let steps = steps
484                .iter()
485                .map(|step| ShowArithmeticStep {
486                    op: step.op.clone(),
487                    operand: take_operand(&mut operands),
488                })
489                .collect();
490            ShowExpression::Arithmetic { first, steps }
491        }
492        ExpressionKind::Comparison(_, op, _) => {
493            let left = Box::new(take_operand(&mut operands));
494            let right = Box::new(take_operand(&mut operands));
495            ShowExpression::Comparison {
496                op: op.clone(),
497                left,
498                right,
499            }
500        }
501        ExpressionKind::UnitConversion(_, _) => {
502            let (_, targets) = conversion_run(node);
503            ShowExpression::UnitConversion {
504                operand: Box::new(take_operand(&mut operands)),
505                targets,
506            }
507        }
508        ExpressionKind::MathematicalComputation(op, _) => ShowExpression::Math {
509            op: op.clone(),
510            operand: Box::new(take_operand(&mut operands)),
511        },
512        ExpressionKind::StepRounding { mode, step, .. } => {
513            let operand = Box::new(take_operand(&mut operands));
514            ShowExpression::StepRounding {
515                mode: *mode,
516                operand,
517                step: step.map(|_| Box::new(take_operand(&mut operands))),
518            }
519        }
520        ExpressionKind::Veto(VetoExpression { message }) => ShowExpression::Veto {
521            message: message.clone(),
522        },
523        ExpressionKind::Now => ShowExpression::Now,
524        ExpressionKind::DateRelative(kind, _) => ShowExpression::DateRelative {
525            kind: *kind,
526            operand: Box::new(take_operand(&mut operands)),
527        },
528        ExpressionKind::DateCalendar(kind, unit, _) => ShowExpression::DateCalendar {
529            kind: *kind,
530            unit: *unit,
531            operand: Box::new(take_operand(&mut operands)),
532        },
533        ExpressionKind::RangeLiteral(_, _) => {
534            let from = Box::new(take_operand(&mut operands));
535            let to = Box::new(take_operand(&mut operands));
536            ShowExpression::RangeLiteral { from, to }
537        }
538        ExpressionKind::PastFutureRange(kind, _) => ShowExpression::PastFutureRange {
539            kind: *kind,
540            operand: Box::new(take_operand(&mut operands)),
541        },
542        ExpressionKind::RangeContainment(_, _) => {
543            let value = Box::new(take_operand(&mut operands));
544            let range = Box::new(take_operand(&mut operands));
545            ShowExpression::RangeContainment { value, range }
546        }
547        ExpressionKind::RangeBound(bound, _) => ShowExpression::RangeBound {
548            bound: *bound,
549            operand: Box::new(take_operand(&mut operands)),
550        },
551        ExpressionKind::ResultIsVeto(_) => ShowExpression::IsVeto {
552            operand: Box::new(take_operand(&mut operands)),
553        },
554        ExpressionKind::Piecewise(_) => {
555            panic!("BUG: piecewise inside a rule branch")
556        }
557        ExpressionKind::SpecMember { base, path, name } => {
558            match scope.import_member(node.child(*base).kind(), path, name) {
559                Some(ImportMember::Rule(rule)) => {
560                    if !show_keys.contains(&rule) {
561                        panic!(
562                            "BUG: `uses` member rule '{}' not in Show.rules keys",
563                            rule.input_key()
564                        );
565                    }
566                    return ShowExpression::Rule {
567                        name: rule.input_key(),
568                    };
569                }
570                Some(ImportMember::Data(data)) => {
571                    return ShowExpression::Data {
572                        name: data.input_key(),
573                    };
574                }
575                None => {}
576            }
577            let mut member = String::new();
578            for segment in path {
579                member.push_str(segment);
580                member.push('.');
581            }
582            member.push_str(name);
583            match node.child(*base).kind() {
584                ExpressionKind::RulePath(rule) => {
585                    if !show_keys.contains(rule) {
586                        panic!(
587                            "BUG: spec member base '{}' not in Show.rules keys",
588                            rule.input_key()
589                        );
590                    }
591                    ShowExpression::SpecMember {
592                        base: rule.input_key(),
593                        member,
594                    }
595                }
596                _ => ShowExpression::Member {
597                    base: Box::new(take_operand(&mut operands)),
598                    member,
599                },
600            }
601        }
602        ExpressionKind::RowField { end, qualifier, .. } => ShowExpression::RowField {
603            field: end.to_string(),
604            qualifier: qualifier.clone(),
605        },
606        ExpressionKind::RowValue { .. } => ShowExpression::RowValue,
607        ExpressionKind::ListCommand { op, .. } => {
608            let list = Box::new(take_operand(&mut operands));
609            let (command, direction, args) = show_list_op(op, &mut operands);
610            ShowExpression::ListCommand {
611                list,
612                command,
613                direction,
614                args,
615            }
616        }
617    }
618}
619
620/// Operand expression ids of a list op, in the order [`show_list_op`] consumes them.
621fn show_list_op(
622    op: &ListOp,
623    operands: &mut std::vec::Drain<'_, ShowExpression>,
624) -> (ShowListCommand, Option<SortDirection>, Vec<ShowExpression>) {
625    use crate::planning::semantics::{ListPredKind, ListReduce};
626    let predicate = |kind: &ListPredKind| match kind {
627        ListPredKind::Any => ShowListCommand::Any,
628        ListPredKind::All => ShowListCommand::All,
629        ListPredKind::Where => ShowListCommand::Where,
630    };
631    match op {
632        ListOp::Find { keys } => (
633            ShowListCommand::Find,
634            None,
635            operands.by_ref().take(keys.len()).collect(),
636        ),
637        ListOp::First => (ShowListCommand::First, None, Vec::new()),
638        ListOp::Last => (ShowListCommand::Last, None, Vec::new()),
639        ListOp::Count => (ShowListCommand::Count, None, Vec::new()),
640        ListOp::Reduce { kind, argument } => {
641            let command = match kind {
642                ListReduce::Sum => ShowListCommand::Sum,
643                ListReduce::Avg => ShowListCommand::Avg,
644                ListReduce::Product => ShowListCommand::Product,
645                ListReduce::Min => ShowListCommand::Min,
646                ListReduce::Max => ShowListCommand::Max,
647            };
648            let args = if argument.is_some() {
649                vec![take_operand(operands)]
650            } else {
651                Vec::new()
652            };
653            (command, None, args)
654        }
655        ListOp::Predicate { kind, .. } => (predicate(kind), None, vec![take_operand(operands)]),
656        ListOp::Sort {
657            descending,
658            argument,
659        } => {
660            let direction = if *descending {
661                SortDirection::Desc
662            } else {
663                SortDirection::Asc
664            };
665            let args = if argument.is_some() {
666                vec![take_operand(operands)]
667            } else {
668                Vec::new()
669            };
670            (ShowListCommand::Sort, Some(direction), args)
671        }
672        ListOp::Top { .. } => (ShowListCommand::Top, None, vec![take_operand(operands)]),
673        ListOp::With { rows } => (
674            ShowListCommand::With,
675            None,
676            operands.by_ref().take(rows.len()).collect(),
677        ),
678        ListOp::WithRow { fields, .. } => (
679            ShowListCommand::With,
680            None,
681            fields
682                .iter()
683                .map(|(path, _)| ShowExpression::RowFieldValue {
684                    field: path.input_key(),
685                    value: Box::new(take_operand(operands)),
686                })
687                .collect(),
688        ),
689        ListOp::Key { fields } => (
690            ShowListCommand::Key,
691            None,
692            fields
693                .iter()
694                .map(|path| ShowExpression::Data {
695                    name: path.input_key(),
696                })
697                .collect(),
698        ),
699    }
700}
701
702/// Project `RuleNode.branches` into Show arms (default first, no synthetic true).
703pub(crate) fn show_branches_from(
704    expression: &Expression,
705    branches: &[(Option<ExprId>, ExprId)],
706    scope: &ShowScope<'_>,
707) -> Vec<ShowBranch> {
708    let mut scratch = ProjectScratch::default();
709    branches
710        .iter()
711        .map(|(condition, result)| ShowBranch {
712            condition: condition
713                .map(|condition| project(expression.at(condition), scope, &mut scratch)),
714            result: project(expression.at(*result), scope, &mut scratch),
715        })
716        .collect()
717}
718
719/// Project planning edges onto the public Show.rules catalog (BTreeSet order).
720///
721/// Local deps must be catalog keys. Import deps (non-empty segments) are planning-internal
722/// and omitted from the public vector.
723pub fn project_depends_on_rules(
724    deps: &std::collections::BTreeSet<RulePath>,
725    catalog_keys: &HashSet<RulePath>,
726) -> Vec<RulePath> {
727    let mut out = Vec::new();
728    for path in deps {
729        if catalog_keys.contains(path) {
730            out.push(path.clone());
731        } else if path.segments.is_empty() {
732            panic!(
733                "BUG: local depends_on_rules entry '{}' missing from Show.rules keys",
734                path.input_key()
735            );
736        }
737    }
738    out
739}
740
741fn conversion_target_from(target: &SemanticConversionTarget) -> ShowConversionTarget {
742    match target {
743        SemanticConversionTarget::Type(kind) => ShowConversionTarget::Type(*kind),
744        SemanticConversionTarget::Unit { unit_name, .. } => ShowConversionTarget::Unit {
745            unit_name: unit_name.clone(),
746        },
747    }
748}