Skip to main content

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