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 in
expr_ops.mbt, so every tree is well-formed by construction. Building one is
total — unknown columns and dtype mismatches surface at evaluation time
(in frame), never here.
The type is opaque: it wraps an @ir.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 lives in the module-internal internal/ir package, which 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 the node() accessor; frame and
lazy are in-module and match @ir.ExprNode directly.
The wrapped AST carries @ir.ExprNode children, not Expr, so this package
bridges the two: a constructor unwraps its child Exprs with .node, and a
walk (in explain.mbt) 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.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).