use std::env::VarError;
use std::fmt;
use std::fmt::Display;
use std::sync::Arc;
use std::sync::LazyLock;
#[cfg(debug_assertions)]
use std::sync::atomic::AtomicUsize;
#[cfg(debug_assertions)]
use std::sync::atomic::Ordering;
use vortex_error::VortexExpect;
use vortex_error::VortexResult;
use vortex_error::vortex_bail;
use vortex_error::vortex_ensure;
use vortex_error::vortex_panic;
use vortex_session::VortexSession;
use crate::AnyCanonical;
use crate::ArrayRef;
use crate::Canonical;
use crate::IntoArray;
use crate::array::ArrayId;
use crate::builders::ArrayBuilder;
use crate::builders::builder_with_capacity_in;
use crate::dtype::DType;
use crate::matcher::Matcher;
use crate::memory::HostAllocatorRef;
use crate::memory::MemorySessionExt;
use crate::optimizer::ArrayOptimizer;
use crate::optimizer::kernels::ArrayKernelsExt;
use crate::optimizer::kernels::ParentExecutionKernels;
use crate::optimizer::kernels::execute_parent_key;
use crate::stats::ArrayStats;
use crate::stats::StatsSet;
use crate::trace_op;
pub(crate) fn max_iterations() -> usize {
static MAX_ITERATIONS: LazyLock<usize> =
LazyLock::new(|| match std::env::var("VORTEX_MAX_ITERATIONS") {
Ok(val) => val.parse::<usize>().unwrap_or_else(|e| {
vortex_panic!("VORTEX_MAX_ITERATIONS is not a valid usize: {e}")
}),
Err(VarError::NotPresent) => 2 << 21, Err(VarError::NotUnicode(_)) => {
vortex_panic!("VORTEX_MAX_ITERATIONS is not a valid unicode string")
}
});
*MAX_ITERATIONS
}
pub trait Executable: Sized {
fn execute(array: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Self>;
}
#[expect(clippy::same_name_method)]
impl ArrayRef {
pub fn execute<E: Executable>(self, ctx: &mut ExecutionCtx) -> VortexResult<E> {
E::execute(self, ctx)
}
pub fn execute_as<E: Executable>(
self,
_name: &'static str,
ctx: &mut ExecutionCtx,
) -> VortexResult<E> {
E::execute(self, ctx)
}
#[allow(clippy::cognitive_complexity)]
pub fn execute_until<M: Matcher>(self, ctx: &mut ExecutionCtx) -> VortexResult<ArrayRef> {
let mut current_array = self;
let mut current_builder: Option<Box<dyn ArrayBuilder>> = None;
let mut stack: Vec<StackFrame> = Vec::new();
let execute_parent_kernels = Arc::clone(&ctx.execute_parent_kernels);
let kernels = execute_parent_kernels.as_ref();
let max_iterations = max_iterations();
trace_op!(record_execute_until_start::<M>(¤t_array));
for _iteration in 0..max_iterations {
trace_op!(record_execute_until_iteration(
_iteration,
¤t_array,
stack
.last()
.map(|frame| (&frame.parent_array, frame.slot_idx)),
current_builder.is_some(),
));
let is_done = stack
.last()
.map_or(M::matches as DonePredicate, |frame| frame.done);
let done_target = is_done(¤t_array);
let done_canonical = AnyCanonical::matches(¤t_array);
trace_op!(record_execute_until_done_check(done_target, done_canonical));
if done_target || done_canonical {
match stack.pop() {
None => {
debug_assert!(
current_builder.is_none(),
"root activation should not retain a builder"
);
trace_op!(record_execute_until_return(¤t_array));
return Ok(current_array);
}
Some(frame) => {
let _slot_idx = frame.slot_idx;
(current_array, current_builder) = pop_frame(frame, current_array)?;
trace_op!(record_execute_until_pop_frame(_slot_idx, ¤t_array));
continue;
}
}
}
if current_builder.is_none()
&& let Some(frame) = stack.last()
&& let Some(result) = {
execute_parent_for_child(
"stack_execute_parent",
&frame.parent_array,
¤t_array,
frame.slot_idx,
kernels,
ctx,
)?
}
{
let frame = stack.pop().vortex_expect("just peeked");
let optimized = result.optimize_ctx(ctx.session())?;
trace_op!(record_execute_optimized(&result, &optimized));
current_array = optimized;
current_builder = frame.parent_builder;
continue;
}
if current_builder.is_none() && stack.last().is_some() {
trace_op!(record_execute_parent_none(
"stack_execute_parent",
¤t_array,
));
}
if current_builder.is_none()
&& let Some(rewritten) = try_execute_parent(¤t_array, kernels, ctx)?
{
let optimized = rewritten.optimize_ctx(ctx.session())?;
trace_op!(record_execute_optimized(&rewritten, &optimized));
current_array = optimized;
continue;
}
if current_builder.is_none() {
trace_op!(record_execute_parent_none(
"child_execute_parent",
¤t_array,
));
}
let expected_len = current_array.len();
let expected_dtype = current_array.dtype().clone();
let stats = current_array.statistics().to_array_stats();
let encoding_id = current_array.encoding_id();
trace_op!(record_execute_encoding(¤t_array));
let result = current_array.execute_encoding_unchecked(ctx)?;
let (array, step) = result.into_parts();
match step {
ExecutionStep::ExecuteSlot(i, done) => {
let (parent, child) = unsafe { array.take_slot_unchecked(i) }?;
trace_op!(record_execute_slot(i, &parent, &child));
stack.push(StackFrame {
parent_array: parent,
parent_builder: current_builder.take(),
slot_idx: i,
done,
original_dtype: child.dtype().clone(),
original_len: child.len(),
});
current_array = child;
current_builder = None;
}
ExecutionStep::AppendChild(i) => {
if current_builder.is_none() {
trace_op!(record_builder_start(&array));
current_builder = Some(builder_with_capacity_in(
ctx.allocator(),
array.dtype(),
array.len(),
));
}
let (parent, child) = unsafe { array.take_slot_unchecked(i) }?;
trace_op!(record_append_child(i, &parent, &child));
trace_op!(record_builder_append(&child));
child.append_to_builder(
current_builder
.as_deref_mut()
.vortex_expect("builder must exist"),
ctx,
)?;
current_array = parent;
}
ExecutionStep::Done => {
let had_builder = current_builder.is_some();
trace_op!(record_execute_done(&array));
(current_array, current_builder) = finalize_done(
array,
current_builder,
expected_len,
expected_dtype,
stats,
encoding_id,
)?;
if had_builder {
trace_op!(record_builder_finish(¤t_array));
}
}
}
}
vortex_bail!(
"Exceeded maximum execution iterations ({}) while executing array",
max_iterations,
)
}
}
struct StackFrame {
parent_array: ArrayRef,
parent_builder: Option<Box<dyn ArrayBuilder>>,
slot_idx: usize,
done: DonePredicate,
original_dtype: DType,
original_len: usize,
}
#[derive(Debug, Clone)]
pub struct ExecutionCtx {
session: VortexSession,
execute_parent_kernels: Arc<ParentExecutionKernels>,
#[cfg(debug_assertions)]
id: usize,
#[cfg(debug_assertions)]
ops: Vec<String>,
}
impl ExecutionCtx {
pub fn new(session: VortexSession) -> Self {
let execute_parent_kernels = session.kernels().execute_parent_snapshot();
Self {
session,
execute_parent_kernels,
#[cfg(debug_assertions)]
id: {
static EXEC_CTX_ID: AtomicUsize = AtomicUsize::new(0);
EXEC_CTX_ID.fetch_add(1, Ordering::Relaxed)
},
#[cfg(debug_assertions)]
ops: Vec::new(),
}
}
pub fn session(&self) -> &VortexSession {
&self.session
}
pub fn allocator(&self) -> HostAllocatorRef {
self.session.allocator()
}
pub fn log(&mut self, msg: fmt::Arguments<'_>) {
#[cfg(debug_assertions)]
if tracing::enabled!(tracing::Level::TRACE) {
let formatted = format!(" - {msg}");
tracing::trace!("exec[{}]: {formatted}", self.id);
self.ops.push(formatted);
}
let _ = msg;
}
}
impl Display for ExecutionCtx {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
#[cfg(debug_assertions)]
return write!(f, "exec[{}]", self.id);
#[cfg(not(debug_assertions))]
write!(f, "exec")
}
}
#[cfg(debug_assertions)]
impl Drop for ExecutionCtx {
fn drop(&mut self) {
if !self.ops.is_empty() && tracing::enabled!(tracing::Level::DEBUG) {
struct FmtOps<'a>(&'a [String]);
impl Display for FmtOps<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, op) in self.0.iter().enumerate() {
if i > 0 {
f.write_str("\n")?;
}
f.write_str(op)?;
}
Ok(())
}
}
tracing::debug!("exec[{}] trace:\n{}", self.id, FmtOps(&self.ops));
}
}
}
impl Executable for ArrayRef {
fn execute(array: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Self> {
trace_op!(record_single_step_start(&array));
if let Some(canonical) = array.as_opt::<AnyCanonical>() {
let output = Canonical::from(canonical).into_array();
trace_op!(record_single_step_applied("canonical", &array, &output));
return Ok(output);
}
trace_op!(record_single_step_phase_none("canonical", &array));
if let Some(reduced) = array.reduce()? {
reduced.statistics().inherit_from(array.statistics());
trace_op!(record_single_step_applied("reduce", &array, &reduced));
return Ok(reduced);
}
trace_op!(record_single_step_phase_none("reduce", &array));
for (slot_idx, slot) in array.slots().iter().enumerate() {
let Some(child) = slot else { continue };
if let Some(reduced_parent) = child.reduce_parent(&array, slot_idx)? {
reduced_parent.statistics().inherit_from(array.statistics());
trace_op!(record_single_step_applied(
"reduce_parent",
&array,
&reduced_parent,
));
return Ok(reduced_parent);
}
}
trace_op!(record_single_step_phase_none("reduce_parent", &array));
let execute_parent_kernels = Arc::clone(&ctx.execute_parent_kernels);
let kernels = execute_parent_kernels.as_ref();
for (slot_idx, slot) in array.slots().iter().enumerate() {
let Some(child) = slot else { continue };
if let Some(executed_parent) = execute_parent_for_child(
"single_step_execute_parent",
&array,
child,
slot_idx,
kernels,
ctx,
)? {
ctx.log(format_args!(
"execute_parent: slot[{}]({}) rewrote {} -> {}",
slot_idx,
child.encoding_id(),
array,
executed_parent
));
executed_parent
.statistics()
.inherit_from(array.statistics());
trace_op!(record_single_step_applied(
"execute_parent",
&array,
&executed_parent,
));
return Ok(executed_parent);
}
}
trace_op!(record_single_step_phase_none("execute_parent", &array));
trace_op!(record_execute_encoding(&array));
let result = array.execute_encoding(ctx)?;
let (array, step) = result.into_parts();
match step {
ExecutionStep::Done => {
trace_op!(record_execute_done(&array));
Ok(array)
}
ExecutionStep::ExecuteSlot(i, _) => {
let child = array.slots()[i].clone().vortex_expect("valid slot index");
let executed_child = child.execute::<ArrayRef>(ctx)?;
unsafe { array.with_slot(i, executed_child) }
}
ExecutionStep::AppendChild(_) => {
trace_op!(record_builder_start(&array));
let builder = builder_with_capacity_in(ctx.allocator(), array.dtype(), array.len());
let mut builder = execute_into_builder(array, builder, ctx)?;
let output = builder.finish();
trace_op!(record_builder_finish(&output));
Ok(output)
}
}
}
}
pub fn execute_into_builder(
array: ArrayRef,
mut builder: Box<dyn ArrayBuilder>,
ctx: &mut ExecutionCtx,
) -> VortexResult<Box<dyn ArrayBuilder>> {
array.append_to_builder(builder.as_mut(), ctx)?;
Ok(builder)
}
fn pop_frame(
frame: StackFrame,
child: ArrayRef,
) -> VortexResult<(ArrayRef, Option<Box<dyn ArrayBuilder>>)> {
debug_assert_eq!(
child.dtype(),
&frame.original_dtype,
"child dtype changed during execution"
);
debug_assert_eq!(
child.len(),
frame.original_len,
"child len changed during execution"
);
let parent_array = unsafe { frame.parent_array.put_slot_unchecked(frame.slot_idx, child) }?;
Ok((parent_array, frame.parent_builder))
}
fn finalize_done(
result: ArrayRef,
mut builder: Option<Box<dyn ArrayBuilder>>,
expected_len: usize,
expected_dtype: DType,
stats: ArrayStats,
encoding_id: ArrayId,
) -> VortexResult<(ArrayRef, Option<Box<dyn ArrayBuilder>>)> {
let output = if let Some(mut builder) = builder.take() {
builder.finish()
} else {
result
};
if cfg!(debug_assertions) {
vortex_ensure!(
output.len() == expected_len,
"Result length mismatch for {:?}",
encoding_id
);
vortex_ensure!(
output.dtype() == &expected_dtype,
"Executed canonical dtype mismatch for {:?}",
encoding_id
);
}
output
.statistics()
.set_iter(StatsSet::from(stats).into_iter());
Ok((output, None))
}
fn execute_parent_for_child(
_phase: &'static str,
parent: &ArrayRef,
child: &ArrayRef,
slot_idx: usize,
kernels: &ParentExecutionKernels,
ctx: &mut ExecutionCtx,
) -> VortexResult<Option<ArrayRef>> {
let key = execute_parent_key(parent.encoding_id(), child.encoding_id());
if let Some(plugins) = kernels.get(&key) {
#[allow(clippy::unused_enumerate_index)]
for (_plugin_idx, plugin) in plugins.as_ref().iter().enumerate() {
if let Some(result) = plugin.execute_parent(child, parent, slot_idx, ctx)? {
if cfg!(debug_assertions) {
vortex_ensure!(
result.len() == parent.len(),
"Executed parent canonical length mismatch"
);
vortex_ensure!(
result.dtype() == parent.dtype(),
"Executed parent canonical dtype mismatch"
);
}
trace_op!(record_session_execute_parent_applied(
_phase,
parent,
child,
slot_idx,
_plugin_idx,
&result,
));
return Ok(Some(result));
}
trace_op!(record_session_execute_parent_declined(
_phase,
parent,
child,
slot_idx,
_plugin_idx,
));
}
}
Ok(None)
}
fn try_execute_parent(
array: &ArrayRef,
kernels: &ParentExecutionKernels,
ctx: &mut ExecutionCtx,
) -> VortexResult<Option<ArrayRef>> {
for (slot_idx, slot) in array.slots().iter().enumerate() {
let Some(child) = slot else { continue };
if let Some(executed_parent) =
execute_parent_for_child("child_execute_parent", array, child, slot_idx, kernels, ctx)?
{
ctx.log(format_args!(
"execute_parent: slot[{}]({}) rewrote {} -> {}",
slot_idx,
child.encoding_id(),
array,
executed_parent
));
executed_parent
.statistics()
.inherit_from(array.statistics());
return Ok(Some(executed_parent));
}
}
Ok(None)
}
pub type DonePredicate = fn(&ArrayRef) -> bool;
pub enum ExecutionStep {
ExecuteSlot(usize, DonePredicate),
AppendChild(usize),
Done,
}
impl fmt::Debug for ExecutionStep {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ExecutionStep::ExecuteSlot(idx, _) => f.debug_tuple("ExecuteSlot").field(idx).finish(),
ExecutionStep::AppendChild(idx) => f.debug_tuple("AppendChild").field(idx).finish(),
ExecutionStep::Done => write!(f, "Done"),
}
}
}
pub struct ExecutionResult {
array: ArrayRef,
step: ExecutionStep,
}
impl ExecutionResult {
pub fn done(result: impl IntoArray) -> Self {
Self {
array: result.into_array(),
step: ExecutionStep::Done,
}
}
pub fn execute_slot<M: Matcher>(array: impl IntoArray, slot_idx: usize) -> Self {
let array = array.into_array();
Self {
array,
step: ExecutionStep::ExecuteSlot(slot_idx, M::matches),
}
}
pub fn append_child(array: impl IntoArray, slot_idx: usize) -> Self {
let array = array.into_array();
Self {
array,
step: ExecutionStep::AppendChild(slot_idx),
}
}
pub fn array(&self) -> &ArrayRef {
&self.array
}
pub fn step(&self) -> &ExecutionStep {
&self.step
}
pub fn into_parts(self) -> (ArrayRef, ExecutionStep) {
(self.array, self.step)
}
}
impl fmt::Debug for ExecutionResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ExecutionResult")
.field("array", &self.array)
.field("step", &self.step)
.finish()
}
}
#[macro_export]
macro_rules! require_child {
($parent:expr, $child:expr, $idx:expr => $M:ty) => {{
if !$child.is::<$M>() {
return Ok($crate::ExecutionResult::execute_slot::<$M>(
$parent.clone(),
$idx,
));
}
$parent
}};
}
#[macro_export]
macro_rules! require_opt_child {
($parent:expr, $child_opt:expr, $idx:expr => $M:ty) => {
if $child_opt.is_some_and(|child| !child.is::<$M>()) {
return Ok($crate::ExecutionResult::execute_slot::<$M>($parent, $idx));
}
};
}
#[macro_export]
macro_rules! require_patches {
($parent:expr, $indices_slot:expr, $values_slot:expr, $chunk_offsets_slot:expr) => {
$crate::require_opt_child!(
$parent,
$parent.slots()[$indices_slot].as_ref(),
$indices_slot => $crate::arrays::Primitive
);
$crate::require_opt_child!(
$parent,
$parent.slots()[$values_slot].as_ref(),
$values_slot => $crate::arrays::Primitive
);
$crate::require_opt_child!(
$parent,
$parent.slots()[$chunk_offsets_slot].as_ref(),
$chunk_offsets_slot => $crate::arrays::Primitive
);
};
}
#[macro_export]
macro_rules! require_validity {
($parent:expr, $idx:expr) => {
$crate::require_opt_child!(
$parent,
$parent.slots()[$idx].as_ref(),
$idx => $crate::arrays::Bool
);
};
}
pub trait VortexSessionExecute {
fn create_execution_ctx(&self) -> ExecutionCtx;
}
impl VortexSessionExecute for VortexSession {
fn create_execution_ctx(&self) -> ExecutionCtx {
ExecutionCtx::new(self.clone())
}
}
#[cfg(test)]
mod tests {
use vortex_session::VortexSession;
use super::*;
use crate::VTable as _;
use crate::VortexSessionExecute;
use crate::arrays::Bool;
use crate::arrays::Primitive;
use crate::optimizer::kernels::ExecuteParentFn;
use crate::optimizer::kernels::KernelSession;
use crate::optimizer::kernels::execute_parent_key;
fn noop_execute_parent(
_child: &ArrayRef,
_parent: &ArrayRef,
_child_idx: usize,
_ctx: &mut ExecutionCtx,
) -> VortexResult<Option<ArrayRef>> {
Ok(None)
}
#[test]
fn execution_ctx_snapshots_execute_parent_kernels_at_creation() {
let session = VortexSession::empty().with_some(KernelSession::empty());
let key = execute_parent_key(Bool.id(), Primitive.id());
let before_registration = session.create_execution_ctx();
assert!(
!before_registration
.execute_parent_kernels
.contains_key(&key)
);
let kernels = session.kernels();
kernels.register_execute_parent(
Bool.id(),
Primitive.id(),
&[noop_execute_parent as ExecuteParentFn],
);
assert!(
!before_registration
.execute_parent_kernels
.contains_key(&key)
);
let after_registration = session.create_execution_ctx();
assert!(after_registration.execute_parent_kernels.contains_key(&key));
}
}