Skip to content

refactor: fold L2Expr/L3Expr scalar-expression nodes into QueryExpr (#205) - #214

Merged
zzylol merged 1 commit into
mainfrom
issue-205-fold-expr-into-queryexpr
Aug 18, 2026
Merged

zzylol merged 1 commit into
mainfrom
issue-205-fold-expr-into-queryexpr

Conversation

@zzylol

@zzylol zzylol commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Closes #205.

Stacked on #213 (issue #179) — this branches from that PR's tip, so
the diff here is scoped to just the #205 change. It'll show as a clean
diff against main once #213 merges; until then this PR's base is
#213's branch.

What

Eliminates L2Expr/L3Expr as a separate scalar-expression type and
folds its 13 variants (Column, Literal, Compare, BoolAnd,
BoolOr, Not, IsNull, IsNotNull, Cast, InList,
FunctionCall, Arith, Case) directly into QueryExpr<C>, generic
over the same ColState that already parameterizes the relational
nodes. QueryExpr's node set now matches
docs/pre-asap-ir.md
exactly — scan is a QueryExpr variant, and so is every scalar
operator.

Design decisions

  • Dropping the compile-time scalar/relational split. Expr<C>
    used to make it impossible to nest a relational node in a scalar
    position (or vice versa) at compile time. That guarantee is dropped
    here, per the design-doc steer, since nothing upstream actually
    relied on the type system to enforce it — front ends already only
    ever build well-shaped trees. Every consumer that only handles one
    half of the merged enum gets an explicit catch-all arm for the
    other half: unreachable!() naming the violated invariant where a
    function should structurally never see the other half (e.g.
    resolve_expr, the binder's column-collection walk,
    dag_export::build), or a graceful default (None, vec![], a
    no-op) where that already matched the function's existing idiom for
    "not applicable here" (relational-only tree visitors stopping at a
    scalar leaf).

  • Boxing. Predicate<C>'s field and Relabel.value name
    QueryExpr<C> directly (not through a Vec), which becomes
    self-referential once Expr<C>'s variants live inside QueryExpr<C>
    itself — Rust's recursive-type-size check (E0072) requires heap
    indirection there, so both became Box<QueryExpr<C>>.
    ProjectItem.expr/SortKey.expr didn't need it, since they're
    always reached through a Vec.

  • expr_ir.rs keeps ColumnRef, L3Scalar, CompareOp, ArithOp
    the vocabulary scalar expressions are built from — but no longer
    defines a tree type of its own.

Blast radius

Because L2QueryExpr/L2Expr/L3Expr share variant names with
QueryExpr, the front ends' existing use ... as L2 aliases absorbed
almost the whole rename — most of the ~225 call sites needed nothing
beyond L2Expr/L3ExprL2/QueryExpr. The recurring real fix was
wrapping now-boxed Predicate/Relabel arguments in Box::new(...)
at their ~15 construction sites (resolve.rs, canonicalize.rs,
dag_export.rs, promql.rs, sql/mod.rs, and several test files).

Verification

cargo build / clippy --all-targets / test / doc all clean
across the workspace. Test counts unchanged and fully green:

  • asap-types: 61
  • asap-frontend-promql: 180
  • asap-frontend-sql: 68
  • asap-integration-tests: 81
  • asap-aware-mapping: 26

This is a pure type reshape — no lowering/binding/canonicalization
behavior changed. The 4 pre-existing cargo doc warnings (broken/
ambiguous intra-doc links in binder.rs, resolve.rs, mod.rs)
predate this change (confirmed against HEAD), not introduced by it.

🤖 Generated with Claude Code

Base automatically changed from worktree-issue-179-delete-l2 to main August 18, 2026 22:34
…205)

Issue #205: eliminate the separate L2Expr/L3Expr scalar-expression type
and fold its 13 variants directly into QueryExpr, generic over the same
ColState (ColumnRef vs. ColumnId) that already parameterizes the
relational nodes. QueryExpr's node set now matches the design doc
(docs/pre-asap-ir.md) exactly: scan is a QueryExpr variant, and so is
every scalar operator.

Design decisions:

