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}