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