improv_engine/plan.rs
1//! The operator plan: a lowered, dimension-explicit representation of a formula
2//! that maps directly onto differential-dataflow operators.
3//!
4//! Produced by the compiler's second phase (`compiler::build_plan`) and
5//! consumed by the dataflow builder (`dataflow::build_collection`).
6
7use crate::typed::TypeInfo;
8use improv_core_model::{BinaryOp, CategoryId, FuncId, ItemId, MeasureId, UnaryOp, Value};
9
10#[derive(Debug, Clone, PartialEq)]
11pub enum PlanNodeKind {
12 /// The base collection for an input measure.
13 InputMeasure(MeasureId),
14 /// A constant, broadcast over the target dimension.
15 Literal(Value),
16 MapUnary(UnaryOp, Box<PlanNode>),
17 /// Element-wise binary op. Operands are assumed dimension-aligned (the
18 /// compiler inserts `Join` nodes to align them first).
19 MapBinary(BinaryOp, Box<PlanNode>, Box<PlanNode>),
20 /// Align two collections on shared categories (broadcast the smaller over
21 /// the larger). Result dimension is the union.
22 Join {
23 left: Box<PlanNode>,
24 right: Box<PlanNode>,
25 /// Categories both sides are joined on.
26 join_keys: Vec<CategoryId>,
27 },
28 /// Reduce `inputs` (aligned on their full coordinate; `pred` filters
29 /// them) down to `group_by`. `params` broadcast one value per group.
30 /// Rows reach the reduction in item order along `order` (the reduced
31 /// categories). With a `window` (one category, kept), one result per
32 /// item, each over its window; a [`improv_core_model::functions::Kind::PerItem`]
33 /// function also keeps the category.
34 Aggregate {
35 inputs: Vec<PlanNode>,
36 params: Vec<PlanNode>,
37 pred: Option<Box<PlanNode>>,
38 group_by: Vec<CategoryId>,
39 func: FuncId,
40 order: Vec<(CategoryId, Vec<ItemId>)>,
41 window: Option<crate::typed::Window>,
42 per_item: bool,
43 /// An array function's landing categories and their items: results
44 /// fill them row-major (the last category varies fastest); results
45 /// past the last cell are dropped. Empty: not a list.
46 into: Vec<(CategoryId, Vec<ItemId>)>,
47 },
48 /// A static per-item collection of a category: one cell per item.
49 Items(CategoryId, Vec<(ItemId, Value)>),
50 /// A recurrence along `cat`: per group of the other categories, items in
51 /// `order`, each cell is `body` evaluated (by the scalar evaluator) with
52 /// `refs` at its coordinate and each slot's placeholder bound to this
53 /// measure's own value `back` items earlier (or the slot's default).
54 Recurrence {
55 body: improv_core_model::Expr,
56 refs: Vec<(MeasureId, PlanNode)>,
57 slots: Vec<(MeasureId, i64, Option<improv_core_model::Expr>)>,
58 cat: CategoryId,
59 order: Vec<ItemId>,
60 },
61 /// An outer union: at each coordinate of the domain, the first of `args`
62 /// with a cell there. The domain is every item combination of `grid`
63 /// (`FILL`), else the cells of the arguments whose dimension is the
64 /// result's. An argument over fewer categories broadcasts onto the
65 /// domain; it never adds coordinates of its own.
66 Coalesce {
67 args: Vec<PlanNode>,
68 grid: Option<Vec<(CategoryId, Vec<ItemId>)>>,
69 },
70 /// `input` with its categories renamed (`RELABEL`, `SWAP`): per
71 /// `(from, to, items)`, a cell's `from` item becomes `to`'s item it maps
72 /// to; a cell whose item has no mapping is dropped. The maps apply at
73 /// once, so two maps exchange two categories.
74 Remap {
75 input: Box<PlanNode>,
76 maps: Remaps,
77 },
78 /// `input` moved `offset` items along `cat` (in `order`); where no cell
79 /// lands, `default` (when given) at the input's own coordinates.
80 Shift {
81 input: Box<PlanNode>,
82 cat: CategoryId,
83 offset: i64,
84 order: Vec<ItemId>,
85 default: Option<Box<PlanNode>>,
86 },
87 /// A (non-aggregating) function call over its argument plans.
88 FuncCall {
89 func: FuncId,
90 args: Vec<PlanNode>,
91 },
92 /// `FIXPOINT(init, LAMBDA(r, body), max_iter, tolerance)`, a measure's
93 /// whole formula: `init` and `body` as written (the body reads r as
94 /// `functions::lambda_slot(0, 0)`). Not a dataflow operator: the engine
95 /// (`session::Engine`) solves it host-side, round by round.
96 Fixpoint {
97 init: improv_core_model::Expr,
98 body: improv_core_model::Expr,
99 max_iter: u32,
100 tolerance: f64,
101 },
102 /// `IF(test, then, [otherwise])`, evaluated lazily: a cell where the test
103 /// is true and `then` has one, where it is false and `otherwise` has one,
104 /// and the test's error where it is an error. `spanning` names the branch
105 /// (0 then, 1 otherwise) whose coordinates extend the test to the
106 /// result's categories; `None` when the test already spans them.
107 If {
108 test: Box<PlanNode>,
109 then: Box<PlanNode>,
110 otherwise: Option<Box<PlanNode>>,
111 spanning: Option<u8>,
112 },
113}
114
115/// Category renames: `(from, to, [(from item, to item)])`.
116pub type Remaps = Vec<(CategoryId, CategoryId, Vec<(ItemId, ItemId)>)>;
117
118#[derive(Debug, Clone, PartialEq)]
119pub struct PlanNode {
120 pub kind: PlanNodeKind,
121 pub ty: TypeInfo,
122}