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