Skip to content

Push expressions through layout plans - #9166

Draft
joseph-isaacs wants to merge 1 commit into
vortex-plan-rulesfrom
vortex-plan-optimizer
Draft

Push expressions through layout plans#9166
joseph-isaacs wants to merge 1 commit into
vortex-plan-rulesfrom
vortex-plan-optimizer

Conversation

@joseph-isaacs

@joseph-isaacs joseph-isaacs commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • replace the virtual Plan::optimize_expression hook with static typed parent-child rules
  • make ExpressionPlan and every concrete expression rule operate on BoundExpression
  • register rules for (ExpressionPlan, StructPlan), (ExpressionPlan, DictPlan),
    (ExpressionPlan, ChunkedPlan), and (ExpressionPlan, RowIdxPlan)
  • partition bound expressions directly and recompute dtypes only for locally rewritten nodes;
    physical-plan optimization does not recursively type-check expressions
  • push independent struct-field expressions into each field and construct a smaller StructPlan
    containing only referenced fields
  • retain cross-field work as a residual expression over a reduced StructPlan
  • push safe boolean expressions into dictionary values while retaining dictionary codes
  • optimize heterogeneous chunks independently while preserving global row-index semantics
  • partition mixed row-index/data expressions and combine their outputs with a residual expression
  • drive rewrites top-down so rules can discard unused children before those children are optimized
  • keep recursion in the optimizer: rules return one unoptimized replacement and the optimizer
    continues traversal from that replacement
  • cover multi-field struct pruning, row-index/struct composition, heterogeneous chunks, and a
    regression proving an unused struct field is never optimized

Stack

This is part 3 of the plan-native scan stack. It is based on #9196 and is followed by #9187.

Example

Before optimization:

ExpressionPlan(($.a > 5) and ($.b > 7))
  StructPlan({a, b, c})

After optimization, the predicates are pushed into a and b, the residual combines their
boolean outputs, and the rewritten node is StructPlan({a, b}). Field c is absent. No synthetic
struct-partition plan node is introduced.

Safety

  • dictionary pushdown requires boolean output, a root reference, strictness, and infallibility
  • nullable struct expressions remain above parent validity
  • cross-field expressions remain residual expressions but run over a struct pruned to their
    referenced fields
  • chunk pushdown rejects expressions referencing global row indices
  • parent-reduction rules verify row-count and dtype preservation in debug builds
  • a same-child-type guard prevents residual expressions from repeatedly applying the same rule

Checks

  • RUSTC_WRAPPER= cargo test -p vortex-layout plan:: — 12 passed on this branch
  • RUSTC_WRAPPER= cargo clippy -p vortex-layout --all-targets --all-features -- -D warnings
  • cargo +nightly fmt --all -- --check
  • git diff --check

@codspeed-hq

codspeed-hq Bot commented Aug 4, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 25.44%

⚡ 1 improved benchmark
✅ 1933 untouched benchmarks
⏩ 51 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation decode_varbin[(1000, 2)] 78.4 µs 62.5 µs +25.44%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing vortex-plan-optimizer (0e24b8f) with vortex-plan-rules (825351d)

Open in CodSpeed

Footnotes

  1. 51 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@joseph-isaacs
joseph-isaacs marked this pull request as ready for review August 4, 2026 11:52
@joseph-isaacs
joseph-isaacs marked this pull request as draft August 4, 2026 11:52
@joseph-isaacs
joseph-isaacs force-pushed the vortex-plan-optimizer branch from 4dced78 to 7968c7e Compare August 4, 2026 16:48
@joseph-isaacs
joseph-isaacs force-pushed the vortex-plan-optimizer branch from 7968c7e to a9043d5 Compare August 5, 2026 14:35
@joseph-isaacs
joseph-isaacs changed the base branch from vortex-plan to vortex-plan-rules August 5, 2026 14:36
@joseph-isaacs
joseph-isaacs force-pushed the vortex-plan-optimizer branch from a9043d5 to bda6747 Compare August 6, 2026 09:27
@joseph-isaacs
joseph-isaacs force-pushed the vortex-plan-optimizer branch from bda6747 to 042e358 Compare August 6, 2026 12:41
@joseph-isaacs
joseph-isaacs force-pushed the vortex-plan-optimizer branch from 2028928 to 0134c4b Compare August 6, 2026 20:40
@joseph-isaacs
joseph-isaacs force-pushed the vortex-plan-optimizer branch from 0134c4b to 6c41c35 Compare August 6, 2026 23:58
@joseph-isaacs
joseph-isaacs force-pushed the vortex-plan-optimizer branch from 6c41c35 to 65aad23 Compare August 7, 2026 12:04
@joseph-isaacs
joseph-isaacs force-pushed the vortex-plan-optimizer branch from 65aad23 to 265f6c2 Compare August 7, 2026 15:33
@joseph-isaacs
joseph-isaacs force-pushed the vortex-plan-optimizer branch from 265f6c2 to b121a9e Compare August 7, 2026 15:57
Add the four parent-reduction rules that push an `Eval` into the operator
below it, and drive them from a top-down optimizer:

  Eval x Concat   push into every chunk, unless the expression reads
                  row indices, which are relative to the whole domain
  Eval x Take     evaluate over dictionary values instead of codes, only
                  when the expression is strict, infallible, and boolean
  Eval x Pack     partition per field, push each partition into its field,
                  and prune fields the expression never reads
  Eval x RowIdx   partition between generated row indices and the data
                  child, evaluating each independently

Two operators support the row-index rule: `RowIdxValues` generates global
row indices over a domain, and `RowIdxPartition` pairs an independently
evaluated row-index branch with a data branch.

The optimizer becomes top-down, driven from `Eval` nodes. A rewrite can
leave a residual expression above the same operator kind, so the driver
tracks the child it just reduced and does not immediately re-fire the same
rule on that residual. This replaces the bottom-up rewriter, whose two
rules (identity elimination and expression fusion) are subsumed by it.

Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012obBhJ8oPZoBbKyeS79yMv
Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>
@joseph-isaacs
joseph-isaacs force-pushed the vortex-plan-optimizer branch from b121a9e to 0e24b8f Compare August 7, 2026 16:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/feature A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant