Skip to main content

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}