Skip to main content

vortex_array/optimizer/
rules.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! Metadata-only rewrite rules for the optimizer (Layers 1 and 2 of the execution model).
5//!
6//! Reduce rules are the cheapest transformations in the execution pipeline: they operate
7//! purely on array structure and metadata without reading any data buffers.
8//!
9//! There are two kinds of reduce rules:
10//!
11//! - [`ArrayReduceRule`] (Layer 1) -- a self-rewrite where an array simplifies itself.
12//!   Example: a `FilterArray` with an all-true mask removes the filter wrapper.
13//!
14//! - [`ArrayParentReduceRule`] (Layer 2) -- a child-driven rewrite where a child rewrites
15//!   its parent. Example: a `DictArray` child of a `ScalarFnArray` pushes the scalar function
16//!   into the dictionary values.
17//!
18//! Rules are collected into [`ReduceRuleSet`] and [`ParentRuleSet`] respectively, and
19//! evaluated by the optimizer in a fixpoint loop until no more rules apply.
20
21use std::any::type_name;
22use std::fmt::Debug;
23use std::marker::PhantomData;
24
25use vortex_error::VortexResult;
26
27use crate::array::ArrayRef;
28use crate::matcher::Matcher;
29use crate::vtable::VTable;
30
31/// A metadata-only rewrite rule that transforms an array based on its own structure (Layer 1).
32///
33/// These rules look only at the array's metadata and children types (not buffer contents)
34/// and return a structurally simpler replacement, or `None` if the rule doesn't apply.
35pub trait ArrayReduceRule<V: VTable>: Debug + Send + Sync + 'static {
36    /// Attempt to rewrite this array.
37    ///
38    /// Returns:
39    /// - `Ok(Some(new_array))` if the rule applied successfully
40    /// - `Ok(None)` if the rule doesn't apply
41    /// - `Err(e)` if an error occurred
42    fn reduce(&self, array: &V::Array) -> VortexResult<Option<ArrayRef>>;
43}
44
45/// A metadata-only rewrite rule where a child encoding rewrites its parent (Layer 2).
46///
47/// The child sees the parent's type via the associated `Parent` [`Matcher`] and can return
48/// a replacement for the parent. This enables optimizations like pushing operations through
49/// compression layers (e.g., pushing a scalar function into dictionary values).
50pub trait ArrayParentReduceRule<V: VTable>: Debug + Send + Sync + 'static {
51    /// The parent array type this rule matches against.
52    type Parent: Matcher;
53
54    /// Attempt to rewrite this child array given information about its parent.
55    ///
56    /// Returns:
57    /// - `Ok(Some(new_array))` if the rule applied successfully
58    /// - `Ok(None)` if the rule doesn't apply
59    /// - `Err(e)` if an error occurred
60    fn reduce_parent(
61        &self,
62        array: &V::Array,
63        parent: <Self::Parent as Matcher>::Match<'_>,
64        child_idx: usize,
65    ) -> VortexResult<Option<ArrayRef>>;
66}
67
68/// Type-erased version of [`ArrayParentReduceRule`] used for dynamic dispatch within
69/// [`ParentRuleSet`].
70pub trait DynArrayParentReduceRule<V: VTable>: Debug + Send + Sync {
71    fn matches(&self, parent: &ArrayRef) -> bool;
72
73    fn reduce_parent(
74        &self,
75        array: &V::Array,
76        parent: &ArrayRef,
77        child_idx: usize,
78    ) -> VortexResult<Option<ArrayRef>>;
79}
80
81/// Bridges a concrete [`ArrayParentReduceRule<V, R>`] to the type-erased
82/// [`DynArrayParentReduceRule<V>`] trait. Created by [`ParentRuleSet::lift`].
83pub struct ParentReduceRuleAdapter<V, R> {
84    rule: R,
85    _phantom: PhantomData<V>,
86}
87
88impl<V: VTable, R: ArrayParentReduceRule<V>> Debug for ParentReduceRuleAdapter<V, R> {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        f.debug_struct("ArrayParentReduceRuleAdapter")
91            .field("parent", &type_name::<R::Parent>())
92            .field("rule", &self.rule)
93            .finish()
94    }
95}
96
97impl<V: VTable, K: ArrayParentReduceRule<V>> DynArrayParentReduceRule<V>
98    for ParentReduceRuleAdapter<V, K>
99{
100    fn matches(&self, parent: &ArrayRef) -> bool {
101        K::Parent::matches(parent)
102    }
103
104    fn reduce_parent(
105        &self,
106        child: &V::Array,
107        parent: &ArrayRef,
108        child_idx: usize,
109    ) -> VortexResult<Option<ArrayRef>> {
110        let Some(parent_view) = K::Parent::try_match(parent) else {
111            return Ok(None);
112        };
113        self.rule.reduce_parent(child, parent_view, child_idx)
114    }
115}
116
117/// A collection of [`ArrayReduceRule`]s registered for a specific encoding.
118///
119/// During optimization, the optimizer calls [`evaluate`](Self::evaluate) which tries each rule
120/// in order. The first rule that returns `Some` wins.
121pub struct ReduceRuleSet<V: VTable> {
122    rules: &'static [&'static dyn ArrayReduceRule<V>],
123}
124
125impl<V: VTable> ReduceRuleSet<V> {
126    /// Create a new reduction rule set with the given rules.
127    pub const fn new(rules: &'static [&'static dyn ArrayReduceRule<V>]) -> Self {
128        Self { rules }
129    }
130
131    /// Evaluate the reduction rules on the given array.
132    pub fn evaluate(&self, array: &V::Array) -> VortexResult<Option<ArrayRef>> {
133        for rule in self.rules.iter() {
134            if let Some(reduced) = rule.reduce(array)? {
135                return Ok(Some(reduced));
136            }
137        }
138        Ok(None)
139    }
140}
141
142/// A set of parent reduction rules for a specific child array encoding.
143pub struct ParentRuleSet<V: VTable> {
144    rules: &'static [&'static dyn DynArrayParentReduceRule<V>],
145}
146
147impl<V: VTable> ParentRuleSet<V> {
148    /// Create a new parent rule set with the given rules.
149    ///
150    /// Use [`ParentRuleSet::lift`] to lift static rules into dynamic trait objects.
151    pub const fn new(rules: &'static [&'static dyn DynArrayParentReduceRule<V>]) -> Self {
152        Self { rules }
153    }
154
155    /// Lift the given rule into a dynamic trait object.
156    pub const fn lift<R: ArrayParentReduceRule<V>>(
157        rule: &'static R,
158    ) -> &'static dyn DynArrayParentReduceRule<V> {
159        // Assert that self is zero-sized
160        const {
161            assert!(
162                !(size_of::<R>() != 0),
163                "Rule must be zero-sized to be lifted"
164            );
165        }
166        unsafe { &*(rule as *const R as *const ParentReduceRuleAdapter<V, R>) }
167    }
168
169    /// Evaluate the parent reduction rules on the given child and parent arrays.
170    pub fn evaluate(
171        &self,
172        child: &V::Array,
173        parent: &ArrayRef,
174        child_idx: usize,
175    ) -> VortexResult<Option<ArrayRef>> {
176        for rule in self.rules.iter() {
177            if !rule.matches(parent) {
178                continue;
179            }
180            if let Some(reduced) = rule.reduce_parent(child, parent, child_idx)? {
181                // Debug assertions because these checks are already run elsewhere.
182                #[cfg(debug_assertions)]
183                {
184                    vortex_error::vortex_ensure!(
185                        reduced.len() == parent.len(),
186                        "Reduced array length mismatch from {:?}\nFrom:\n{}\nTo:\n{}",
187                        rule,
188                        parent.encoding_id(),
189                        reduced.encoding_id()
190                    );
191                    vortex_error::vortex_ensure!(
192                        reduced.dtype() == parent.dtype(),
193                        "Reduced array dtype mismatch from {:?}\nFrom:\n{}\nTo:\n{}",
194                        rule,
195                        parent.encoding_id(),
196                        reduced.encoding_id()
197                    );
198                }
199
200                return Ok(Some(reduced));
201            }
202        }
203        Ok(None)
204    }
205}