Skip to content

Commit b121a9e

Browse files
joseph-isaacsclaude
andcommitted
Push expressions through plan operators
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>
1 parent 08b86f2 commit b121a9e

12 files changed

Lines changed: 1311 additions & 73 deletions

File tree

vortex-layout/src/plan/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,20 @@ pub use plans::PackData;
3838
pub use plans::PackPlan;
3939
pub use plans::RowIdx;
4040
pub use plans::RowIdxData;
41+
pub use plans::RowIdxPartition;
42+
pub use plans::RowIdxPartitionPlan;
4143
pub use plans::RowIdxPlan;
4244
pub use plans::RowIdxPlanMetadata;
45+
pub use plans::RowIdxValues;
46+
pub use plans::RowIdxValuesData;
47+
pub use plans::RowIdxValuesPlan;
48+
pub use plans::RowIdxValuesPlanMetadata;
4349
pub use plans::SegmentScan;
4450
pub use plans::SegmentScanData;
4551
pub use plans::SegmentScanPlan;
4652
pub use plans::Take;
4753
pub use plans::TakePlan;
54+
pub use plans::row_idx_dtype;
4855
pub use typed::DynPlan;
4956
pub use typed::Plan;
5057
pub use typed::PlanParts;

vortex-layout/src/plan/optimize.rs

Lines changed: 12 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,27 @@
11
// SPDX-License-Identifier: Apache-2.0
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

4-
//! Plan optimization rules.
4+
//! Plan optimization.
55
//!
6-
//! Rules are written against the generic plan tree rather than as a method on each operator, so
7-
//! adding a rule does not require touching every operator.
6+
//! Optimization is driven top-down from [`Eval`] nodes, which apply the static parent-reduction
7+
//! rules in [`crate::plan::optimizer`] as they become applicable. Operators without a rule simply
8+
//! optimize their children.
89
9-
use vortex_array::expr::BoundExpression;
10-
use vortex_array::expr::traversal::NodeExt;
11-
use vortex_array::expr::traversal::NodeRewriter;
12-
use vortex_array::expr::traversal::Transformed;
13-
use vortex_array::expr::traversal::TraversalOrder;
1410
use vortex_error::VortexResult;
1511

1612
use crate::plan::Eval;
17-
use crate::plan::EvalPlan;
1813
use crate::plan::PlanRef;
1914

