tenferro_tensor_core/lib.rs
1//! Backend-independent rank/layout, dtype and scalar metadata.
2//!
3//! `tenferro-tensor-core` owns backend-independent tensor metadata: scalar
4//! tags and promotion facts, rank/layout metadata, and layout validation. It
5//! does not own tensor storage, execution backends, backend buffers, GPU
6//! handles, provider selection, or materializing kernels. The tensor families
7//! that carry storage live in `tenferro-tensor`.
8//!
9//! # Examples
10//!
11//! ```rust
12//! use tenferro_tensor_core::{DType, Rank, TensorLayout};
13//!
14//! let layout = TensorLayout::<Rank<2>>::compact([2, 3])?;
15//! let transposed = layout.transpose_view([1, 0])?;
16//! assert_eq!(transposed.shape(), &[3, 2]);
17//!
18//! assert_eq!(DType::F64.spec().width, 64);
19//! # Ok::<(), tenferro_tensor_core::ValidationError>(())
20//! ```
21
22use num_complex::{Complex32, Complex64};
23use tinyvec::TinyVec;
24
25mod error;
26mod layout;
27mod rank;
28mod scalar;
29#[macro_use]
30mod scalar_set;
31
32pub use error::{ErrorKind, ShapeMismatch, ValidationError, ValidationKind};
33pub use layout::TensorLayout;
34pub use rank::{DynRank, IntoRankShape, Rank, TensorRank};
35pub use scalar::{ad_admission, AdAdmissionError, Scalar, ScalarArithmetic, ScalarDomain};
36pub use scalar_set::{promote_in_set, promote_specs, MemberKind, MemberSpec};
37
38/// Maximum rank whose shape/stride metadata stays inline without a heap
39/// allocation.
40///
41/// Mirrors `strided_view::INLINE_RANK`; `tenferro-cpu-basic` carries a
42/// compile-time assertion that the two values agree. At rank `<= INLINE_RANK`
43/// [`ShapeVec`] and [`StrideVec`] keep their elements inline; a higher rank
44/// spills to the heap gracefully.
45///
46/// # Examples
47///
48/// ```rust
49/// use tenferro_tensor_core::INLINE_RANK;
50///
51/// assert_eq!(INLINE_RANK, 8);
52/// ```
53pub const INLINE_RANK: usize = 8;
54
55/// Small tensor shape vector with inline capacity for common dynamic ranks.
56///
57/// # Examples
58///
59/// ```rust
60/// use tenferro_tensor_core::ShapeVec;
61///
62/// let shape: ShapeVec = vec![2, 3].into_iter().collect();
63/// assert_eq!(shape.as_slice(), &[2, 3]);
64/// ```
65pub type ShapeVec = TinyVec<[usize; INLINE_RANK]>;
66
67/// Convert a common shape container into the dynamic owned shape type.
68///
69/// Arrays, vectors, slices, and [`ShapeVec`] implement this trait through
70/// their `AsRef<[usize]>` representation.
71pub trait IntoShapeVec {
72 /// Convert this shape container into an owned [`ShapeVec`].
73 fn into_shape_vec(self) -> ShapeVec;
74}
75
76impl<S> IntoShapeVec for S
77where
78 S: AsRef<[usize]>,
79{
80 fn into_shape_vec(self) -> ShapeVec {
81 self.as_ref().iter().copied().collect()
82 }
83}
84
85/// Small tensor stride vector with signed element strides.
86///
87/// # Examples
88///
89/// ```rust
90/// use tenferro_tensor_core::StrideVec;
91///
92/// let strides: StrideVec = vec![1, 2].into_iter().collect();
93/// assert_eq!(strides.as_slice(), &[1, 2]);
94/// ```
95pub type StrideVec = TinyVec<[isize; INLINE_RANK]>;
96
97/// Result type for tensor data-model operations.
98///
99/// # Examples
100///
101/// ```rust
102/// use tenferro_tensor_core::{Result, ValidationError};
103///
104/// let result: Result<()> = Err(ValidationError::RankMismatch { expected: 2, actual: 1 });
105/// assert!(result.is_err());
106/// ```
107pub type Result<T> = std::result::Result<T, ValidationError>;
108
109define_scalar_tag! {
110 /// Runtime scalar dtype tag.
111 ///
112 /// # Examples
113 ///
114 /// ```rust
115 /// use tenferro_tensor_core::DType;
116 ///
117 /// assert_eq!(DType::F64, DType::F64);
118 /// ```
119 pub enum DType {
120 /// 32-bit floating point.
121 F32 => f32 : Float 0 32,
122 /// 64-bit floating point.
123 F64 => f64 : Float 1 64,
124 /// 32-bit signed integer.
125 I32 => i32 : Integer 0 32,
126 /// 64-bit signed integer.
127 I64 => i64 : Integer 1 64,
128 /// Boolean.
129 Bool => bool : Boolean 0 0,
130 /// 32-bit complex floating point.
131 C32 => Complex32 : Complex 0 32,
132 /// 64-bit complex floating point.
133 C64 => Complex64 : Complex 1 64,
134 }
135 external External(core::any::TypeId);
136}
137
138/// Sealed trait for scalar types supported by the core tensor data model.
139///
140/// # Examples
141///
142/// ```rust
143/// use tenferro_tensor_core::{DType, TensorScalar};
144///
145/// assert_eq!(f64::dtype(), DType::F64);
146/// assert_eq!(num_complex::Complex64::dtype(), DType::C64);
147/// ```
148pub trait TensorScalar: Scalar + Copy + Clone + Send + Sync + 'static + private::Sealed {
149 /// Real-valued counterpart of this scalar type.
150 type Real: TensorScalar;
151
152 /// Return the scalar dtype tag.
153 ///
154 /// # Examples
155 ///
156 /// ```rust
157 /// use tenferro_tensor_core::{DType, TensorScalar};
158 ///
159 /// assert_eq!(i64::dtype(), DType::I64);
160 /// ```
161 fn dtype() -> DType;
162}
163
164mod private {
165 pub trait Sealed {}
166
167 impl Sealed for f32 {}
168 impl Sealed for f64 {}
169 impl Sealed for i32 {}
170 impl Sealed for i64 {}
171 impl Sealed for bool {}
172 impl Sealed for num_complex::Complex32 {}
173 impl Sealed for num_complex::Complex64 {}
174}
175
176macro_rules! scalar_domain {
177 (float) => {
178 ScalarDomain::Field
179 };
180 (complex) => {
181 ScalarDomain::Field
182 };
183 (integer) => {
184 ScalarDomain::Field
185 };
186 (boolean) => {
187 ScalarDomain::NonField
188 };
189}
190
191macro_rules! impl_scalar_arithmetic {
192 ($ty:ty, float) => {
193 impl ScalarArithmetic for $ty {
194 fn scalar_zero() -> Self {
195 0.0
196 }
197
198 fn scalar_one() -> Self {
199 1.0
200 }
201
202 fn scalar_add(self, rhs: Self) -> Self {
203 std::ops::Add::add(self, rhs)
204 }
205
206 fn scalar_sub(self, rhs: Self) -> Self {
207 std::ops::Sub::sub(self, rhs)
208 }
209
210 fn scalar_mul(self, rhs: Self) -> Self {
211 std::ops::Mul::mul(self, rhs)
212 }
213 }
214 };
215 ($ty:ty, complex) => {
216 impl ScalarArithmetic for $ty {
217 fn scalar_zero() -> Self {
218 <$ty>::new(0.0, 0.0)
219 }
220
221 fn scalar_one() -> Self {
222 <$ty>::new(1.0, 0.0)
223 }
224
225 fn scalar_add(self, rhs: Self) -> Self {
226 std::ops::Add::add(self, rhs)
227 }
228
229 fn scalar_sub(self, rhs: Self) -> Self {
230 std::ops::Sub::sub(self, rhs)
231 }
232
233 fn scalar_mul(self, rhs: Self) -> Self {
234 std::ops::Mul::mul(self, rhs)
235 }
236 }
237 };
238 ($ty:ty, integer) => {
239 impl ScalarArithmetic for $ty {
240 fn scalar_zero() -> Self {
241 0
242 }
243
244 fn scalar_one() -> Self {
245 1
246 }
247
248 fn scalar_add(self, rhs: Self) -> Self {
249 self.wrapping_add(rhs)
250 }
251
252 fn scalar_sub(self, rhs: Self) -> Self {
253 self.wrapping_sub(rhs)
254 }
255
256 fn scalar_mul(self, rhs: Self) -> Self {
257 self.wrapping_mul(rhs)
258 }
259 }
260 };
261 ($ty:ty, boolean) => {};
262}
263
264macro_rules! impl_scalar {
265 ($ty:ty, $real:ty, $dtype:expr, $variant:ident, $kind:ident) => {
266 impl Scalar for $ty {
267 const DOMAIN: ScalarDomain = scalar_domain!($kind);
268 }
269
270 impl_scalar_arithmetic!($ty, $kind);
271
272 impl TensorScalar for $ty {
273 type Real = $real;
274
275 fn dtype() -> DType {
276 $dtype
277 }
278 }
279 };
280}
281
282// The single preset table: tag, real counterpart, erased variant, and algebra
283// kind are declared once here and expanded into every contract the preset
284// scalars implement.
285impl_scalar!(f32, f32, DType::F32, F32, float);
286impl_scalar!(f64, f64, DType::F64, F64, float);
287impl_scalar!(i32, i32, DType::I32, I32, integer);
288impl_scalar!(i64, i64, DType::I64, I64, integer);
289impl_scalar!(bool, bool, DType::Bool, Bool, boolean);
290impl_scalar!(Complex32, f32, DType::C32, C32, complex);
291impl_scalar!(Complex64, f64, DType::C64, C64, complex);
292
293/// Explicit slice descriptor.
294///
295/// A zero step is invalid. Layout metadata APIs support signed steps when
296/// reachable-range validation proves the view stays inside the backing
297/// allocation.
298///
299/// # Examples
300///
301/// ```rust
302/// use tenferro_tensor_core::SliceSpec;
303///
304/// let spec = SliceSpec { start: 1, end: 4, step: 2 };
305/// assert_eq!(spec.step, 2);
306/// ```
307#[derive(Clone, Copy, Debug, PartialEq, Eq)]
308pub struct SliceSpec {
309 pub start: isize,
310 pub end: isize,
311 pub step: isize,
312}
313
314// A valid empty slice. `tinyvec`'s storage (`Array::Item: Default`) needs a
315// default value to initialize unused inline slots; unlike a derived default,
316// `step` stays nonzero so the value is a legal `SliceSpec`.
317impl Default for SliceSpec {
318 fn default() -> Self {
319 Self {
320 start: 0,
321 end: 0,
322 step: 1,
323 }
324 }
325}
326
327fn checked_product(shape: &[usize]) -> Result<usize> {
328 shape.iter().try_fold(1usize, |acc, &dim| {
329 acc.checked_mul(dim).ok_or(ValidationError::IntegerOverflow)
330 })
331}
332
333fn checked_logical_element_count(shape: &[usize]) -> Result<usize> {
334 if shape.contains(&0) {
335 return Ok(0);
336 }
337 checked_product(shape)
338}
339
340/// Return compact column-major strides for a shape.
341///
342/// # Examples
343///
344/// ```rust
345/// use tenferro_tensor_core::col_major_strides;
346///
347/// assert_eq!(col_major_strides(&[2, 3])?.as_slice(), &[1, 2]);
348/// # Ok::<(), tenferro_tensor_core::ValidationError>(())
349/// ```
350///
351/// # Errors
352///
353/// Returns [`ValidationError::IntegerOverflow`] when a stride or extent
354/// cannot be represented by the metadata arithmetic.
355pub fn col_major_strides(shape: &[usize]) -> Result<StrideVec> {
356 let mut strides = StrideVec::new();
357 let mut stride = 1isize;
358 for &extent in shape {
359 strides.push(stride);
360 let extent = isize::try_from(extent).map_err(|_| ValidationError::IntegerOverflow)?;
361 stride = stride
362 .checked_mul(extent)
363 .ok_or(ValidationError::IntegerOverflow)?;
364 }
365 Ok(strides)
366}
367
368fn validate_permutation(rank: usize, axes: &[usize]) -> Result<()> {
369 if axes.len() != rank {
370 return Err(ValidationError::InvalidPermutationLength {
371 expected: rank,
372 actual: axes.len(),
373 });
374 }
375 // Common ranks track seen axes in a bitmask; only very high ranks allocate.
376 if rank <= u128::BITS as usize {
377 let mut seen = 0u128;
378 for &axis in axes {
379 if axis >= rank {
380 return Err(ValidationError::AxisOutOfBounds { axis, rank });
381 }
382 let bit = 1u128 << axis;
383 if seen & bit != 0 {
384 return Err(ValidationError::DuplicateAxis {
385 axis,
386 role: "permutation",
387 });
388 }
389 seen |= bit;
390 }
391 return Ok(());
392 }
393 let mut seen = vec![false; rank];
394 for &axis in axes {
395 if axis >= rank {
396 return Err(ValidationError::AxisOutOfBounds { axis, rank });
397 }
398 if seen[axis] {
399 return Err(ValidationError::DuplicateAxis {
400 axis,
401 role: "permutation",
402 });
403 }
404 seen[axis] = true;
405 }
406 Ok(())
407}