- The compile-time "scalar vs. relational" distinction that Expr<C>
  used to buy is dropped, per the design-doc steer: front ends already
  build only well-shaped trees (a Filter's pred is never a Scan, a
  Compare's operand is never an Aggregate), so nothing upstream of this
  merge relied on the type system to enforce it. Every consumer that
  only handles one half of the merged enum now has an explicit
  catch-all match arm for the other half — `unreachable!()` with a
  message naming the violated invariant where the function should
  structurally never see the other half (e.g. resolve_expr, the
  binder's column-collection walk, dag_export::build), or a graceful
  default (`None`, `vec![]`, a no-op) where that already matched the
  function's existing idiom for "not applicable here" (e.g. a
  relational-only tree visitor stopping at a scalar leaf).

- Boxing: Predicate<C> and Relabel's `value` field name QueryExpr<C>
  directly (not through a Vec), which is now self-referential once
  Expr<C>'s variants live inside QueryExpr<C> itself — Rust's
  recursive-type-size check (E0072) requires heap indirection there, so
  both became Box<QueryExpr<C>>. ProjectItem.expr and SortKey.expr
  didn't need it: they're always reached through a Vec, which already
  provides indirection.

- expr_ir.rs keeps ColumnRef, L3Scalar, CompareOp, and ArithOp — the
  vocabulary scalar expressions are built from — but no longer defines
  a tree type of its own.

Mechanical fallout: because L2QueryExpr and L2/L3Expr's constructors
share the same variant names, the front ends' existing `use ... as L2`
aliases absorbed almost the entire rename — most call sites needed
nothing beyond L2Expr/L3Expr -> L2/QueryExpr. The only recurring fix
was wrapping now-boxed Predicate/Relabel arguments in Box::new(...) at
their ~15 construction sites across resolve.rs, canonicalize.rs,
dag_export.rs, promql.rs, sql/mod.rs, and several test files.

Verified: cargo build/clippy(--all-targets)/test/doc --workspace all
clean. Full test count unchanged and green (asap-types 61,
asap-frontend-promql 180, asap-frontend-sql 68, asap-integration-tests
81, asap-aware-mapping 26, asap-devtools 0+7) — this is a pure type
reshape, no lowering/binding/canonicalization behavior changed. The 4
pre-existing `cargo doc` warnings (unresolved/ambiguous intra-doc
links in binder.rs, resolve.rs, mod.rs) predate this change, confirmed
against HEAD.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@zzylol
zzylol force-pushed the issue-205-fold-expr-into-queryexpr branch from 5912b18 to 46d39ee Compare August 18, 2026 22:37
@zzylol
zzylol merged commit 2223516 into main Aug 18, 2026
3 checks passed
@zzylol
zzylol deleted the issue-205-fold-expr-into-queryexpr branch August 18, 2026 22:43
zzylol added a commit that referenced this pull request Aug 19, 2026
The `pre-asap-ir.md`/`post-asap-ir.md` design docs don't define an
L1–L5 layer numbering (#211 explicitly removed it from the docs as
undefined jargon, in favor of the terms the docs actually define:
"pre-ASAP IR" / "post-ASAP IR"). The source code still had it all over
— both as real type names and as prose shorthand — surfaced during
#214's review ("what is L3Scalar?" → "does L2 refer to pre-asap-ir?").
This drops it from the code too.

Type renames (mechanical, no behavior change):
- `L2QueryExpr` -> `UnresolvedQueryExpr` (`QueryExpr<ColumnRef>`, the
  front-end-emitted, name-based tree)
- `L3QueryExpr` -> `ResolvedQueryExpr` (`QueryExpr<ColumnId>`, the
  positional, resolved tree)
- `L4Node` -> `SummaryNode`, `L4Schema` -> `SummarySchema`,
  `L4DataType` -> `SummaryDataType`, `L4Field` -> `SummaryField` (all
  four already sat next to `SummaryExpr`/`SummaryKind`/`SummaryParams`
  in `post_asap` — this makes them consistent with their own module's
  naming instead of a leftover number)

The two front ends' local `L2` construction alias
(`UnresolvedQueryExpr as L2`) becomes `Unresolved`, matching the same
treatment. A few incidentally-adjacent stale names got the same
cleanup while touching their call sites: `df_expr_to_l2` ->
`df_expr_to_unresolved`, `scalar_value_to_l3` -> `scalar_value_to_asap`,
`arrow_to_l3`/`l3_to_arrow` -> `arrow_to_dtype`/`dtype_to_arrow`,
`matcher_to_l3expr` -> `matcher_to_compare`, plus local variables named
`l2`/`l3`/`l4` (`tree`, `resolved`, `bound`, `logical_leaf`, …) and two
test-local `as L2`/`as L3` aliases (dropped — the bare names already
read fine at those call sites).

Two real name collisions surfaced while doing this: `sql/mod.rs` and
`sql/types.rs` both already import `datafusion::common::ScalarValue`
unqualified — aliased that import to `DfScalarValue` in both (same
`DfColumn`/`ArrowDataType`-style convention those files already use),
rather than picking a different name for the `asap-types` type.

The much larger remainder was prose: module docs, doc comments, and
`//` explanatory comments across nearly every crate describing a step
as "L1 parse", "the L2 tree", "an L4 concern", etc. Replaced with
either the descriptive phrase already used elsewhere in this repo
("pre-ASAP" / "post-ASAP" / "canonical" / "unresolved" / "resolved"/
"a deployment's own physical stage" for the L5 placeholder that was
never modeled in this crate to begin with) or, where the sentence
just meant "the canonical tree", dropped the label entirely. A few
doubly-stale spots got fixed at the same time since they were in the
exact text being rewritten: `bind.rs`'s/`show_post_asap_ir.rs`'s
references to the deleted `asap-ir`/`asap-lower` crate names, and
`devtools/src/lib.rs`'s reference to a "shared L2→L3 converter" that
issue #179 removed (each front end now calls `resolve_root` directly).

Left alone: `boundary.rs`'s "L2-norm"/"L1-norm" (real math, Count-
Sketch's error bound), and two literal citations of an external design
doc's own section titles ("§6 ... L3 edge", "§'L4 — sketch algebra'")
in `schema.rs`/`l4_binding.rs` — that doc's own numbering, not this
crate's.

Verified: cargo build / clippy(--all-targets --all-features --locked
-- -D warnings) / fmt --all -- --check / test(--locked) / doc
--workspace --no-deps all clean. Same test counts as before, all
green. `cargo doc` warnings are one *fewer* than baseline (fixed the
`asap_ir` link along the way) — no new warnings introduced. Pure
rename — no behavior change.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Eliminate L2Expr/L3Expr as a separate scalar-expression type; fold into QueryExpr, generic over column-reference state

1 participant