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;
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>(&current_array));
178
179        for _iteration in 0..max_iterations {
180            trace_op!(record_execute_until_iteration(
181                _iteration,
182                &current_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(&current_array);
194            let done_canonical = AnyCanonical::matches(&current_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(&current_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, &current_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                        &current_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                    &current_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(&current_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                    &current_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(&current_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(&current_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}