Skip to main content

tenferro_ops/
shape_constraint.rs

1use tenferro_tensor::{DType, ErrorKind, ValidationKind};
2
3use crate::SymDim;
4
5/// A relation between two symbolic shape expressions.
6///
7/// # Examples
8///
9/// ```rust
10/// use tenferro_ops::ShapeRelation;
11///
12/// assert_eq!(ShapeRelation::Equal, ShapeRelation::Equal);
13/// ```
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
15pub enum ShapeRelation {
16    /// The two symbolic expressions must evaluate to the same dimension.
17    Equal,
18}
19
20/// A shape relation recorded by an extension during metadata inference.
21///
22/// The constraint stores expressions as provided. It does not attempt to
23/// normalize or solve them.
24#[doc(hidden)]
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct ExtensionShapeConstraint {
27    relation: ShapeRelation,
28    lhs: SymDim,
29    rhs: SymDim,
30}
31
32impl ExtensionShapeConstraint {
33    fn equal(lhs: SymDim, rhs: SymDim) -> Self {
34        Self {
35            relation: ShapeRelation::Equal,
36            lhs,
37            rhs,
38        }
39    }
40
41    /// Return the relation imposed by this constraint.
42    #[doc(hidden)]
43    pub fn relation(&self) -> ShapeRelation {
44        self.relation
45    }
46
47    /// Return the left-hand symbolic expression.
48    #[doc(hidden)]
49    pub fn lhs(&self) -> &SymDim {
50        &self.lhs
51    }
52
53    /// Return the right-hand symbolic expression.
54    #[doc(hidden)]
55    pub fn rhs(&self) -> &SymDim {
56        &self.rhs
57    }
58}
59
60/// Errors produced while extension shape requirements inspect input metadata.
61///
62/// # Examples
63///
64/// ```rust
65/// use tenferro_ops::ExtensionShapeError;
66///
67/// let error = ExtensionShapeError::InputOutOfBounds {
68///     family_id: "example.v1",
69///     input: 2,
70///     input_count: 1,
71/// };
72/// assert!(error.to_string().contains("example.v1"));
73/// ```
74#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
75pub enum ExtensionShapeError {
76    /// An input metadata index was outside the supplied input list.
77    #[error(
78        "extension family {family_id:?} input index {input} out of bounds for {input_count} inputs"
79    )]
80    InputOutOfBounds {
81        /// Stable extension family identifier.
82        family_id: &'static str,
83        /// Requested input index.
84        input: usize,
85        /// Number of available inputs.
86        input_count: usize,
87    },
88    /// An axis was outside the selected input's rank.
89    #[error(
90        "extension family {family_id:?} axis {axis} out of bounds for input {input} rank {rank}"
91    )]
92    AxisOutOfBounds {
93        /// Stable extension family identifier.
94        family_id: &'static str,
95        /// Selected input index.
96        input: usize,
97        /// Requested axis index.
98        axis: usize,
99        /// Rank of the selected input.
100        rank: usize,
101    },
102    /// Two inputs required to have the same shape had different ranks.
103    #[error(
104        "extension family {family_id:?} requires inputs {lhs_input} and {rhs_input} to have the same shape, but their ranks are {lhs_rank} and {rhs_rank}"
105    )]
106    RankMismatch {
107        /// Stable extension family identifier.
108        family_id: &'static str,
109        /// Left-hand input index.
110        lhs_input: usize,
111        /// Left-hand input rank.
112        lhs_rank: usize,
113        /// Right-hand input index.
114        rhs_input: usize,
115        /// Right-hand input rank.
116        rhs_rank: usize,
117    },
118}
119
120impl From<ExtensionShapeError> for tenferro_tensor::Error {
121    fn from(error: ExtensionShapeError) -> Self {
122        let (family_id, kind) = match &error {
123            ExtensionShapeError::InputOutOfBounds { family_id, .. } => (
124                *family_id,
125                ErrorKind::Validation(ValidationKind::InvalidArgument),
126            ),
127            ExtensionShapeError::AxisOutOfBounds { family_id, .. } => (
128                *family_id,
129                ErrorKind::Validation(ValidationKind::AxisOutOfBounds),
130            ),
131            ExtensionShapeError::RankMismatch { family_id, .. } => (
132                *family_id,
133                ErrorKind::Validation(ValidationKind::RankMismatch),
134            ),
135        };
136        tenferro_tensor::Error::extension("extension", family_id, kind, error)
137    }
138}
139
140/// Input metadata and equality requirements for one extension inference call.
141///
142/// The context records requirements declaratively. It does not prove, reject,
143/// normalize, or solve them.
144///
145/// # Examples
146///
147/// ```rust
148/// use tenferro_ops::ExtensionShapeContext;
149///
150/// fn infer(ctx: &mut ExtensionShapeContext<'_>) -> tenferro_tensor::Result<()> {
151///     let lhs = ctx.input_axis(0, 0)?;
152///     let rhs = ctx.input_axis(1, 0)?;
153///     ctx.require_equal(lhs, 2 * rhs)?;
154///     Ok(())
155/// }
156/// ```
157#[derive(Debug)]
158pub struct ExtensionShapeContext<'a> {
159    family_id: &'static str,
160    input_dtypes: &'a [DType],
161    input_shapes: &'a [&'a [SymDim]],
162    constraints: Vec<ExtensionShapeConstraint>,
163}
164
165impl<'a> ExtensionShapeContext<'a> {
166    /// Construct a context for the internal extension inference driver.
167    #[doc(hidden)]
168    pub fn new_for_inference(
169        family_id: &'static str,
170        input_dtypes: &'a [DType],
171        input_shapes: &'a [&'a [SymDim]],
172    ) -> Self {
173        Self {
174            family_id,
175            input_dtypes,
176            input_shapes,
177            constraints: Vec::new(),
178        }
179    }
180
181    /// Return the dtype of one extension input.
182    ///
183    /// # Examples
184    ///
185    /// ```rust
186    /// use tenferro_ops::ExtensionShapeContext;
187    /// use tenferro_tensor::DType;
188    ///
189    /// fn infer(ctx: &ExtensionShapeContext<'_>) -> tenferro_tensor::Result<DType> {
190    ///     Ok(ctx.input_dtype(0)?)
191    /// }
192    /// ```
193    ///
194    /// # Errors
195    ///
196    /// Returns [`ExtensionShapeError::InputOutOfBounds`] when `input` is not
197    /// present in the extension metadata.
198    pub fn input_dtype(&self, input: usize) -> Result<DType, ExtensionShapeError> {
199        self.input_dtypes
200            .get(input)
201            .copied()
202            .ok_or(ExtensionShapeError::InputOutOfBounds {
203                family_id: self.family_id,
204                input,
205                input_count: self.input_dtypes.len(),
206            })
207    }
208
209    /// Return the symbolic shape of one extension input.
210    ///
211    /// # Examples
212    ///
213    /// ```rust
214    /// use tenferro_ops::ExtensionShapeContext;
215    ///
216    /// fn infer(ctx: &ExtensionShapeContext<'_>) -> tenferro_tensor::Result<()> {
217    ///     let _shape = ctx.input_shape(0)?;
218    ///     Ok(())
219    /// }
220    /// ```
221    ///
222    /// # Errors
223    ///
224    /// Returns [`ExtensionShapeError::InputOutOfBounds`] when `input` is not
225    /// present in the extension metadata.
226    pub fn input_shape(&self, input: usize) -> Result<&[SymDim], ExtensionShapeError> {
227        self.input_shapes
228            .get(input)
229            .copied()
230            .ok_or(ExtensionShapeError::InputOutOfBounds {
231                family_id: self.family_id,
232                input,
233                input_count: self.input_shapes.len(),
234            })
235    }
236
237    /// Return one symbolic axis expression from an extension input.
238    ///
239    /// # Examples
240    ///
241    /// ```rust
242    /// use tenferro_ops::ExtensionShapeContext;
243    /// use tenferro_ops::SymDim;
244    ///
245    /// fn infer(ctx: &ExtensionShapeContext<'_>) -> tenferro_tensor::Result<SymDim> {
246    ///     Ok(ctx.input_axis(0, 0)?)
247    /// }
248    /// ```
249    ///
250    /// # Errors
251    ///
252    /// Returns [`ExtensionShapeError::InputOutOfBounds`] for an unknown input
253    /// or [`ExtensionShapeError::AxisOutOfBounds`] for an axis outside that
254    /// input's rank.
255    pub fn input_axis(&self, input: usize, axis: usize) -> Result<SymDim, ExtensionShapeError> {
256        let shape = self.input_shape(input)?;
257        shape
258            .get(axis)
259            .cloned()
260            .ok_or(ExtensionShapeError::AxisOutOfBounds {
261                family_id: self.family_id,
262                input,
263                axis,
264                rank: shape.len(),
265            })
266    }
267
268    /// Record equality of two symbolic dimension expressions.
269    ///
270    /// This method records the expressions without trying to solve them.
271    ///
272    /// # Examples
273    ///
274    /// ```rust
275    /// use tenferro_ops::ExtensionShapeContext;
276    ///
277    /// fn infer(ctx: &mut ExtensionShapeContext<'_>) -> tenferro_tensor::Result<()> {
278    ///     let lhs = ctx.input_axis(0, 0)?;
279    ///     let rhs = ctx.input_axis(1, 0)?;
280    ///     ctx.require_equal(lhs, 2 * rhs)?;
281    ///     Ok(())
282    /// }
283    /// ```
284    ///
285    /// # Errors
286    ///
287    /// Returns `Ok(())`; this method only records a symbolic equality and does
288    /// not evaluate it or produce an [`ExtensionShapeError`].
289    pub fn require_equal(&mut self, lhs: SymDim, rhs: SymDim) -> Result<(), ExtensionShapeError> {
290        self.constraints
291            .push(ExtensionShapeConstraint::equal(lhs, rhs));
292        Ok(())
293    }
294
295    /// Record equality of two input axes.
296    ///
297    /// # Examples
298    ///
299    /// ```rust
300    /// use tenferro_ops::ExtensionShapeContext;
301    ///
302    /// fn infer(ctx: &mut ExtensionShapeContext<'_>) -> tenferro_tensor::Result<()> {
303    ///     ctx.require_axes_equal((0, 0), (1, 0))?;
304    ///     Ok(())
305    /// }
306    /// ```
307    ///
308    /// # Errors
309    ///
310    /// Returns [`ExtensionShapeError::InputOutOfBounds`] or
311    /// [`ExtensionShapeError::AxisOutOfBounds`] when either referenced axis is
312    /// absent from the extension metadata.
313    pub fn require_axes_equal(
314        &mut self,
315        lhs: (usize, usize),
316        rhs: (usize, usize),
317    ) -> Result<(), ExtensionShapeError> {
318        let lhs = self.input_axis(lhs.0, lhs.1)?;
319        let rhs = self.input_axis(rhs.0, rhs.1)?;
320        self.require_equal(lhs, rhs)
321    }
322
323    /// Require two extension inputs to have the same rank and axis extents.
324    ///
325    /// A rank mismatch is returned before any equality is recorded.
326    ///
327    /// # Examples
328    ///
329    /// ```rust
330    /// use tenferro_ops::ExtensionShapeContext;
331    ///
332    /// fn infer(ctx: &mut ExtensionShapeContext<'_>) -> tenferro_tensor::Result<()> {
333    ///     ctx.require_same_shape(0, 1)?;
334    ///     Ok(())
335    /// }
336    /// ```
337    ///
338    /// # Errors
339    ///
340    /// Returns [`ExtensionShapeError::InputOutOfBounds`] when an input is
341    /// absent, or [`ExtensionShapeError::RankMismatch`] when the two inputs do
342    /// not have the same rank.
343    pub fn require_same_shape(
344        &mut self,
345        lhs_input: usize,
346        rhs_input: usize,
347    ) -> Result<(), ExtensionShapeError> {
348        let lhs_shape = self.input_shape(lhs_input)?;
349        let rhs_shape = self.input_shape(rhs_input)?;
350        if lhs_shape.len() != rhs_shape.len() {
351            return Err(ExtensionShapeError::RankMismatch {
352                family_id: self.family_id,
353                lhs_input,
354                lhs_rank: lhs_shape.len(),
355                rhs_input,
356                rhs_rank: rhs_shape.len(),
357            });
358        }
359
360        let equalities: Vec<_> = lhs_shape
361            .iter()
362            .cloned()
363            .zip(rhs_shape.iter().cloned())
364            .map(|(lhs, rhs)| ExtensionShapeConstraint::equal(lhs, rhs))
365            .collect();
366        self.constraints.extend(equalities);
367        Ok(())
368    }
369
370    /// Borrow the constraints collected by this inference call.
371    #[doc(hidden)]
372    pub fn constraints(&self) -> &[ExtensionShapeConstraint] {
373        &self.constraints
374    }
375
376    /// Consume the context and return its collected constraints.
377    #[doc(hidden)]
378    pub fn into_constraints(self) -> Vec<ExtensionShapeConstraint> {
379        self.constraints
380    }
381}
382
383#[cfg(test)]
384mod tests;