2015
/// Optimizes `plan`, preserving its dtype and row domain.
21-
///
22-
/// Rewrites run bottom-up in a single pass, so a rule always observes children that have already
23-
/// been rewritten.
2416
pub fn optimize(plan: PlanRef) -> VortexResult<PlanRef> {
25-
Ok(plan.rewrite(&mut PlanRules)?.into_inner())
26-
}
27-
28-
/// The built-in plan rewrite rules.
29-
struct PlanRules;
30-
31-
impl NodeRewriter for PlanRules {
32-
type NodeTy = PlanRef;
33-
34-
fn visit_up(&mut self, node: PlanRef) -> VortexResult<Transformed<PlanRef>> {
35-
let Some(eval) = node.as_opt::<Eval>() else {
36-
return Ok(Transformed::no(node));
37-
};
38-
39-
// An identity expression contributes nothing over its child.
40-
if eval.expression().is_root() {
41-
return Ok(Transformed::yes(eval.child_plan().clone()));
42-
}
43-
44-
// Fuse adjacent expressions so a chain evaluates in one pass.
45-
if let Some(inner) = eval.child_plan().as_opt::<Eval>() {
46-
let fused = replace_root(eval.expression().clone(), inner.expression().clone())?;
47-
return Ok(Transformed::yes(
48-
EvalPlan::new(fused, inner.child_plan().clone()).into_plan(),
49-
));
50-
}
51-
52-
Ok(Transformed::no(node))
17+
if let Some(eval) = plan.as_opt::<Eval>() {
18+
return eval.optimize_top_down(None);
5319
}
54-
}
5520

56-
/// Substitutes `replacement` for every root reference in `expression`.
57-
fn replace_root(
58-
expression: BoundExpression,
59-
replacement: BoundExpression,
60-
) -> VortexResult<BoundExpression> {
61-
Ok(expression
62-
.transform_down(|node| {
63-
if node.is_root() {
64-
Ok(Transformed {
65-
value: replacement.clone(),
66-
order: TraversalOrder::Skip,
67-
changed: true,
68-
})
69-
} else {
70-
Ok(Transformed::no(node))
71-
}
72-
})?
73-
.into_inner())
21+
let children = plan
22+
.children()
23+
.iter()
24+
.map(|child| optimize(child.clone()))
25+
.collect::<VortexResult<Vec<_>>>()?;
26+
plan.with_children(children)
7427
}

vortex-layout/src/plan/optimizer/mod.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,38 @@ pub use rules::DynPlanParentReduceRule;
99
pub use rules::PlanParentReduceRule;
1010
pub use rules::PlanParentReduceRuleAdapter;
1111
pub use rules::PlanParentRuleSet;
12+
use vortex_error::VortexResult;
13+
14+
use super::Concat;
15+
use super::Pack;
16+
use super::PlanRef;
17+
use super::RowIdx;
18+
use super::Take;
19+
use super::plans::ExpressionConcatRule;
20+
use super::plans::ExpressionPackRule;
21+
use super::plans::ExpressionRowIdxRule;
22+
use super::plans::ExpressionTakeRule;
23+
24+
static EXPRESSION_CONCAT_RULE: PlanParentReduceRuleAdapter<Concat, ExpressionConcatRule> =
25+
PlanParentReduceRuleAdapter::new(ExpressionConcatRule);
26+
static EXPRESSION_TAKE_RULE: PlanParentReduceRuleAdapter<Take, ExpressionTakeRule> =
27+
PlanParentReduceRuleAdapter::new(ExpressionTakeRule);
28+
static EXPRESSION_ROW_IDX_RULE: PlanParentReduceRuleAdapter<RowIdx, ExpressionRowIdxRule> =
29+
PlanParentReduceRuleAdapter::new(ExpressionRowIdxRule);
30+
static EXPRESSION_PACK_RULE: PlanParentReduceRuleAdapter<Pack, ExpressionPackRule> =
31+
PlanParentReduceRuleAdapter::new(ExpressionPackRule);
32+
33+
static PARENT_RULES: PlanParentRuleSet = PlanParentRuleSet::new(&[
34+
&EXPRESSION_CONCAT_RULE,
35+
&EXPRESSION_TAKE_RULE,
36+
&EXPRESSION_ROW_IDX_RULE,
37+
&EXPRESSION_PACK_RULE,
38+
]);
39+
40+
/// Attempts a static rewrite for `parent` and its child at `child_idx`.
41+
pub(crate) fn reduce_parent(parent: &PlanRef, child_idx: usize) -> VortexResult<Option<PlanRef>> {
42+
let Some(child) = parent.child(child_idx) else {
43+
return Ok(None);
44+
};
45+
PARENT_RULES.evaluate(child, parent, child_idx)
46+
}

vortex-layout/src/plan/plans/concat.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,21 @@ use std::sync::Arc;
66

77
use vortex_array::EmptyMetadata;
88
use vortex_array::dtype::DType;
9+
use vortex_array::expr::ExactBoundExpr;
10+
use vortex_array::expr::label_bound_tree;
911
use vortex_error::VortexResult;
1012
use vortex_error::vortex_bail;
1113
use vortex_session::registry::CachedId;
1214

15+
use crate::layouts::row_idx::RowIdx as RowIdxFn;
16+
use crate::plan::Eval;
17+
use crate::plan::EvalPlan;
1318
use crate::plan::Plan;
1419
use crate::plan::PlanId;
1520
use crate::plan::PlanParts;
1621
use crate::plan::PlanRef;
1722
use crate::plan::PlanVTable;
23+
use crate::plan::optimizer::PlanParentReduceRule;
1824

1925
/// Concatenates its children row-wise.
2026
#[derive(Clone, Debug)]
@@ -93,3 +99,45 @@ impl PlanVTable for Concat {
9399
Cow::Owned(format!("chunks[{index}]"))
94100
}
95101
}
102+
103+
/// Pushes an expression into every chunk of a [`Concat`].
104+
#[derive(Debug)]
105+
pub(crate) struct ExpressionConcatRule;
106+
107+
impl PlanParentReduceRule<Concat> for ExpressionConcatRule {
108+
type Parent = Eval;
109+
110+
fn reduce_parent(
111+
&self,
112+
child: &Plan<Concat>,
113+
parent: &Plan<Eval>,
114+
_child_idx: usize,
115+
) -> VortexResult<Option<PlanRef>> {
116+
let expression = parent.expression();
117+
// Row-index expressions are relative to the whole row domain, so they cannot be evaluated
118+
// chunk by chunk.
119+
let references_row_idx = label_bound_tree(
120+
expression,
121+
|node| {
122+
node.as_scalar()
123+
.is_some_and(|scalar_fn| scalar_fn.is::<RowIdxFn>())
124+
},
125+
|acc, &child| acc | child,
126+
)
127+
.get(&ExactBoundExpr(expression.clone()))
128+
.copied()
129+
.unwrap_or(false);
130+
if references_row_idx {
131+
return Ok(None);
132+
}
133+
134+
let chunks = child
135+
.children()
136+
.iter()
137+
.map(|chunk| EvalPlan::new(expression.clone(), chunk.clone()).into_plan())
138+
.collect::<Vec<_>>();
139+
Ok(Some(
140+
ConcatPlan::try_new(expression.dtype().clone(), chunks)?.into_plan(),
141+
))
142+
}
143+
}

