Skip to main content

tenferro_ops/
dim_expr.rs

1/// Arithmetic expression over tensor dimension sizes.
2///
3/// Evaluated at execution time from actual input tensor shapes.
4/// `InputDim { input_idx, axis }` references the axis size of
5/// the op's `input_idx`-th input tensor.
6///
7/// # Examples
8///
9/// ```rust
10/// use tenferro_ops::dim_expr::DimExpr;
11///
12/// let expr = DimExpr::mul(
13///     DimExpr::InputDim {
14///         input_idx: 0,
15///         axis: 0,
16///     },
17///     DimExpr::InputDim {
18///         input_idx: 0,
19///         axis: 1,
20///     },
21/// );
22/// assert_eq!(expr.eval(&[&[3, 4]]).unwrap(), 12);
23/// ```
24#[derive(Clone, Debug, PartialEq, Eq, Hash)]
25pub enum DimExpr {
26    /// A concrete dimension size.
27    Const(usize),
28    /// Axis size of the op's `input_idx`-th input tensor.
29    InputDim { input_idx: usize, axis: usize },
30    /// Sum of two dimension expressions.
31    Add(Box<DimExpr>, Box<DimExpr>),
32    /// Difference of two dimension expressions.
33    Sub(Box<DimExpr>, Box<DimExpr>),
34    /// Product of two dimension expressions.
35    Mul(Box<DimExpr>, Box<DimExpr>),
36    /// Floor division of two dimension expressions.
37    FloorDiv(Box<DimExpr>, Box<DimExpr>),
38    /// Minimum of two dimension expressions.
39    Min(Box<DimExpr>, Box<DimExpr>),
40    /// Maximum of two dimension expressions.
41    Max(Box<DimExpr>, Box<DimExpr>),
42}
43
44/// Error produced while evaluating a [`DimExpr`] against concrete shapes.
45#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
46pub enum DimExprEvalError {
47    /// `InputDim` referenced an input that was not provided.
48    #[error(
49        "DimExpr::InputDim input index {input_idx} out of bounds for {input_count} input shapes"
50    )]
51    InputOutOfBounds {
52        input_idx: usize,
53        input_count: usize,
54    },
55    /// `InputDim` referenced an axis that does not exist on the selected input.
56    #[error("DimExpr::InputDim axis {axis} out of bounds for input {input_idx} rank {rank}")]
57    AxisOutOfBounds {
58        input_idx: usize,
59        axis: usize,
60        rank: usize,
61    },
62    /// Addition overflowed `usize`.
63    #[error("DimExpr::Add overflow: {lhs} + {rhs}")]
64    AddOverflow { lhs: usize, rhs: usize },
65    /// Subtraction would underflow `usize`.
66    #[error("DimExpr::Sub underflow: left operand {lhs} is smaller than {rhs}")]
67    SubUnderflow { lhs: usize, rhs: usize },
68    /// Multiplication overflowed `usize`.
69    #[error("DimExpr::Mul overflow: {lhs} * {rhs}")]
70    MulOverflow { lhs: usize, rhs: usize },
71    /// Floor division divisor evaluated to zero.
72    #[error("DimExpr::FloorDiv divide by zero: left operand {lhs}, divisor {rhs}")]
73    FloorDivByZero { lhs: usize, rhs: usize },
74}
75
76impl DimExpr {
77    /// Evaluate the expression using actual input tensor shapes.
78    ///
79    /// # Examples
80    ///
81    /// ```rust
82    /// use tenferro_ops::dim_expr::DimExpr;
83    ///
84    /// let expr = DimExpr::add(
85    ///     DimExpr::InputDim {
86    ///         input_idx: 0,
87    ///         axis: 0,
88    ///     },
89    ///     DimExpr::Const(2),
90    /// );
91    /// assert_eq!(expr.eval(&[&[5, 7]]).unwrap(), 7);
92    /// ```
93    ///
94    /// # Errors
95    ///
96    /// Returns [`DimExprEvalError`] when an input or axis is unavailable, an
97    /// arithmetic operation overflows, subtraction underflows, or division
98    /// would use zero as its divisor.
99    pub fn eval(&self, input_shapes: &[&[usize]]) -> Result<usize, DimExprEvalError> {
100        match self {
101            Self::Const(v) => Ok(*v),
102            Self::InputDim { input_idx, axis } => input_shapes
103                .get(*input_idx)
104                .ok_or(DimExprEvalError::InputOutOfBounds {
105                    input_idx: *input_idx,
106                    input_count: input_shapes.len(),
107                })
108                .and_then(|shape| {
109                    shape
110                        .get(*axis)
111                        .copied()
112                        .ok_or(DimExprEvalError::AxisOutOfBounds {
113                            input_idx: *input_idx,
114                            axis: *axis,
115                            rank: shape.len(),
116                        })
117                }),
118            Self::Add(a, b) => {
119                let lhs = a.eval(input_shapes)?;
120                let rhs = b.eval(input_shapes)?;
121                lhs.checked_add(rhs)
122                    .ok_or(DimExprEvalError::AddOverflow { lhs, rhs })
123            }
124            Self::Sub(a, b) => {
125                let lhs = a.eval(input_shapes)?;
126                let rhs = b.eval(input_shapes)?;
127                lhs.checked_sub(rhs)
128                    .ok_or(DimExprEvalError::SubUnderflow { lhs, rhs })
129            }
130            Self::Mul(a, b) => {
131                let lhs = a.eval(input_shapes)?;
132                let rhs = b.eval(input_shapes)?;
133                lhs.checked_mul(rhs)
134                    .ok_or(DimExprEvalError::MulOverflow { lhs, rhs })
135            }
136            Self::FloorDiv(a, b) => {
137                let lhs = a.eval(input_shapes)?;
138                let rhs = b.eval(input_shapes)?;
139                if rhs == 0 {
140                    return Err(DimExprEvalError::FloorDivByZero { lhs, rhs });
141                }
142                Ok(lhs / rhs)
143            }
144            Self::Min(a, b) => Ok(a.eval(input_shapes)?.min(b.eval(input_shapes)?)),
145            Self::Max(a, b) => Ok(a.eval(input_shapes)?.max(b.eval(input_shapes)?)),
146        }
147    }
148
149    /// Return the maximum referenced `input_idx`, or `None` if the expression
150    /// contains only constants.
151    ///
152    /// # Examples
153    ///
154    /// ```rust
155    /// use tenferro_ops::dim_expr::DimExpr;
156    ///
157    /// let expr = DimExpr::add(
158    ///     DimExpr::InputDim {
159    ///         input_idx: 0,
160    ///         axis: 0,
161    ///     },
162    ///     DimExpr::InputDim {
163    ///         input_idx: 2,
164    ///         axis: 1,
165    ///     },
166    /// );
167    /// assert_eq!(expr.max_input_idx(), Some(2));
168    /// ```
169    pub fn max_input_idx(&self) -> Option<usize> {
170        match self {
171            Self::Const(_) => None,
172            Self::InputDim { input_idx, .. } => Some(*input_idx),
173            Self::Add(a, b)
174            | Self::Sub(a, b)
175            | Self::Mul(a, b)
176            | Self::FloorDiv(a, b)
177            | Self::Min(a, b)
178            | Self::Max(a, b) => match (a.max_input_idx(), b.max_input_idx()) {
179                (Some(x), Some(y)) => Some(x.max(y)),
180                (Some(x), None) | (None, Some(x)) => Some(x),
181                (None, None) => None,
182            },
183        }
184    }
185
186    /// Remap `InputDim { input_idx: from, .. }` to `InputDim { input_idx: to, .. }`.
187    ///
188    /// # Examples
189    ///
190    /// ```rust
191    /// use tenferro_ops::dim_expr::DimExpr;
192    ///
193    /// let expr = DimExpr::InputDim {
194    ///     input_idx: 0,
195    ///     axis: 1,
196    /// };
197    /// assert_eq!(expr.remap(0, 2), DimExpr::InputDim { input_idx: 2, axis: 1 });
198    /// ```
199    pub fn remap(&self, from: usize, to: usize) -> Self {
200        match self {
201            Self::Const(v) => Self::Const(*v),
202            Self::InputDim { input_idx, axis } => Self::InputDim {
203                input_idx: if *input_idx == from { to } else { *input_idx },
204                axis: *axis,
205            },
206            Self::Add(a, b) => Self::add(a.remap(from, to), b.remap(from, to)),
207            Self::Sub(a, b) => Self::sub(a.remap(from, to), b.remap(from, to)),
208            Self::Mul(a, b) => Self::mul(a.remap(from, to), b.remap(from, to)),
209            Self::FloorDiv(a, b) => Self::floor_div(a.remap(from, to), b.remap(from, to)),
210            Self::Min(a, b) => Self::min(a.remap(from, to), b.remap(from, to)),
211            Self::Max(a, b) => Self::max(a.remap(from, to), b.remap(from, to)),
212        }
213    }
214
215    /// Construct a constant dimension expression.
216    ///
217    /// # Examples
218    ///
219    /// ```rust
220    /// use tenferro_ops::dim_expr::DimExpr;
221    ///
222    /// assert_eq!(DimExpr::constant(4), DimExpr::Const(4));
223    /// ```
224    pub fn constant(v: usize) -> Self {
225        Self::Const(v)
226    }
227
228    /// Construct an addition node.
229    ///
230    /// # Examples
231    ///
232    /// ```rust
233    /// use tenferro_ops::dim_expr::DimExpr;
234    ///
235    /// let expr = DimExpr::add(DimExpr::Const(2), DimExpr::Const(3));
236    /// assert_eq!(expr.eval(&[]).unwrap(), 5);
237    /// ```
238    // Public constructor names mirror the DimExpr variants; operator traits are a separate API choice.
239    #[allow(clippy::should_implement_trait)]
240    pub fn add(a: Self, b: Self) -> Self {
241        Self::Add(Box::new(a), Box::new(b))
242    }
243
244    /// Construct a subtraction node.
245    ///
246    /// # Examples
247    ///
248    /// ```rust
249    /// use tenferro_ops::dim_expr::DimExpr;
250    ///
251    /// let expr = DimExpr::sub(DimExpr::Const(7), DimExpr::Const(2));
252    /// assert_eq!(expr.eval(&[]).unwrap(), 5);
253    /// ```
254    // Public constructor names mirror the DimExpr variants; operator traits are a separate API choice.
255    #[allow(clippy::should_implement_trait)]
256    pub fn sub(a: Self, b: Self) -> Self {
257        Self::Sub(Box::new(a), Box::new(b))
258    }
259
260    /// Construct a multiplication node.
261    ///
262    /// # Examples
263    ///
264    /// ```rust
265    /// use tenferro_ops::dim_expr::DimExpr;
266    ///
267    /// let expr = DimExpr::mul(DimExpr::Const(3), DimExpr::Const(4));
268    /// assert_eq!(expr.eval(&[]).unwrap(), 12);
269    /// ```
270    // Public constructor names mirror the DimExpr variants; operator traits are a separate API choice.
271    #[allow(clippy::should_implement_trait)]
272    pub fn mul(a: Self, b: Self) -> Self {
273        Self::Mul(Box::new(a), Box::new(b))
274    }
275
276    /// Construct a floor-division node.
277    ///
278    /// # Examples
279    ///
280    /// ```rust
281    /// use tenferro_ops::dim_expr::DimExpr;
282    ///
283    /// let expr = DimExpr::floor_div(DimExpr::Const(9), DimExpr::Const(2));
284    /// assert_eq!(expr.eval(&[]).unwrap(), 4);
285    /// ```
286    pub fn floor_div(a: Self, b: Self) -> Self {
287        Self::FloorDiv(Box::new(a), Box::new(b))
288    }
289
290    /// Construct a minimum node.
291    ///
292    /// # Examples
293    ///
294    /// ```rust
295    /// use tenferro_ops::dim_expr::DimExpr;
296    ///
297    /// let expr = DimExpr::min(DimExpr::Const(3), DimExpr::Const(5));
298    /// assert_eq!(expr.eval(&[]).unwrap(), 3);
299    /// ```
300    pub fn min(a: Self, b: Self) -> Self {
301        Self::Min(Box::new(a), Box::new(b))
302    }
303
304    /// Construct a maximum node.
305    ///
306    /// # Examples
307    ///
308    /// ```rust
309    /// use tenferro_ops::dim_expr::DimExpr;
310    ///
311    /// let expr = DimExpr::max(DimExpr::Const(3), DimExpr::Const(5));
312    /// assert_eq!(expr.eval(&[]).unwrap(), 5);
313    /// ```
314    pub fn max(a: Self, b: Self) -> Self {
315        Self::Max(Box::new(a), Box::new(b))
316    }
317
318    /// Return `true` when this expression is a constant.
319    ///
320    /// # Examples
321    ///
322    /// ```rust
323    /// use tenferro_ops::dim_expr::DimExpr;
324    ///
325    /// assert!(DimExpr::Const(3).is_const());
326    /// ```
327    pub fn is_const(&self) -> bool {
328        matches!(self, Self::Const(_))
329    }
330
331    /// Convert a concrete shape to constant expressions.
332    ///
333    /// # Examples
334    ///
335    /// ```rust
336    /// use tenferro_ops::dim_expr::DimExpr;
337    ///
338    /// assert_eq!(DimExpr::from_concrete(&[2, 3]), vec![DimExpr::Const(2), DimExpr::Const(3)]);
339    /// ```
340    pub fn from_concrete(shape: &[usize]) -> Vec<Self> {
341        shape.iter().map(|&v| Self::Const(v)).collect()
342    }
343
344    /// Build `[InputDim(input_idx, 0), ..., InputDim(input_idx, rank - 1)]`.
345    ///
346    /// # Examples
347    ///
348    /// ```rust
349    /// use tenferro_ops::dim_expr::DimExpr;
350    ///
351    /// let shape = DimExpr::input_shape(1, 2);
352    /// assert_eq!(
353    ///     shape,
354    ///     vec![
355    ///         DimExpr::InputDim { input_idx: 1, axis: 0 },
356    ///         DimExpr::InputDim { input_idx: 1, axis: 1 },
357    ///     ]
358    /// );
359    /// ```
360    pub fn input_shape(input_idx: usize, rank: usize) -> Vec<Self> {
361        (0..rank)
362            .map(|axis| Self::InputDim { input_idx, axis })
363            .collect()
364    }
365
366    /// Evaluate a slice of expressions against actual input shapes.
367    ///
368    /// # Examples
369    ///
370    /// ```rust
371    /// use tenferro_ops::dim_expr::DimExpr;
372    ///
373    /// let exprs = vec![
374    ///     DimExpr::InputDim { input_idx: 0, axis: 0 },
375    ///     DimExpr::Const(4),
376    /// ];
377    /// assert_eq!(DimExpr::eval_all(&exprs, &[&[3, 5]]).unwrap(), vec![3, 4]);
378    /// ```
379    ///
380    /// # Errors
381    ///
382    /// Returns the first [`DimExprEvalError`] encountered while evaluating an
383    /// expression.
384    pub fn eval_all(
385        exprs: &[Self],
386        input_shapes: &[&[usize]],
387    ) -> Result<Vec<usize>, DimExprEvalError> {
388        exprs.iter().map(|e| e.eval(input_shapes)).collect()
389    }
390
391    /// Remap all `InputDim` references in a slice of expressions.
392    ///
393    /// # Examples
394    ///
395    /// ```rust
396    /// use tenferro_ops::dim_expr::DimExpr;
397    ///
398    /// let exprs = vec![DimExpr::InputDim { input_idx: 0, axis: 0 }];
399    /// assert_eq!(
400    ///     DimExpr::remap_all(&exprs, 0, 1),
401    ///     vec![DimExpr::InputDim { input_idx: 1, axis: 0 }]
402    /// );
403    /// ```
404    pub fn remap_all(exprs: &[Self], from: usize, to: usize) -> Vec<Self> {
405        exprs.iter().map(|e| e.remap(from, to)).collect()
406    }
407
408    /// Compute the maximum referenced `input_idx` across a slice.
409    ///
410    /// # Examples
411    ///
412    /// ```rust
413    /// use tenferro_ops::dim_expr::DimExpr;
414    ///
415    /// let exprs = vec![
416    ///     DimExpr::InputDim { input_idx: 0, axis: 0 },
417    ///     DimExpr::InputDim { input_idx: 2, axis: 1 },
418    /// ];
419    /// assert_eq!(DimExpr::max_input_idx_all(&exprs), Some(2));
420    /// ```
421    pub fn max_input_idx_all(exprs: &[Self]) -> Option<usize> {
422        exprs.iter().filter_map(Self::max_input_idx).max()
423    }
424}
425
426impl From<usize> for DimExpr {
427    fn from(v: usize) -> Self {
428        Self::Const(v)
429    }
430}
431
432impl From<&DimExpr> for DimExpr {
433    fn from(value: &DimExpr) -> Self {
434        value.clone()
435    }
436}