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