vortex-layout/src/plan/plans/eval.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@
44
use std::borrow::Cow;
55

66
use vortex_array::EmptyMetadata;
7+
use vortex_array::dtype::DType;
8+
use vortex_array::dtype::FieldName;
79
use vortex_array::expr::BoundExpression;
10+
use vortex_array::expr::traversal::NodeExt;
11+
use vortex_array::expr::traversal::Transformed;
12+
use vortex_array::expr::traversal::TraversalOrder;
13+
use vortex_array::scalar_fn::ScalarFnVTableExt;
14+
use vortex_array::scalar_fn::fns::get_item::GetItem;
815
use vortex_error::VortexResult;
916
use vortex_session::registry::CachedId;
1017

@@ -14,6 +21,8 @@ use crate::plan::PlanParts;
1421
use crate::plan::PlanRef;
1522
use crate::plan::PlanVTable;
1623
use crate::plan::check_child_count;
24+
use crate::plan::optimize;
25+
use crate::plan::optimizer::reduce_parent;
1726

1827
/// Applies an expression to the output of its child.
1928
#[derive(Clone, Debug)]
@@ -82,3 +91,122 @@ impl PlanVTable for Eval {
8291
}
8392
}
8493
}
94+
95+
impl EvalPlan {
96+
/// Optimizes this plan top-down, applying parent-reduction rules as they become applicable.
97+
///
98+
/// `blocked_child_type` suppresses one rule re-firing on its own residual output, which would
99+
/// otherwise loop when a rewrite leaves an expression above the same child kind.
100+
pub(crate) fn optimize_top_down(
101+
&self,
102+
blocked_child_type: Option<PlanId>,
103+
) -> VortexResult<PlanRef> {
104+
if self.expression().is_root() {
105+
return optimize(self.child_plan().clone());
106+
}
107+
if let Some(inner) = self.child_plan().as_opt::<Eval>() {
108+
let expression = replace_root(self.expression().clone(), inner.expression().clone())?;
109+
return EvalPlan::new(expression, inner.child_plan().clone()).optimize_top_down(None);
110+
}
111+
112+
let child_type = self.child_plan().id();
113+
let parent =
114+
EvalPlan::new(self.expression().clone(), self.child_plan().clone()).into_plan();
115+
if blocked_child_type != Some(child_type)
116+
&& let Some(rewritten) = reduce_parent(&parent, 0)?
117+
{
118+
return Self::optimize_rewrite(rewritten, child_type);
119+
}
120+
121+
let child = optimize(self.child_plan().clone())?;
122+
if let Some(inner) = child.as_opt::<Eval>() {
123+
let expression = replace_root(self.expression().clone(), inner.expression().clone())?;
124+
return EvalPlan::new(expression, inner.child_plan().clone()).optimize_top_down(None);
125+
}
126+
127+
let child_type = child.id();
128+
let parent = EvalPlan::new(self.expression().clone(), child).into_plan();
129+
if blocked_child_type != Some(child_type)
130+
&& let Some(rewritten) = reduce_parent(&parent, 0)?
131+
{
132+
return Self::optimize_rewrite(rewritten, child_type);
133+
}
134+
Ok(parent)
135+
}
136+
137+
fn optimize_rewrite(rewritten: PlanRef, previous_child_type: PlanId) -> VortexResult<PlanRef> {
138+
let Some(eval) = rewritten.as_opt::<Eval>() else {
139+
return optimize(rewritten);
140+
};
141+
// A residual expression may remain above the same child kind after a successful rewrite.
142+
// Do not immediately apply that rule again; recursively optimize only the retained child.
143+
let child_type = eval.child_plan().id();
144+
let blocked = (child_type == previous_child_type).then_some(previous_child_type);
145+
eval.optimize_top_down(blocked)
146+
}
147+
}
148+
149+
/// Substitutes `replacement` for every root reference in `expression`.
150+
pub(crate) fn replace_root(
151+
expression: BoundExpression,
152+
replacement: BoundExpression,
153+
) -> VortexResult<BoundExpression> {
154+
Ok(expression
155+
.transform_down(|node| {
156+
if node.is_root() {
157+
Ok(Transformed {
158+
value: replacement.clone(),
159+
order: TraversalOrder::Skip,
160+
changed: true,
161+
})
162+
} else {
163+
Ok(Transformed::no(node))
164+
}
165+
})?
166+
.into_inner())
167+
}
168+
169+
/// Rewrites partition accessors in `expression` to read from a partitioned root.
170+
pub(crate) fn rewrite_partition_root(
171+
expression: BoundExpression,
172+
root_dtype: DType,
173+
collapsed: &[(FieldName, FieldName)],
174+
) -> VortexResult<BoundExpression> {
175+
Ok(expression
176+
.transform_down(|node| {
177+
if let Some(value_name) = node
178+
.as_scalar()
179+
.and_then(|scalar_fn| scalar_fn.as_opt::<GetItem>())
180+
{
181+
let partition_access = &node.children()[0];
182+
if let Some(partition_name) = partition_access
183+
.as_scalar()
184+
.and_then(|scalar_fn| scalar_fn.as_opt::<GetItem>())
185+
&& partition_access.children()[0].is_root()
186+
&& collapsed.iter().any(|(partition, value)| {
187+
partition == partition_name && value == value_name
188+
})
189+
{
190+
return Ok(Transformed {
191+
value: BoundExpression::try_new(
192+
GetItem.bind(partition_name.clone()),
193+
[BoundExpression::new_root(root_dtype.clone())],
194+
)?,
195+
changed: true,
196+
order: TraversalOrder::Skip,
197+
});
198+
}
199+
}
200+
201+
if node.is_root() {
202+
Ok(Transformed {
203+
value: BoundExpression::new_root(root_dtype.clone()),
204+
changed: true,
205+
order: TraversalOrder::Skip,
206+
})
207+
} else {
208+
Ok(Transformed::no(node))
209+
}
210+
})?
211+
.into_inner())
212+
}

0 commit comments

Comments
 (0)