///|
/// A composable column expression — the reified upgrade over the closure-based
/// surfaces (the original row-predicate closure) and pre-materialised `Series`
/// columns. An `Expr` is built through the constructor functions
/// (`col` / `lit` / `lit_*` / `when`), the operator impls, and the methods,
/// so every tree is well-formed by construction; the `when` chain stages
/// through the `WhenThen` / `WhenThenElse` types below, completing as a
/// `Ternary`. Building one is
/// **total** — unknown columns and dtype mismatches surface at evaluation time
/// (in `frame`), never here.
///
/// The type is **opaque**: it wraps an `ExprNode` AST and exposes no
/// variants. Outside this package an expression is a value you build and pass
/// on, or render with `to_string` — there is no matching on its shape. The AST
/// itself is module-internal, in a package a downstream module cannot import
/// at all, so no caller can name a node, match
/// one, or hold one: adding a node for a new operator breaks nobody.
///
/// The shape is not observable either: there is no `==` on an expression. One
/// existed, comparing the two trees, and it made *how an operator lowers* a
/// promise to callers — normalising a tree or merging two node kinds would
/// have changed what compared equal without changing what any expression
/// means. `to_string()` is what a caller inspects with instead; it renders
/// what it prints, so two literal series differing only in their cells render
/// alike.
///
/// The engine reads the shape through four accessors beside the type —
/// `node()` hands over the wrapped AST, and `children()` /
/// `referenced_columns()` / `output_name()` answer its introspection
/// questions. `frame` and `lazy` are in-module and match `ExprNode`
/// directly.
///
/// The wrapped AST carries `ExprNode` children, not `Expr`, so this package
/// bridges the two: a constructor unwraps its child `Expr`s with `.node`, and
/// the `children` accessor below wraps `ExprNode` children back into `Expr`. The
/// nodes, in construction-route order:
/// - `Col` / `Lit` / `LitSeries` — leaves (`col`, `lit` / `lit_*`,
/// `lit_series`); a `LitSeries` embeds a pre-materialised `Series`;
/// - `Binary` — arithmetic `+ - * /`, comparisons, Kleene `&` / `|`;
/// - `Unary` — `-e`, `.not()`, the null / NaN probes, and `.abs()` / `.floor()`
/// / `.ceil()` / `.sign()` / `.round()`;
/// - `Agg` — `.sum()` / `.mean()` / … reductions;
/// - `Str` — the `.str_*` namespace;
/// - `Cast` — `.cast(dtype)`; `Alias` — `.with_alias(name)`;
/// - `Ternary` — `when(c).then(a).otherwise(b)`;
/// - `FillNull` / `FillNan` — `.fill_null(v)` / `.fill_nan(v)`, dedicated nodes
/// (not lowered to a `Ternary`) so the operand appears once and a chained
/// coalesce stays linear;
/// - `IsIn` / `IsBetween` — the membership / range predicates, likewise
/// dedicated so the operand is evaluated once;
/// - `Map` / `MapBatches` — the row-wise / batched closure escape hatches
/// (`.map_elements()` / `map_many()` / `.map_batches()`), the function opaque
/// so each is identified by its `(label, inputs)` (`MapBatches` also by its
/// `returns_scalar` flag).
pub struct Expr {
priv node : @ir.ExprNode
}
///|
/// The engine's read seam onto the opaque `Expr`, returning the wrapped AST.
/// `#doc(hidden)` keeps it out of the generated interface; the `ExprNode` it
/// returns is module-internal regardless, so the AST cannot leak either way.
/// `frame` / `lazy` enter a tree walk through this. Total.
#doc(hidden)
#internal(engine, "MoonFrame execution engine API")
pub fn Expr::node(self : Expr) -> @ir.ExprNode {
self.node
}
///|
/// The immediate sub-expressions of a node, left to right, as opaque `Expr`
/// handles — the wrapper-level face of `ExprNode::children`, which is the
/// single place that encodes the tree's recursive shape. A new variant declares
/// its child structure once, in `ExprNode`, and every walk takes its *descent*
/// from there: this handle-level face, the AST-side analyses behind
/// `referenced_columns` / `output_name`, and the lazy optimizer's `row_stable` /
/// `group_cell_stable` (which walk the raw `ExprNode` children directly).
/// What a variant does not inherit is what those walks *decide* about it — the
/// naming and pushdown analyses classify variant by variant, under exhaustive
/// `match`es that stop compiling until the new one is classified. Total — it only
/// inspects shape.
#doc(hidden)
#internal(engine, "MoonFrame execution engine API")
pub fn Expr::children(self : Expr) -> Array[Expr] {
// The AST's shape lives on `ExprNode::children`; wrap each raw child back
// into the opaque `Expr` this returns, so a caller walks the handle.
self.node.children().map(Expr::of)
}
///|
/// Every column name the tree references, as a set — the handle-level face of
/// `ExprNode::referenced_columns` (the analysis itself lives
/// beside the AST). A `Ternary`'s condition counts. Total.
#doc(hidden)
#internal(engine, "MoonFrame execution engine API")
pub fn Expr::referenced_columns(self : Expr) -> @set.Set[String] {
self.node.referenced_columns()
}
///|
/// The output-column name this expression produces (Polars' naming rule: an
/// alias wins, else the leftmost column reference, else `"literal"`) — the
/// handle-level face of `ExprNode::output_name`. Total.
#doc(hidden)
#internal(engine, "MoonFrame execution engine API")
pub fn Expr::output_name(self : Expr) -> String {
self.node.output_name()
}
///|
/// Wrap an `ExprNode` into the opaque handle — the inverse of `node`, used
/// inside this package by the constructors and the introspection walks.
fn Expr::of(node : @ir.ExprNode) -> Expr {
{ node, }
}
///|
/// First step of the conditional chain
/// `when(cond).then(a).otherwise(b)` — holds the `Bool` condition until
/// `then` supplies the matching branch. Constructible only through `when`,
/// which is a `pub` function and so cannot return a `priv` type — that, and
/// nothing else, is why the struct is `pub`. Its field is private, so the only
/// thing a caller can do with a `WhenThen` is call `then`.
pub struct WhenThen {
priv cond : Expr
}
///|
/// Second step of the conditional chain — condition plus `then` branch,
/// waiting for `otherwise` to complete the expression. Same private-field
/// rationale as `WhenThen`: the only move is `otherwise`.
pub struct WhenThenElse {
priv cond : Expr
priv then_value : Expr
}
///|
/// Open a conditional expression: `when(cond).then(a).otherwise(b)`
/// evaluates to `a` where `cond` is `true`, `b` where it is `false`, and
/// null where `cond` is null. The chain is the only construction route, so
/// a conditional is complete by the time it becomes an `Expr`.
///
/// # Example
/// ```mbt check
/// test "when: the chain is the only construction route" {
/// let tagged = when(col("qty").gt(lit_int(5)))
/// .then(lit_str("big"))
/// .otherwise(lit_str("small"))
/// inspect(
/// tagged.to_string(),
/// content="when((col(qty) > 5)).then(\"big\").otherwise(\"small\")",
/// )
/// }
/// ```
pub fn when(cond : Expr) -> WhenThen {
{ cond, }
}
///|
/// Supply the branch taken where the condition is `true`.
pub fn WhenThen::then(self : WhenThen, value : Expr) -> WhenThenElse {
{ cond: self.cond, then_value: value, }
}
///|
/// Supply the branch taken where the condition is `false`, completing the
/// conditional as a `Ternary` expression node.
pub fn WhenThenElse::otherwise(self : WhenThenElse, value : Expr) -> Expr {
Expr::of(
@ir.ExprNode::Ternary(self.cond.node, self.then_value.node, value.node),
)
}