vortex_array/executor.rs
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! Iterative array execution.
5//!
6//! The single-step [`Executable`] implementation for [`ArrayRef`] tries `reduce`,
7//! `reduce_parent`, `execute_parent`, then `execute` once. The matcher-driven
8//! [`ArrayRef::execute_until`] loop interprets [`ExecutionStep::ExecuteSlot`],
9//! [`ExecutionStep::AppendChild`], and [`ExecutionStep::Done`] using an explicit stack plus an
10//! optional builder, so encodings can advance without recursive descent.
11//!
12//! See <https://docs.vortex.dev/developer-guide/internals/execution> for the full execution
13//! narrative, diagrams, and walkthroughs.
14
15use std::env::VarError;
16use std::fmt;
17use std::fmt::Display;
18use std::sync::Arc;
19use std::sync::LazyLock;
20use std::sync::OnceLock;
21#[cfg(debug_assertions)]
22use std::sync::atomic::AtomicUsize;
23#[cfg(debug_assertions)]
24use std::sync::atomic::Ordering;
25
26use vortex_error::VortexExpect;
27use vortex_error::VortexResult;
28use vortex_error::vortex_bail;
29use vortex_error::vortex_ensure;
30use vortex_error::vortex_panic;
31use vortex_session::VortexSession;
32
33use crate::AnyCanonical;
34use crate::ArrayRef;
35use crate::Canonical;
36use crate::IntoArray;
37use crate::array::ArrayId;
38use crate::builders::ArrayBuilder;
39use crate::builders::builder_with_capacity_in;
40use crate::dtype::DType;
41use crate::matcher::Matcher;
42use crate::memory::BufferAllocatorRef;
43use crate::memory::MemorySessionExt;
44use crate::optimizer::ArrayOptimizer;
45use crate::optimizer::kernels::ArrayKernelsExt;
46use crate::optimizer::kernels::ParentExecutionKernels;
47use crate::optimizer::kernels::execute_parent_key;
48use crate::stats::ArrayStats;
49use crate::stats::StatsSet;
50use crate::trace_op;
51
52/// Returns the maximum number of iterations to attempt when executing an array before giving up and returning
53/// an error, can be by the `VORTEX_MAX_ITERATIONS` env variables, otherwise defaults to 2^22.
54pub(crate) fn max_iterations() -> usize {
55 static MAX_ITERATIONS: LazyLock<usize> =
56 LazyLock::new(|| match std::env::var("VORTEX_MAX_ITERATIONS") {
57 Ok(val) => val.parse::<usize>().unwrap_or_else(|e| {
58 vortex_panic!("VORTEX_MAX_ITERATIONS is not a valid usize: {e}")
59 }),
60 Err(VarError::NotPresent) => 2 << 21, // 2 ^ 22
61 Err(VarError::NotUnicode(_)) => {
62 vortex_panic!("VORTEX_MAX_ITERATIONS is not a valid unicode string")
63 }
64 });
65 *MAX_ITERATIONS
66}
67
68/// Marker trait for types that an [`ArrayRef`] can be executed into.
69///
70/// Implementors must provide an implementation of `execute` that takes
71/// an [`ArrayRef`] and an [`ExecutionCtx`], and produces an instance of the
72/// implementor type.
73///
74/// Users should use the `Array::execute` or `Array::execute_as` methods
75pub trait Executable: Sized {
76 fn execute(array: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Self>;
77}
78
79#[expect(clippy::same_name_method)]
80impl ArrayRef {
81 /// Execute this array to produce an instance of `E`.
82 ///
83 /// See the [`Executable`] implementation for details on how this execution is performed.
84 pub fn execute<E: Executable>(self, ctx: &mut ExecutionCtx) -> VortexResult<E> {
85 E::execute(self, ctx)
86 }
87
88 /// Execute this array, labeling the execution step with a name for tracing.
89 pub fn execute_as<E: Executable>(
90 self,
91 _name: &'static str,
92 ctx: &mut ExecutionCtx,
93 ) -> VortexResult<E> {
94 E::execute(self, ctx)
95 }
96
97 /// Iteratively execute this array until the [`Matcher`] matches, using an explicit work
98 /// stack plus an optional builder for `AppendChild`.
99 ///
100 /// Note: the returned array may not match `M`. If execution converges to a canonical form
101 /// that does not match `M`, the canonical array is returned since no further execution
102 /// progress is possible.
103 ///
104 /// For safety, this errors once execution reaches a configurable maximum number of
105 /// iterations (default `2^22`, override with `VORTEX_MAX_ITERATIONS`).
106 ///
107 /// # Loop state
108 ///
109 /// - `current_array: ArrayRef` -- the array currently in focus.
110 /// - `current_builder: Option<Box<dyn ArrayBuilder>>` -- active only for builder-mode
111 /// execution. `AppendChild` appends detached children here. `Done` finishes the builder
112 /// and turns it back into the next `current_array`.
113 /// - `stack: Vec<StackFrame>` -- suspended parents from `ExecuteSlot`, including the
114 /// detached slot index, its [`DonePredicate`], and the parent builder that was active
115 /// before focus moved into the child.
116 ///
117 /// Example after `ExecuteSlot(1, pred)` has focused slot 1 of a parent:
118 ///
119 /// ```text
120 /// stack[top].parent_array:
121 /// RunEnd <-- suspended parent
122 /// +-- slot 0: ends
123 /// +-- slot 1: _ (detached)
124 ///
125 /// current_array:
126 /// DictEncoding <-- focused child
127 /// +-- slot 0: codes
128 /// +-- slot 1: dictionary
129 ///
130 /// current_builder:
131 /// None
132 /// ```
133 ///
134 /// Each loop iteration works like this:
135 ///
136 /// ```text
137 /// loop:
138 /// Step 1: done(current_array)?
139 /// - root activation -> return current_array
140 /// - ExecuteSlot frame -> pop, reattach child, resume parent
141 ///
142 /// Step 2: current_builder active?
143 /// - yes -> skip Step 2a / 2b
144 /// - no -> try parent kernels
145 ///
146 /// Step 2a: if stack.top exists:
147 /// parent = stack.top.parent_array
148 /// child = current_array
149 /// kernels[(parent.encoding_id(), child.encoding_id())]
150 /// .try_execute_parent(child, parent, stack.top.slot_idx)
151 ///
152 /// Step 2b: for child in current_array.children():
153 /// parent = current_array
154 /// kernels[(parent.encoding_id(), child.encoding_id())]
155 /// .try_execute_parent(child, parent, child.slot_idx)
156 ///
157 /// Step 3: match current_array.execute()
158 /// ExecuteSlot(i, pred) -> push parent on stack, focus child `i`
159 /// AppendChild(i) -> detach child `i`, append it into current_builder,
160 /// keep parent as current_array
161 /// Done -> finish current_builder if present, else use returned array
162 /// ```
163 ///
164 /// Step 2a and Step 2b are skipped while `current_builder` is active. `AppendChild`
165 /// partially consumes `current_array`: some slots already live in the builder, so a
166 /// parent rewrite would observe inconsistent state and could discard accumulated builder
167 /// data.
168 #[allow(clippy::cognitive_complexity)]
169 pub fn execute_until<M: Matcher>(self, ctx: &mut ExecutionCtx) -> VortexResult<ArrayRef> {
170 let mut current_array = self;
171 let mut current_builder: Option<Box<dyn ArrayBuilder>> = None;
172 let mut stack: Vec<StackFrame> = Vec::new();
173 let execute_parent_kernels = Arc::clone(&ctx.execute_parent_kernels);
174 let kernels = execute_parent_kernels.as_ref();
175 let max_iterations = max_iterations();
176
177 trace_op!(record_execute_until_start::<M>(¤t_array));
178
179 for _iteration in 0..max_iterations {
180 trace_op!(record_execute_until_iteration(
181 _iteration,
182 ¤t_array,
183 stack
184 .last()
185 .map(|frame| (&frame.parent_array, frame.slot_idx)),
186 current_builder.is_some(),
187 ));
188
189 let is_done = stack
190 .last()
191 .map_or(M::matches as DonePredicate, |frame| frame.done);
192
193 let done_target = is_done(¤t_array);
194 let done_canonical = AnyCanonical::matches(¤t_array);
195 trace_op!(record_execute_until_done_check(done_target, done_canonical));
196
197 if done_target || done_canonical {
198 match stack.pop() {
199 None => {
200 debug_assert!(
201 current_builder.is_none(),
202 "root activation should not retain a builder"
203 );
204 trace_op!(record_execute_until_return(¤t_array));
205 return Ok(current_array);
206 }
207 Some(frame) => {
208 let _slot_idx = frame.slot_idx;
209 (current_array, current_builder) = pop_frame(frame, current_array)?;
210 trace_op!(record_execute_until_pop_frame(_slot_idx, ¤t_array));
211 continue;
212 }
213 }
214 }
215
216 // Step 2a: execute_parent against the suspended parent from ExecuteSlot.
217 //
218 // When executing a child for ExecuteSlot, try execute_parent against
219 // the suspended parent on the stack. This lets kernels like RunEnd's
220 // FilterKernel fire before the child is forced to canonical.
221 //
222 // Skip when a builder is active: the current array has been partially
223 // consumed by AppendChild (some slots are already in the builder), so
224 // a parent rewrite would see inconsistent state and the builder data
225 // would be lost when we restore frame.parent_builder.
226 if current_builder.is_none()
227 && let Some(frame) = stack.last()
228 && let Some(result) = {
229 execute_parent_for_child(
230 "stack_execute_parent",
231 &frame.parent_array,
232 ¤t_array,
233 frame.slot_idx,
234 kernels,
235 ctx,
236 )?
237 }
238 {
239 let frame = stack.pop().vortex_expect("just peeked");
240 let optimized = result.optimize_ctx(ctx.session())?;
241 trace_op!(record_execute_optimized(&result, &optimized));
242 current_array = optimized;
243 current_builder = frame.parent_builder;
244 continue;
245 }
246 if current_builder.is_none() && stack.last().is_some() {
247 trace_op!(record_execute_parent_none(
248 "stack_execute_parent",
249 ¤t_array,
250 ));
251 }
252
253 // Step 2b: execute_parent against current_array's own children.
254 if current_builder.is_none()
255 && let Some(rewritten) = try_execute_parent(¤t_array, kernels, ctx)?
256 {
257 let optimized = rewritten.optimize_ctx(ctx.session())?;
258 trace_op!(record_execute_optimized(&rewritten, &optimized));
259 current_array = optimized;
260 continue;
261 }
262 if current_builder.is_none() {
263 trace_op!(record_execute_parent_none(
264 "child_execute_parent",
265 ¤t_array,
266 ));
267 }
268
269 let expected_len = current_array.len();
270 let expected_dtype = current_array.dtype().clone();
271 let stats = current_array.statistics().to_array_stats();
272 let encoding_id = current_array.encoding_id();
273 trace_op!(record_execute_encoding(¤t_array));
274 let result = current_array.execute_encoding_unchecked(ctx)?;
275 let (array, step) = result.into_parts();
276 match step {
277 ExecutionStep::ExecuteSlot(i, done) => {
278 let (parent, child) = unsafe { array.take_slot_unchecked(i) }?;
279
280 trace_op!(record_execute_slot(i, &parent, &child));
281 stack.push(StackFrame {
282 parent_array: parent,
283 parent_builder: current_builder.take(),
284 slot_idx: i,
285 done,
286 original_dtype: child.dtype().clone(),
287 original_len: child.len(),
288 });
289 current_array = child;
290 current_builder = None;
291 }
292 ExecutionStep::AppendChild(i) => {
293 if current_builder.is_none() {
294 trace_op!(record_builder_start(&array));
295 current_builder = Some(builder_with_capacity_in(
296 array.dtype(),
297 array.len(),
298 ctx.allocator(),
299 ));
300 }
301 let (parent, child) = unsafe { array.take_slot_unchecked(i) }?;
302
303 trace_op!(record_append_child(i, &parent, &child));
304 trace_op!(record_builder_append(&child));
305
306 // TODO(joe)[7674]: replace with a builder kernel registry so we don't
307 // need to go through the VTable append_to_builder indirection.
308 child.append_to_builder(
309 current_builder
310 .as_deref_mut()
311 .vortex_expect("builder must exist"),
312 ctx,
313 )?;
314 current_array = parent;
315 }
316 ExecutionStep::Done => {
317 let had_builder = current_builder.is_some();
318 trace_op!(record_execute_done(&array));
319 (current_array, current_builder) = finalize_done(
320 array,
321 current_builder,
322 expected_len,
323 expected_dtype,
324 stats,
325 encoding_id,
326 )?;
327 if had_builder {
328 trace_op!(record_builder_finish(¤t_array));
329 }
330 }
331 }
332 }
333
334 vortex_bail!(
335 "Exceeded maximum execution iterations ({}) while executing array",
336 max_iterations,
337 )
338 }
339}
340
341struct StackFrame {
342 parent_array: ArrayRef,
343 parent_builder: Option<Box<dyn ArrayBuilder>>,
344 slot_idx: usize,
345 done: DonePredicate,
346 original_dtype: DType,
347 original_len: usize,
348}
349
350/// Execution context for batch CPU compute.
351#[derive(Debug, Clone)]
352pub struct ExecutionCtx {
353 session: VortexSession,
354 // OnceLock avoids cloning the session allocator when a context does not allocate.
355 allocator: OnceLock<BufferAllocatorRef>,
356 execute_parent_kernels: Arc<ParentExecutionKernels>,
357 #[cfg(debug_assertions)]
358 id: usize,
359 #[cfg(debug_assertions)]
360 ops: Vec<String>,
361}
362
363impl ExecutionCtx {
364 /// Create a new execution context with the given session.
365 ///
366 /// This captures a snapshot of the session's execute-parent kernel registry. Kernels
367 /// registered after this context is created are not visible to it; create a new
368 /// [`ExecutionCtx`] after registration to use newly registered kernels.
369 pub fn new(session: VortexSession) -> Self {
370 let execute_parent_kernels = session.kernels().execute_parent_snapshot();
371 Self {
372 session,
373 allocator: OnceLock::new(),
374 execute_parent_kernels,
375 #[cfg(debug_assertions)]
376 id: {
377 static EXEC_CTX_ID: AtomicUsize = AtomicUsize::new(0);
378 EXEC_CTX_ID.fetch_add(1, Ordering::Relaxed)
379 },
380 #[cfg(debug_assertions)]
381 ops: Vec::new(),
382 }
383 }
384
385 /// Get the session associated with this execution context.
386 pub fn session(&self) -> &VortexSession {
387 &self.session
388 }
389
390 /// Get the allocator for this execution context.
391 pub fn allocator(&self) -> &BufferAllocatorRef {
392 self.allocator.get_or_init(|| self.session.allocator())
393 }
394
395 /// Set the allocator for this execution context.
396 pub fn with_allocator(mut self, allocator: BufferAllocatorRef) -> Self {
397 self.allocator = OnceLock::from(allocator);
398 self
399 }
400
401 /// Log an execution step at the current depth.
402 ///
403 /// Steps are accumulated and dumped as a single trace on Drop at DEBUG level.
404 /// Individual steps are also logged at TRACE level for real-time following.
405 ///
406 /// Use the [`format_args!`] macro to create the `msg` argument.
407 pub fn log(&mut self, msg: fmt::Arguments<'_>) {
408 #[cfg(debug_assertions)]
409 if tracing::enabled!(tracing::Level::TRACE) {
410 let formatted = format!(" - {msg}");
411 tracing::trace!("exec[{}]: {formatted}", self.id);
412 self.ops.push(formatted);
413 }
414 let _ = msg;
415 }
416}
417
418impl Display for ExecutionCtx {
419 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
420 #[cfg(debug_assertions)]
421 return write!(f, "exec[{}]", self.id);
422 #[cfg(not(debug_assertions))]
423 write!(f, "exec")
424 }
425}
426
427#[cfg(debug_assertions)]
428impl Drop for ExecutionCtx {
429 fn drop(&mut self) {
430 if !self.ops.is_empty() && tracing::enabled!(tracing::Level::DEBUG) {
431 // Unlike itertools `.format()` (panics in 0.14 on second format)
432 struct FmtOps<'a>(&'a [String]);
433 impl Display for FmtOps<'_> {
434 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
435 for (i, op) in self.0.iter().enumerate() {
436 if i > 0 {
437 f.write_str("\n")?;
438 }
439 f.write_str(op)?;
440 }
441 Ok(())
442 }
443 }
444 tracing::debug!("exec[{}] trace:\n{}", self.id, FmtOps(&self.ops));
445 }
446 }
447}
448
449/// Single-step execution: takes one step toward canonical form.
450///
451/// Steps through reduce, reduce_parent, execute_parent, then execute. For `ExecuteSlot`,
452/// only a single child execution step is performed — the child is executed once and put back,
453/// making this a lightweight, bounded operation.
454///
455/// **However**, if `execute_step` returns [`ExecutionStep::AppendChild`], this implementation
456/// drives the *entire* array to completion via [`execute_into_builder`] in a single call.
457/// This can do substantially more work than a normal step because it creates a builder and
458/// fully decodes the array into that builder before returning. Callers should be aware that a
459/// single `.execute::<ArrayRef>(ctx)` call may perform O(n_children * decode_cost) work when
460/// `AppendChild` is returned.
461impl Executable for ArrayRef {
462 fn execute(array: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Self> {
463 trace_op!(record_single_step_start(&array));
464
465 if let Some(canonical) = array.as_opt::<AnyCanonical>() {
466 let output = Canonical::from(canonical).into_array();
467 trace_op!(record_single_step_applied("canonical", &array, &output));
468 return Ok(output);
469 }
470 trace_op!(record_single_step_phase_none("canonical", &array));
471
472 if let Some(reduced) = array.reduce()? {
473 reduced.statistics().inherit_from(array.statistics());
474 trace_op!(record_single_step_applied("reduce", &array, &reduced));
475 return Ok(reduced);
476 }
477 trace_op!(record_single_step_phase_none("reduce", &array));
478
479 for (slot_idx, slot) in array.slots().iter().enumerate() {
480 let Some(child) = slot else { continue };
481 if let Some(reduced_parent) = child.reduce_parent(&array, slot_idx)? {
482 reduced_parent.statistics().inherit_from(array.statistics());
483 trace_op!(record_single_step_applied(
484 "reduce_parent",
485 &array,
486 &reduced_parent,
487 ));
488 return Ok(reduced_parent);
489 }
490 }
491 trace_op!(record_single_step_phase_none("reduce_parent", &array));
492
493 let execute_parent_kernels = Arc::clone(&ctx.execute_parent_kernels);
494 let kernels = execute_parent_kernels.as_ref();
495
496 for (slot_idx, slot) in array.slots().iter().enumerate() {
497 let Some(child) = slot else { continue };
498 if let Some(executed_parent) = execute_parent_for_child(
499 "single_step_execute_parent",
500 &array,
501 child,
502 slot_idx,
503 kernels,
504 ctx,
505 )? {
506 ctx.log(format_args!(
507 "execute_parent: slot[{}]({}) rewrote {} -> {}",
508 slot_idx,
509 child.encoding_id(),
510 array,
511 executed_parent
512 ));
513 executed_parent
514 .statistics()
515 .inherit_from(array.statistics());
516 trace_op!(record_single_step_applied(
517 "execute_parent",
518 &array,
519 &executed_parent,
520 ));
521 return Ok(executed_parent);
522 }
523 }
524 trace_op!(record_single_step_phase_none("execute_parent", &array));
525 trace_op!(record_execute_encoding(&array));
526
527 let result = array.execute_encoding(ctx)?;
528 let (array, step) = result.into_parts();
529 match step {
530 ExecutionStep::Done => {
531 trace_op!(record_execute_done(&array));
532 Ok(array)
533 }
534 ExecutionStep::ExecuteSlot(i, _) => {
535 let child = array.slots()[i].clone().vortex_expect("valid slot index");
536 let executed_child = child.execute::<ArrayRef>(ctx)?;
537 // SAFETY: execution of a child slot produces a logically equivalent array in a
538 // different physical representation, preserving parent values and statistics.
539 unsafe { array.with_slot(i, executed_child) }
540 }
541 ExecutionStep::AppendChild(_) => {
542 // Single-step: build the entire parent via the builder path.
543 trace_op!(record_builder_start(&array));
544 let builder = builder_with_capacity_in(array.dtype(), array.len(), ctx.allocator());
545 let mut builder = execute_into_builder(array, builder, ctx)?;
546 let output = builder.finish();
547 trace_op!(record_builder_finish(&output));
548 Ok(output)
549 }
550 }
551 }
552}
553
554/// Execute `array` into the given `builder`.
555///
556/// This uses the encoding's [`crate::array::VTable::append_to_builder`] implementation. Most
557/// encodings use the default path of `execute::<Canonical>` followed by re-dispatching
558/// `append_to_builder` on the canonical array, while encodings like `Chunked` can override that to
559/// append child-by-child without materializing the entire parent.
560///
561/// The builder must have a [`DType`] that is a nullability-superset of `array.dtype()`.
562pub fn execute_into_builder(
563 array: ArrayRef,
564 mut builder: Box<dyn ArrayBuilder>,
565 ctx: &mut ExecutionCtx,
566) -> VortexResult<Box<dyn ArrayBuilder>> {
567 array.append_to_builder(builder.as_mut(), ctx)?;
568 Ok(builder)
569}
570
571/// Pop a stack frame, restoring the parent with the finished child in its slot.
572fn pop_frame(
573 frame: StackFrame,
574 child: ArrayRef,
575) -> VortexResult<(ArrayRef, Option<Box<dyn ArrayBuilder>>)> {
576 debug_assert_eq!(
577 child.dtype(),
578 &frame.original_dtype,
579 "child dtype changed during execution"
580 );
581 debug_assert_eq!(
582 child.len(),
583 frame.original_len,
584 "child len changed during execution"
585 );
586 let parent_array = unsafe { frame.parent_array.put_slot_unchecked(frame.slot_idx, child) }?;
587 Ok((parent_array, frame.parent_builder))
588}
589
590fn finalize_done(
591 result: ArrayRef,
592 mut builder: Option<Box<dyn ArrayBuilder>>,
593 expected_len: usize,
594 expected_dtype: DType,
595 stats: ArrayStats,
596 encoding_id: ArrayId,
597) -> VortexResult<(ArrayRef, Option<Box<dyn ArrayBuilder>>)> {
598 let output = if let Some(mut builder) = builder.take() {
599 builder.finish()
600 } else {
601 result
602 };
603
604 if cfg!(debug_assertions) {
605 vortex_ensure!(
606 output.len() == expected_len,
607 "Result length mismatch for {:?}",
608 encoding_id
609 );
610 vortex_ensure!(
611 output.dtype() == &expected_dtype,
612 "Executed canonical dtype mismatch for {:?}",
613 encoding_id
614 );
615 }
616
617 output
618 .statistics()
619 .set_iter(StatsSet::from(stats).into_iter());
620 Ok((output, None))
621}
622
623fn execute_parent_for_child(
624 _phase: &'static str,
625 parent: &ArrayRef,
626 child: &ArrayRef,
627 slot_idx: usize,
628 kernels: &ParentExecutionKernels,
629 ctx: &mut ExecutionCtx,
630) -> VortexResult<Option<ArrayRef>> {
631 let key = execute_parent_key(parent.encoding_id(), child.encoding_id());
632 if let Some(plugins) = kernels.get(&key) {
633 #[allow(clippy::unused_enumerate_index)]
634 for (_plugin_idx, plugin) in plugins.as_ref().iter().enumerate() {
635 if let Some(result) = plugin.execute_parent(child, parent, slot_idx, ctx)? {
636 if cfg!(debug_assertions) {
637 vortex_ensure!(
638 result.len() == parent.len(),
639 "Executed parent canonical length mismatch"
640 );
641 vortex_ensure!(
642 result.dtype() == parent.dtype(),
643 "Executed parent canonical dtype mismatch"
644 );
645 }
646 trace_op!(record_session_execute_parent_applied(
647 _phase,
648 parent,
649 child,
650 slot_idx,
651 _plugin_idx,
652 &result,
653 ));
654 return Ok(Some(result));
655 }
656 trace_op!(record_session_execute_parent_declined(
657 _phase,
658 parent,
659 child,
660 slot_idx,
661 _plugin_idx,
662 ));
663 }
664 }
665
666 Ok(None)
667}
668
669/// Try execute_parent on each occupied slot of the array.
670fn try_execute_parent(
671 array: &ArrayRef,
672 kernels: &ParentExecutionKernels,
673 ctx: &mut ExecutionCtx,
674) -> VortexResult<Option<ArrayRef>> {
675 for (slot_idx, slot) in array.slots().iter().enumerate() {
676 let Some(child) = slot else { continue };
677 if let Some(executed_parent) =
678 execute_parent_for_child("child_execute_parent", array, child, slot_idx, kernels, ctx)?
679 {
680 ctx.log(format_args!(
681 "execute_parent: slot[{}]({}) rewrote {} -> {}",
682 slot_idx,
683 child.encoding_id(),
684 array,
685 executed_parent
686 ));
687 executed_parent
688 .statistics()
689 .inherit_from(array.statistics());
690 return Ok(Some(executed_parent));
691 }
692 }
693 Ok(None)
694}
695
696/// A predicate that determines when an array has reached a desired form during execution.
697pub type DonePredicate = fn(&ArrayRef) -> bool;
698
699/// Scheduler step indicator returned alongside an array in [`ExecutionResult`].
700///
701/// Instead of recursively executing children, encodings return an `ExecutionStep` that tells the
702/// scheduler what to do next. This enables the scheduler to manage execution iteratively using
703/// an explicit work stack plus an optional builder.
704///
705/// # Semantics
706///
707/// Each variant describes a different execution strategy with distinct cost profiles:
708///
709/// - [`Done`](ExecutionStep::Done): The current activation has finished its work. If no builder
710/// is active, the returned array is the result. If a builder is active, the scheduler ignores
711/// the placeholder array and finishes the builder instead. The scheduler may continue
712/// executing if the target form (e.g. canonical) has not yet been reached.
713///
714/// - [`ExecuteSlot`](ExecutionStep::ExecuteSlot): The encoding needs one of its children
715/// decoded before it can make further progress. The scheduler detaches that child, pushes
716/// the parent onto the explicit stack, executes the child until the [`DonePredicate`]
717/// matches, puts it back, and re-enters the parent. This is a cooperative yield: the
718/// encoding does a bounded amount of work per step while the loop tracks the parent-child
719/// relationship explicitly.
720///
721/// - [`AppendChild`](ExecutionStep::AppendChild): The encoding needs one child executed to
722/// canonical form and then appended into a builder owned by the current activation. The
723/// scheduler detaches that child, lazily creates `current_builder` if needed, appends the
724/// child into it, and keeps the parent as `current_array` for the next iteration. While the
725/// builder is active, parent-kernel rewrites are skipped because the parent is partially
726/// consumed. **Important:** in the single-step executor ([`Executable`] for [`ArrayRef`]),
727/// returning `AppendChild` still causes the executor to drive the *entire* array to
728/// completion via [`execute_into_builder`] in one call — this can do significantly more
729/// work than a single `ExecuteSlot` step.
730pub enum ExecutionStep {
731 /// Request that the scheduler execute the slot at the given index, using the provided
732 /// [`DonePredicate`] to determine when the slot is "done", then replace the slot in this
733 /// array and re-enter execution.
734 ///
735 /// Use [`ExecutionResult::execute_slot`] instead of constructing this variant directly.
736 ExecuteSlot(usize, DonePredicate),
737
738 /// Detach the slot at the given index, append that child into the current activation's
739 /// canonical builder, and keep the returned parent as `current_array`.
740 ///
741 /// `Done` finalizes that builder and turns it into the result of the activation.
742 ///
743 /// **Note:** In the single-step executor ([`Executable`] for [`ArrayRef`]), this variant
744 /// drives the entire parent to completion in one call via [`execute_into_builder`], which
745 /// may perform substantially more work than a single `ExecuteSlot` step.
746 AppendChild(usize),
747
748 /// Execution is complete. If no builder is active, the array in the accompanying
749 /// [`ExecutionResult`] is the result. Otherwise, the scheduler finalizes the active
750 /// builder and uses that finished array instead.
751 ///
752 /// The scheduler will continue executing if it has not yet reached the target form.
753 Done,
754}
755
756impl fmt::Debug for ExecutionStep {
757 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
758 match self {
759 ExecutionStep::ExecuteSlot(idx, _) => f.debug_tuple("ExecuteSlot").field(idx).finish(),
760 ExecutionStep::AppendChild(idx) => f.debug_tuple("AppendChild").field(idx).finish(),
761 ExecutionStep::Done => write!(f, "Done"),
762 }
763 }
764}
765
766/// The result of a single execution step on an array encoding.
767///
768/// Combines an [`ArrayRef`] with an [`ExecutionStep`] to tell the scheduler both what to do next
769/// and what array to work with.
770pub struct ExecutionResult {
771 array: ArrayRef,
772 step: ExecutionStep,
773}
774
775impl ExecutionResult {
776 /// Signal that execution is complete with the given result array.
777 pub fn done(result: impl IntoArray) -> Self {
778 Self {
779 array: result.into_array(),
780 step: ExecutionStep::Done,
781 }
782 }
783
784 /// Request execution of slot at `slot_idx` until it matches the given [`Matcher`].
785 ///
786 /// The provided array is the (possibly modified) parent that still needs its slot executed.
787 pub fn execute_slot<M: Matcher>(array: impl IntoArray, slot_idx: usize) -> Self {
788 let array = array.into_array();
789 Self {
790 array,
791 step: ExecutionStep::ExecuteSlot(slot_idx, M::matches),
792 }
793 }
794
795 /// Request that the child slot at `slot_idx` be detached, appended into the current
796 /// activation's canonical builder, and leave the returned parent as the next
797 /// `current_array`.
798 pub fn append_child(array: impl IntoArray, slot_idx: usize) -> Self {
799 let array = array.into_array();
800 Self {
801 array,
802 step: ExecutionStep::AppendChild(slot_idx),
803 }
804 }
805
806 /// Returns a reference to the array.
807 pub fn array(&self) -> &ArrayRef {
808 &self.array
809 }
810
811 /// Returns a reference to the step.
812 pub fn step(&self) -> &ExecutionStep {
813 &self.step
814 }
815
816 /// Decompose into parts.
817 pub fn into_parts(self) -> (ArrayRef, ExecutionStep) {
818 (self.array, self.step)
819 }
820}
821
822impl fmt::Debug for ExecutionResult {
823 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
824 f.debug_struct("ExecutionResult")
825 .field("array", &self.array)
826 .field("step", &self.step)
827 .finish()
828 }
829}
830
831/// Require that a child array matches `$M`. If the child already matches, returns the same
832/// array unchanged. Otherwise, early-returns an [`ExecutionResult`] requesting execution of
833/// child `$idx` until it matches `$M`.
834///
835/// ```ignore
836/// let array = require_child!(array, array.codes(), 0 => Primitive);
837/// let array = require_child!(array, array.values(), 1 => AnyCanonical);
838/// ```
839#[macro_export]
840macro_rules! require_child {
841 ($parent:expr, $child:expr, $idx:expr => $M:ty) => {{
842 if !$child.is::<$M>() {
843 return Ok($crate::ExecutionResult::execute_slot::<$M>(
844 $parent.clone(),
845 $idx,
846 ));
847 }
848 $parent
849 }};
850}
851
852/// Like [`require_child!`], but for optional children. If the child is `None`, this is a no-op.
853/// If the child is `Some` but does not match `$M`, early-returns an [`ExecutionResult`] requesting
854/// execution of child `$idx`.
855///
856/// Unlike `require_child!`, this is a statement macro (no value produced) and does not clone
857/// `$parent` - it is moved into the early-return path.
858///
859/// ```ignore
860/// require_opt_child!(array, array.patches().map(|p| p.indices()), 1 => Primitive);
861/// ```
862#[macro_export]
863macro_rules! require_opt_child {
864 ($parent:expr, $child_opt:expr, $idx:expr => $M:ty) => {
865 if $child_opt.is_some_and(|child| !child.is::<$M>()) {
866 return Ok($crate::ExecutionResult::execute_slot::<$M>($parent, $idx));
867 }
868 };
869}
870
871/// Require that patch slots (indices, values, and optionally chunk_offsets) are `Primitive`.
872/// If no patches are present (slots are `None`), this is a no-op.
873///
874/// Like [`require_opt_child!`], `$parent` is moved (not cloned) into the early-return path.
875///
876/// ```ignore
877/// require_patches!(
878/// array,
879/// MySlots::PATCH_INDICES,
880/// MySlots::PATCH_VALUES,
881/// MySlots::PATCH_CHUNK_OFFSETS
882/// );
883/// ```
884#[macro_export]
885macro_rules! require_patches {
886 ($parent:expr, $indices_slot:expr, $values_slot:expr, $chunk_offsets_slot:expr) => {
887 $crate::require_opt_child!(
888 $parent,
889 $parent.slots()[$indices_slot].as_ref(),
890 $indices_slot => $crate::arrays::Primitive
891 );
892 $crate::require_opt_child!(
893 $parent,
894 $parent.slots()[$values_slot].as_ref(),
895 $values_slot => $crate::arrays::Primitive
896 );
897 $crate::require_opt_child!(
898 $parent,
899 $parent.slots()[$chunk_offsets_slot].as_ref(),
900 $chunk_offsets_slot => $crate::arrays::Primitive
901 );
902 };
903}
904
905/// Require that the validity slot is a [`Bool`](crate::arrays::Bool) array. If validity is not
906/// array-backed (e.g. `NonNullable` or `AllValid`), this is a no-op. If it is array-backed but
907/// not `Bool`, early-returns an [`ExecutionResult`] requesting execution of the validity slot.
908///
909/// Like [`require_opt_child!`], `$parent` is moved (not cloned) into the early-return path.
910///
911/// ```ignore
912/// require_validity!(array, MySlots::VALIDITY);
913/// ```
914#[macro_export]
915macro_rules! require_validity {
916 ($parent:expr, $idx:expr) => {
917 $crate::require_opt_child!(
918 $parent,
919 $parent.slots()[$idx].as_ref(),
920 $idx => $crate::arrays::Bool
921 );
922 };
923}
924
925/// Extension trait for creating an execution context from a session.
926pub trait VortexSessionExecute {
927 /// Create a new execution context from this session.
928 fn create_execution_ctx(&self) -> ExecutionCtx;
929}
930
931impl VortexSessionExecute for VortexSession {
932 fn create_execution_ctx(&self) -> ExecutionCtx {
933 ExecutionCtx::new(self.clone())
934 }
935}
936
937#[cfg(test)]
938mod tests {
939 use static_assertions::assert_impl_all;
940 use vortex_session::SessionExt;
941 use vortex_session::VortexSession;
942
943 use super::*;
944 use crate::VTable as _;
945 use crate::VortexSessionExecute;
946 use crate::arrays::Bool;
947 use crate::arrays::Primitive;
948 use crate::memory::BufferAllocatorRef;
949 use crate::memory::MemorySession;
950 use crate::memory::MemorySessionExt;
951 use crate::optimizer::kernels::ExecuteParentFn;
952 use crate::optimizer::kernels::KernelSession;
953 use crate::optimizer::kernels::execute_parent_key;
954
955 assert_impl_all!(ExecutionCtx: Send, Sync);
956
957 fn noop_execute_parent(
958 _child: &ArrayRef,
959 _parent: &ArrayRef,
960 _child_idx: usize,
961 _ctx: &mut ExecutionCtx,
962 ) -> VortexResult<Option<ArrayRef>> {
963 Ok(None)
964 }
965
966 #[test]
967 fn execution_ctx_snapshots_execute_parent_kernels_at_creation() {
968 let session = VortexSession::empty().with_some(KernelSession::empty());
969 let key = execute_parent_key(Bool.id(), Primitive.id());
970
971 let before_registration = session.create_execution_ctx();
972 assert!(
973 !before_registration
974 .execute_parent_kernels
975 .contains_key(&key)
976 );
977
978 let kernels = session.kernels();
979 kernels.register_execute_parent(
980 Bool.id(),
981 Primitive.id(),
982 &[noop_execute_parent as ExecuteParentFn],
983 );
984
985 assert!(
986 !before_registration
987 .execute_parent_kernels
988 .contains_key(&key)
989 );
990
991 let after_registration = session.create_execution_ctx();
992 assert!(after_registration.execute_parent_kernels.contains_key(&key));
993 }
994
995 #[test]
996 fn execution_ctx_allocator_override() {
997 let first = BufferAllocatorRef::new(vortex_buffer::StaticBufferAllocator);
998 let second = BufferAllocatorRef::new(vortex_buffer::StaticBufferAllocator);
999 let third = BufferAllocatorRef::new(vortex_buffer::StaticBufferAllocator);
1000 let session = VortexSession::empty()
1001 .with::<MemorySession>()
1002 .with_allocator(first.clone());
1003 let ctx = session.create_execution_ctx();
1004
1005 session
1006 .get_mut::<MemorySession>()
1007 .set_allocator(third.clone());
1008
1009 assert!(session.allocator().ptr_eq(&third));
1010 assert!(ctx.allocator().ptr_eq(&third));
1011
1012 let ctx = ctx.with_allocator(second.clone());
1013 session.get_mut::<MemorySession>().set_allocator(first);
1014
1015 assert!(ctx.allocator().ptr_eq(&second));
1016 }
1017}