From 2ae4de657f40d681984d61b9f28b45d2419593b4 Mon Sep 17 00:00:00 2001 From: Nicholas Gates Date: Sun, 6 Sep 2026 02:58:48 -0400 Subject: [PATCH] Batch fragmented CASE branches and push selections before scalar execution Signed-off-by: Nicholas Gates --- vortex-array/benches/expr/case_when_bench.rs | 61 +++- vortex-array/src/arrays/filter/rules.rs | 9 +- vortex-array/src/arrays/scalar_fn/rules.rs | 42 +-- vortex-array/src/scalar_fn/fns/case_when.rs | 354 +++++++++++++------ vortex-array/src/test_harness/trace/tests.rs | 23 +- 5 files changed, 345 insertions(+), 144 deletions(-) diff --git a/vortex-array/benches/expr/case_when_bench.rs b/vortex-array/benches/expr/case_when_bench.rs index fa5af4bc0ef..5076c1c80bd 100644 --- a/vortex-array/benches/expr/case_when_bench.rs +++ b/vortex-array/benches/expr/case_when_bench.rs @@ -14,7 +14,13 @@ use vortex_array::IntoArray; use vortex_array::VortexSessionExecute; use vortex_array::array_session; use vortex_array::arrays::BoolArray; +use vortex_array::arrays::ConstantArray; +use vortex_array::arrays::DecimalArray; +use vortex_array::arrays::ScalarFnArray; use vortex_array::arrays::StructArray; +use vortex_array::arrays::VarBinViewArray; +use vortex_array::builtins::ArrayBuiltins; +use vortex_array::dtype::DecimalDType; use vortex_array::expr::case_when; use vortex_array::expr::case_when_no_else; use vortex_array::expr::eq; @@ -24,6 +30,12 @@ use vortex_array::expr::lit; use vortex_array::expr::lt; use vortex_array::expr::nested_case_when; use vortex_array::expr::root; +use vortex_array::scalar::Scalar; +use vortex_array::scalar_fn::ScalarFnVTableExt; +use vortex_array::scalar_fn::fns::case_when::CaseWhen; +use vortex_array::scalar_fn::fns::case_when::CaseWhenOptions; +use vortex_array::scalar_fn::fns::operators::Operator; +use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_session::VortexSession; @@ -37,6 +49,53 @@ fn main() { divan::main(); } +#[divan::bench(args = [1, 4, 16, 64, 256, 4096])] +fn case_when_decimal_product(bencher: Bencher, run_length: usize) { + let len = 65_536; + let lhs = DecimalArray::new( + Buffer::from_iter((0..len).map(|i| (i % 10_000) as i64)), + DecimalDType::new(15, 2), + Validity::NonNullable, + ) + .into_array(); + let rhs = DecimalArray::new( + Buffer::from_iter((0..len).map(|i| (i % 100) as i64)), + DecimalDType::new(15, 2), + Validity::NonNullable, + ) + .into_array(); + let product = lhs.binary(rhs, Operator::Mul).unwrap(); + let otherwise = ConstantArray::new(Scalar::zero_value(product.dtype()), len).into_array(); + bench_branches(bencher, run_length, product, otherwise); +} + +#[divan::bench(args = [1, 4, 16, 64, 256, 4096])] +fn case_when_string_values(bencher: Bencher, run_length: usize) { + let len = 65_536; + let lhs = VarBinViewArray::from_iter_str((0..len).map(|i| format!("long-left-value-{i}"))) + .into_array(); + let rhs = VarBinViewArray::from_iter_str((0..len).map(|i| format!("long-right-value-{i}"))) + .into_array(); + bench_branches(bencher, run_length, lhs, rhs); +} + +fn bench_branches(bencher: Bencher, run_length: usize, lhs: ArrayRef, rhs: ArrayRef) { + let mask = BoolArray::from_iter((0..lhs.len()).map(|i| (i / run_length).is_multiple_of(2))) + .into_array(); + let array = ScalarFnArray::try_new( + CaseWhen.bind(CaseWhenOptions { + num_when_then_pairs: 1, + has_else: true, + }), + vec![mask, lhs, rhs], + ) + .unwrap() + .into_array(); + bencher + .with_inputs(|| SESSION.create_execution_ctx()) + .bench_refs(|ctx| array.clone().execute::(ctx).unwrap()); +} + fn make_struct_array(size: usize) -> ArrayRef { let data: Buffer = (0..size as i32).collect(); let field = data.into_array(); @@ -264,7 +323,7 @@ fn case_when_all_false(bencher: Bencher, size: usize) { }); } -/// Benchmark CASE WHEN cycling through 3 branches per row (triggers merge_row_by_row). +/// Benchmark compact CASE WHEN assembly with three interleaved branches. /// Run length = 1; exercises branch 0, branch 1, and the else fallback at every 3rd row. #[divan::bench(args = [100, 400])] fn case_when_fragmented(bencher: Bencher, size: usize) { diff --git a/vortex-array/src/arrays/filter/rules.rs b/vortex-array/src/arrays/filter/rules.rs index 53f35c154cd..a161931279c 100644 --- a/vortex-array/src/arrays/filter/rules.rs +++ b/vortex-array/src/arrays/filter/rules.rs @@ -67,7 +67,14 @@ impl ArrayReduceRule for TrivialFilterRule { match array.filter_mask() { Mask::AllTrue(_) => Ok(Some(array.child().clone())), Mask::AllFalse(_) => Ok(Some(Canonical::empty(array.dtype()).into_array())), - Mask::Values(_) => Ok(None), + mask @ Mask::Values(_) => { + if let (Some(first), Some(last)) = (mask.first(), mask.last()) + && last - first + 1 == mask.true_count() + { + return Ok(Some(array.child().slice(first..last + 1)?)); + } + Ok(None) + } } } } diff --git a/vortex-array/src/arrays/scalar_fn/rules.rs b/vortex-array/src/arrays/scalar_fn/rules.rs index 52373d80a33..a9dc7619783 100644 --- a/vortex-array/src/arrays/scalar_fn/rules.rs +++ b/vortex-array/src/arrays/scalar_fn/rules.rs @@ -27,7 +27,7 @@ pub(super) const RULES: ReduceRuleSet = ReduceRuleSet::new(&[&ScalarFnPackToStructRule, &ScalarFnAbstractReduceRule]); pub(super) const PARENT_RULES: ParentRuleSet = ParentRuleSet::new(&[ - ParentRuleSet::lift(&ScalarFnUnaryFilterPushDownRule), + ParentRuleSet::lift(&ScalarFnFilterPushDownRule), ParentRuleSet::lift(&ScalarFnSliceReduceRule), ]); @@ -95,9 +95,9 @@ impl ArrayReduceRule for ScalarFnAbstractReduceRule { } #[derive(Debug)] -struct ScalarFnUnaryFilterPushDownRule; +struct ScalarFnFilterPushDownRule; -impl ArrayParentReduceRule for ScalarFnUnaryFilterPushDownRule { +impl ArrayParentReduceRule for ScalarFnFilterPushDownRule { type Parent = Filter; fn reduce_parent( @@ -106,31 +106,21 @@ impl ArrayParentReduceRule for ScalarFnUnaryFilterPushDownRule { parent: ArrayView<'_, Filter>, _child_idx: usize, ) -> VortexResult> { - // If we only have one non-constant child, then it is _always_ cheaper to push down the - // filter over the children of the scalar function array. - if child + // Selection precedes value evaluation, including errors in unselected rows. + let new_children: Vec<_> = child .iter_children() - .filter(|c| !c.is::()) - .count() - == 1 - { - let new_children: Vec<_> = child - .iter_children() - .map(|c| match c.as_opt::() { - Some(array) => { - Ok(ConstantArray::new(array.scalar().clone(), parent.len()).into_array()) - } - None => c.filter(parent.filter_mask().clone()), - }) - .try_collect()?; - - let new_array = - ScalarFnArray::try_new(child.scalar_fn().clone(), new_children)?.into_array(); - - return Ok(Some(new_array)); - } + .map(|c| match c.as_opt::() { + Some(array) => { + Ok(ConstantArray::new(array.scalar().clone(), parent.len()).into_array()) + } + None => c.filter(parent.filter_mask().clone()), + }) + .try_collect()?; - Ok(None) + Ok(Some( + ScalarFnArray::try_new_with_len(child.scalar_fn().clone(), new_children, parent.len())? + .into_array(), + )) } } diff --git a/vortex-array/src/scalar_fn/fns/case_when.rs b/vortex-array/src/scalar_fn/fns/case_when.rs index b9d3450562a..7db1a0e4df1 100644 --- a/vortex-array/src/scalar_fn/fns/case_when.rs +++ b/vortex-array/src/scalar_fn/fns/case_when.rs @@ -16,6 +16,7 @@ use std::hash::Hash; use std::sync::Arc; use prost::Message; +use vortex_buffer::BufferMut; use vortex_error::VortexResult; use vortex_error::vortex_bail; use vortex_mask::AllOr; @@ -30,7 +31,6 @@ use crate::IntoArray; use crate::arrays::BoolArray; use crate::arrays::ConstantArray; use crate::arrays::bool::BoolArrayExt; -use crate::builders::ArrayBuilder; use crate::builders::builder_with_capacity_in; use crate::builtins::ArrayBuiltins; use crate::dtype::DType; @@ -46,7 +46,6 @@ use crate::scalar_fn::SimplifyCtx; use crate::scalar_fn::fns::is_not_null::IsNotNull; use crate::scalar_fn::fns::is_null::IsNull; use crate::scalar_fn::fns::literal::Literal; -use crate::scalar_fn::fns::zip::zip_impl; /// Options for the n-ary CaseWhen expression. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -211,14 +210,12 @@ impl ScalarFnVTable for CaseWhen { args: &dyn ExecutionArgs, ctx: &mut ExecutionCtx, ) -> VortexResult { - // Inspired by https://datafusion.apache.org/blog/2026/02/02/datafusion_case/ - // - // TODO: shrink input to `remaining` rows between WHEN iterations (batch reduction). - // TODO: project to only referenced columns before batch reduction (column projection). - // TODO: evaluate THEN/ELSE on compact matching/non-matching rows and scatter-merge the results. - // TODO: for constant WHEN/THEN values, compile to a hash table for a single-pass lookup. let row_count = args.row_count(); let num_pairs = options.num_when_then_pairs as usize; + let dtypes = (0..args.num_inputs()) + .map(|index| Ok(args.get(index)?.dtype().clone())) + .collect::>>()?; + let output_dtype = self.return_dtype(options, &dtypes)?; let mut remaining = Mask::new_true(row_count); let mut branches: Vec<(Mask, ArrayRef)> = Vec::with_capacity(num_pairs); @@ -228,17 +225,17 @@ impl ScalarFnVTable for CaseWhen { break; } - let condition = args.get(i * 2)?; + let condition = args.get(i * 2)?.filter(remaining.clone())?; let cond_bool = condition.execute::(ctx)?; let cond_mask = cond_bool.to_mask_fill_null_false(ctx); - let effective_mask = &remaining & &cond_mask; + let effective_mask = remaining.intersect_by_rank(&cond_mask); if effective_mask.all_false() { continue; } let then_value = args.get(i * 2 + 1)?; - remaining = remaining.bitand_not(&cond_mask); + remaining = remaining.bitand_not(&effective_mask); branches.push((effective_mask, then_value)); } @@ -250,10 +247,10 @@ impl ScalarFnVTable for CaseWhen { }; if branches.is_empty() { - return Ok(else_value); + return else_value.cast(output_dtype); } - merge_case_branches(branches, else_value, ctx) + merge_case_branches(branches, else_value, ctx)?.cast(output_dtype) } fn simplify( @@ -311,136 +308,142 @@ impl ScalarFnVTable for CaseWhen { } } -/// Average run length at which slicing + context-aware builder appends become cheaper than `scalar_at`. -/// Measured empirically via benchmarks. -const SLICE_CROSSOVER_RUN_LEN: usize = 4; - -/// Merges disjoint `(mask, then_value)` branch pairs with an `else_value` into a single array. -/// -/// Branch masks are guaranteed disjoint by the remaining-row tracking in [`CaseWhen::execute`]. +/// Assemble coarse runs directly; compact fragmented branches before restoring row positions. fn merge_case_branches( branches: Vec<(Mask, ArrayRef)>, else_value: ArrayRef, ctx: &mut ExecutionCtx, ) -> VortexResult { - if branches.len() == 1 { - let (mask, then_value) = &branches[0]; - return zip_impl(then_value, &else_value, mask, ctx); - } - let output_nullability = branches .iter() .fold(else_value.dtype().nullability(), |acc, (_, arr)| { acc | arr.dtype().nullability() }); let output_dtype = else_value.dtype().with_nullability(output_nullability); - let branch_arrays: Vec<&ArrayRef> = branches.iter().map(|(_, arr)| arr).collect(); - - let mut spans: Vec<(usize, usize, usize)> = Vec::new(); - for (branch_idx, (mask, _)) in branches.iter().enumerate() { - match mask.slices() { - AllOr::All => return branch_arrays[branch_idx].cast(output_dtype), - AllOr::None => {} - AllOr::Some(slices) => { - for &(start, end) in slices { - spans.push((start, end, branch_idx)); - } - } - } + let len = else_value.len(); + if branches.is_empty() { + return else_value.cast(output_dtype); + } + if let Some((_, value)) = branches.iter().find(|(mask, _)| mask.all_true()) { + return value.cast(output_dtype); } - spans.sort_unstable_by_key(|&(start, ..)| start); - if spans.is_empty() { - return else_value.cast(output_dtype); + if prefer_runs(&branches, len) { + return merge_runs(&branches, &else_value, &output_dtype, ctx); } - let builder = builder_with_capacity_in(&output_dtype, else_value.len(), ctx.allocator()); + merge_compact(&branches, &else_value, &output_dtype, ctx) +} - let fragmented = spans.len() > else_value.len() / SLICE_CROSSOVER_RUN_LEN; - if fragmented { - merge_row_by_row( - &branch_arrays, - &else_value, - &spans, - &output_dtype, - builder, - ctx, - ) - } else { - merge_run_by_run( - &branch_arrays, - &else_value, - &spans, - &output_dtype, - builder, - ctx, - ) +fn prefer_runs(branches: &[(Mask, ArrayRef)], len: usize) -> bool { + // A span may add an ELSE run. Coarse runs amortize slicing without the + // extra compact-output buffer and gather; isolated branch selections do too. + const MIN_ROWS_PER_RUN: usize = 128; + let mut remaining_spans = branches.len().max(len / (2 * MIN_ROWS_PER_RUN)); + for (mask, _) in branches { + let count = match mask { + Mask::AllTrue(_) => 1, + Mask::AllFalse(_) => 0, + Mask::Values(values) => values.cached_slices().map_or_else( + || { + values + .bit_buffer() + .set_slices() + .take(remaining_spans.saturating_add(1)) + .count() + }, + |slices| slices.len(), + ), + }; + if count > remaining_spans { + return false; + } + remaining_spans -= count; } + true } -/// Iterates spans directly, emitting one `scalar_at` per row. -/// Zero per-run allocations; preferred for fragmented masks (avg run < [`SLICE_CROSSOVER_RUN_LEN`]). -fn merge_row_by_row( - branch_arrays: &[&ArrayRef], +fn merge_compact( + branches: &[(Mask, ArrayRef)], else_value: &ArrayRef, - spans: &[(usize, usize, usize)], output_dtype: &DType, - mut builder: Box, ctx: &mut ExecutionCtx, ) -> VortexResult { - let mut pos = 0; - for &(start, end, branch_idx) in spans { - for row in pos..start { - let scalar = else_value.execute_scalar(row, ctx)?; - builder.append_scalar(&scalar.cast(output_dtype)?)?; - } - for row in start..end { - let scalar = branch_arrays[branch_idx].execute_scalar(row, ctx)?; - builder.append_scalar(&scalar.cast(output_dtype)?)?; - } - pos = end; + let len = else_value.len(); + let mut remaining = Mask::new_true(len); + let mut builder = builder_with_capacity_in(output_dtype, len, ctx.allocator()); + let mut indices = BufferMut::::zeroed_in(len, ctx.allocator().clone()); + for (mask, value) in branches { + let offset = builder.len(); + value + .filter(mask.clone())? + .cast(output_dtype.clone())? + .append_to_builder(builder.as_mut(), ctx)?; + assign_positions(&mut indices, mask, offset); + remaining = remaining.bitand_not(mask); } - for row in pos..else_value.len() { - let scalar = else_value.execute_scalar(row, ctx)?; - builder.append_scalar(&scalar.cast(output_dtype)?)?; + if !remaining.all_false() { + let offset = builder.len(); + else_value + .filter(remaining.clone())? + .cast(output_dtype.clone())? + .append_to_builder(builder.as_mut(), ctx)?; + assign_positions(&mut indices, &remaining, offset); } - - Ok(builder.finish()) + builder.finish().take(indices.freeze().into_array()) } -/// Bulk-copies each span via `slice()` and context-aware builder appends. -/// Preferred when runs are long enough that memcpy dominates over per-slice allocation cost. -/// Lazy cast via `arr.cast(output_dtype)` is executed once per span as a block. -fn merge_run_by_run( - branch_arrays: &[&ArrayRef], +fn merge_runs( + branches: &[(Mask, ArrayRef)], else_value: &ArrayRef, - spans: &[(usize, usize, usize)], output_dtype: &DType, - mut builder: Box, ctx: &mut ExecutionCtx, ) -> VortexResult { - let else_value = else_value.cast(output_dtype.clone())?; - let len = else_value.len(); - for (start, end, branch_idx) in spans { - if builder.len() < *start { + let mut spans = Vec::new(); + for (index, (mask, _)) in branches.iter().enumerate() { + if let AllOr::Some(slices) = mask.slices() { + spans.extend(slices.iter().map(|&(start, end)| (start, end, index))); + } + } + spans.sort_unstable_by_key(|&(start, ..)| start); + let mut builder = builder_with_capacity_in(output_dtype, else_value.len(), ctx.allocator()); + for (start, end, branch) in spans { + if builder.len() < start { else_value - .slice(builder.len()..*start)? + .slice(builder.len()..start)? + .cast(output_dtype.clone())? .append_to_builder(builder.as_mut(), ctx)?; } - branch_arrays[*branch_idx] + branches[branch] + .1 + .slice(start..end)? .cast(output_dtype.clone())? - .slice(*start..*end)? .append_to_builder(builder.as_mut(), ctx)?; } - if builder.len() < len { + if builder.len() < else_value.len() { else_value - .slice(builder.len()..len)? + .slice(builder.len()..else_value.len())? + .cast(output_dtype.clone())? .append_to_builder(builder.as_mut(), ctx)?; } - Ok(builder.finish()) } +fn assign_positions(indices: &mut [u64], mask: &Mask, offset: usize) { + match mask.indices() { + AllOr::All => { + for (position, index) in indices.iter_mut().enumerate() { + *index = (offset + position) as u64; + } + } + AllOr::None => {} + AllOr::Some(selected) => { + for (position, &row) in selected.iter().enumerate() { + indices[row] = (offset + position) as u64; + } + } + } +} #[cfg(test)] mod tests { use std::sync::LazyLock; @@ -454,10 +457,14 @@ mod tests { use crate::IntoArray; use crate::VortexSessionExecute; use crate::arrays::BoolArray; + use crate::arrays::DecimalArray; use crate::arrays::PrimitiveArray; + use crate::arrays::ScalarFnArray; use crate::arrays::StructArray; + use crate::arrays::VarBinViewArray; use crate::assert_arrays_eq; use crate::dtype::DType; + use crate::dtype::DecimalDType; use crate::dtype::Nullability; use crate::dtype::PType; use crate::dtype::StructFields; @@ -474,9 +481,153 @@ mod tests { use crate::expr::root; use crate::expr::test_harness; use crate::scalar::Scalar; + use crate::scalar_fn::ScalarFnVTableExt; + use crate::scalar_fn::VecExecutionArgs; + use crate::scalar_fn::fns::operators::Operator; + use crate::validity::Validity; static SESSION: LazyLock = LazyLock::new(crate::array_session); + #[test] + fn compact_and_run_merges_agree_for_nested_and_nullable_values() -> VortexResult<()> { + let primitive = + PrimitiveArray::from_option_iter([Some(1i64), None, Some(3), Some(4)]).into_array(); + let fixtures = vec![ + primitive.clone(), + DecimalArray::new( + buffer![100i64, -200, 300, 400], + DecimalDType::new(15, 2), + Validity::from_iter([true, false, true, true]), + ) + .into_array(), + DecimalArray::new( + buffer![1i128 << 100, -2, 3, 4], + DecimalDType::new(38, 0), + Validity::NonNullable, + ) + .into_array(), + VarBinViewArray::from_iter( + [ + Some("long out-of-line value"), + None, + Some("short"), + Some(""), + ], + DType::Utf8(Nullability::Nullable), + ) + .into_array(), + StructArray::try_from_iter([("nested", primitive)])?.into_array(), + ]; + let mut ctx = SESSION.create_execution_ctx(); + for value in fixtures { + let otherwise = value + .take(buffer![3u32, 2, 1, 0].into_array())? + .execute::(&mut ctx)? + .into_array(); + for selection in [ + [true, false, true, false], + [true, true, false, false], + [false, true, false, false], + ] { + let branches = vec![(Mask::from_iter(selection), value.clone())]; + assert_arrays_eq!( + merge_runs(&branches, &otherwise, value.dtype(), &mut ctx)?, + merge_compact(&branches, &otherwise, value.dtype(), &mut ctx)?, + &mut ctx + ); + } + } + Ok(()) + } + + #[test] + fn selected_rows_do_not_execute_erroring_branches_or_later_conditions() -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let condition = + BoolArray::from_iter([Some(true), Some(false), Some(true), None]).into_array(); + let division = buffer![20i32, 30, 40, 50] + .into_array() + .binary(buffer![2i32, 0, 4, 0].into_array(), Operator::Div)?; + assert!(division.clone().execute::(&mut ctx).is_err()); + let options = CaseWhenOptions { + num_when_then_pairs: 1, + has_else: true, + }; + let result = CaseWhen.execute( + &options, + &VecExecutionArgs::new( + vec![ + condition.clone(), + division.clone(), + ConstantArray::new(7i32, 4).into_array(), + ], + 4, + ), + &mut ctx, + )?; + assert_arrays_eq!(result, buffer![10i32, 7, 10, 7].into_array(), &mut ctx); + + let result = CaseWhen.execute( + &options, + &VecExecutionArgs::new( + vec![ + condition.clone(), + StructArray::try_from_iter([("value", division.clone())])?.into_array(), + StructArray::try_from_iter([( + "value", + ConstantArray::new(7i32, 4).into_array(), + )])? + .into_array(), + ], + 4, + ), + &mut ctx, + )?; + assert_arrays_eq!( + result, + StructArray::try_from_iter([("value", buffer![10i32, 7, 10, 7].into_array())])? + .into_array(), + &mut ctx + ); + + let later_condition = buffer![20i32, 30, 40, 50] + .into_array() + .binary(buffer![0i32, 3, 0, 5].into_array(), Operator::Div)? + .binary(ConstantArray::new(0i32, 4).into_array(), Operator::Gt)?; + let result = CaseWhen.execute( + &CaseWhenOptions { + num_when_then_pairs: 2, + has_else: true, + }, + &VecExecutionArgs::new( + vec![ + condition, + ConstantArray::new(2i32, 4).into_array(), + later_condition, + ConstantArray::new(3i32, 4).into_array(), + division, + ], + 4, + ), + &mut ctx, + )?; + assert_arrays_eq!(result, buffer![2i32, 3, 2, 3].into_array(), &mut ctx); + + let selected_error = ScalarFnArray::try_new( + CaseWhen.bind(options), + vec![ + ConstantArray::new(true, 4).into_array(), + buffer![20i32, 30, 40, 50] + .into_array() + .binary(buffer![2i32, 0, 4, 0].into_array(), Operator::Div)?, + ConstantArray::new(7i32, 4).into_array(), + ], + )? + .into_array(); + assert!(selected_error.execute::(&mut ctx).is_err()); + Ok(()) + } + /// Helper to evaluate an expression using the apply+execute pattern fn evaluate_expr(expr: &Expression, array: &ArrayRef) -> ArrayRef { let mut ctx = SESSION.create_execution_ctx(); @@ -1458,8 +1609,7 @@ mod tests { #[test] fn test_merge_case_branches_alternating_mask() -> VortexResult<()> { let mut ctx = SESSION.create_execution_ctx(); - // Exercises the scalar path: alternating rows produce one slice per row (no runs), - // triggering the per-row cursor path in merge_case_branches. + // Alternating rows exercise compact branch assembly. let n = 100usize; // Branch 0: even rows → 0, Branch 1: odd rows → 1, Else: never reached. diff --git a/vortex-array/src/test_harness/trace/tests.rs b/vortex-array/src/test_harness/trace/tests.rs index bfe99c3857b..798f8085749 100644 --- a/vortex-array/src/test_harness/trace/tests.rs +++ b/vortex-array/src/test_harness/trace/tests.rs @@ -393,7 +393,7 @@ fn trace_optimize_parent_reduce_fixpoint_attempts() -> VortexResult<()> { )? .into_array(); let outer = - FilterArray::try_new(inner, Mask::from_iter([false, true, true, false]))?.into_array(); + FilterArray::try_new(inner, Mask::from_iter([true, false, true, false]))?.into_array(); let traced = trace_op_with( TraceOptions { @@ -406,7 +406,7 @@ fn trace_optimize_parent_reduce_fixpoint_attempts() -> VortexResult<()> { assert!(optimized_filter.child().is::()); assert_arrays_eq!( traced.output, - PrimitiveArray::from_iter([2i32, 3]), + PrimitiveArray::from_iter([0i32, 3]), &mut execution_ctx() ); insta::assert_snapshot!(traced.trace.to_string(), @r" @@ -430,25 +430,20 @@ fn trace_optimize_parent_reduce_fixpoint_attempts() -> VortexResult<()> { // The trace below only records the shape of the execution; pin the values it produced too. assert_arrays_eq!( traced.output, - PrimitiveArray::from_iter([2i32, 3]), + PrimitiveArray::from_iter([0i32, 3]), &mut execution_ctx() ); insta::assert_snapshot!(traced.trace.to_string(), @" execute_until target=AnyCanonical root=vortex.filter(i32, len=2) iter 0 current=vortex.filter(i32, len=2) builder_active=false - Done array=vortex.slice(i32, len=2) - iter 1 current=vortex.slice(i32, len=2) builder_active=false - ExecuteSlot slot=0 parent=vortex.slice(i32, len=2) child=vortex.filter(i32, len=4) - iter 2 current=vortex.filter(i32, len=4) stack_parent=vortex.slice(i32, len=2) slot=0 builder_active=false + ExecuteSlot slot=0 parent=vortex.filter(i32, len=2) child=vortex.filter(i32, len=4) + iter 1 current=vortex.filter(i32, len=4) stack_parent=vortex.filter(i32, len=2) slot=0 builder_active=false Done array=vortex.primitive(i32, len=4) - iter 3 current=vortex.primitive(i32, len=4) stack_parent=vortex.slice(i32, len=2) slot=0 builder_active=false - pop_frame slot=0 output=vortex.slice(i32, len=2) - iter 4 current=vortex.slice(i32, len=2) builder_active=false - optimize root=vortex.slice(i32, len=2) session=false - reduce_parent static:SliceReduceAdaptor(Primitive) slot=0 parent=vortex.slice(i32, len=2) child=vortex.primitive(i32, len=4) -> vortex.primitive(i32, len=2) - done output=vortex.primitive(i32, len=2) + iter 2 current=vortex.primitive(i32, len=4) stack_parent=vortex.filter(i32, len=2) slot=0 builder_active=false + pop_frame slot=0 output=vortex.filter(i32, len=2) + iter 3 current=vortex.filter(i32, len=2) builder_active=false Done array=vortex.primitive(i32, len=2) - iter 5 current=vortex.primitive(i32, len=2) builder_active=false + iter 4 current=vortex.primitive(i32, len=2) builder_active=false return output=vortex.primitive(i32, len=2) ");