strided_view/raw.rs
1//! Borrowed raw strided layout types.
2//!
3//! These are the prepared-replay counterparts to [`crate::StridedView`] and
4//! [`crate::StridedViewMut`]. They borrow shape/stride metadata instead of
5//! owning it, so compiled kernels can reuse already-validated layout
6//! descriptors without rebuilding dynamic-rank view wrappers.
7
8use crate::element_op::Identity;
9use crate::view::validate_bounds;
10use core::marker::PhantomData;
11use core::mem::MaybeUninit;
12use core::ptr::NonNull;
13use num_complex::{Complex32, Complex64};
14
15mod private {
16 pub trait Sealed {}
17}
18
19/// A scalar type that can safely witness storage for an erased kernel view.
20pub trait KernelStorageElement: private::Sealed + Copy + 'static {
21 const DTYPE: KernelDType;
22}
23
24macro_rules! kernel_storage_element {
25 ($($ty:ty => $dtype:ident),* $(,)?) => {$ (
26 impl private::Sealed for $ty {}
27 impl KernelStorageElement for $ty {
28 const DTYPE: KernelDType = KernelDType::$dtype;
29 }
30 )* };
31}
32
33kernel_storage_element! {
34 f32 => F32,
35 f64 => F64,
36 i32 => I32,
37 i64 => I64,
38 bool => Bool,
39 Complex32 => C32,
40 Complex64 => C64,
41}
42
43use crate::{Result, StridedError, StridedView, StridedViewMut};
44
45/// Dtypes supported by dtype-erased kernel entry points.
46///
47/// The enum is intentionally limited to the scalar set currently used by the
48/// tensor runtime callers. Later FFI layers should map their ABI dtype tags to
49/// this enum before dispatching into prepared kernels.
50#[non_exhaustive]
51#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
52#[repr(u8)]
53pub enum KernelDType {
54 F32 = 1,
55 F64 = 2,
56 I32 = 3,
57 I64 = 4,
58 Bool = 5,
59 C32 = 6,
60 C64 = 7,
61}
62
63impl KernelDType {
64 #[inline]
65 pub const fn label(self) -> &'static str {
66 match self {
67 Self::F32 => "f32",
68 Self::F64 => "f64",
69 Self::I32 => "i32",
70 Self::I64 => "i64",
71 Self::Bool => "bool",
72 Self::C32 => "c32",
73 Self::C64 => "c64",
74 }
75 }
76
77 #[inline]
78 pub const fn size_of(self) -> usize {
79 match self {
80 Self::F32 => core::mem::size_of::<f32>(),
81 Self::F64 => core::mem::size_of::<f64>(),
82 Self::I32 => core::mem::size_of::<i32>(),
83 Self::I64 => core::mem::size_of::<i64>(),
84 Self::Bool => core::mem::size_of::<bool>(),
85 Self::C32 => core::mem::size_of::<Complex32>(),
86 Self::C64 => core::mem::size_of::<Complex64>(),
87 }
88 }
89
90 #[inline]
91 pub const fn alignment(self) -> usize {
92 match self {
93 Self::F32 => core::mem::align_of::<f32>(),
94 Self::F64 => core::mem::align_of::<f64>(),
95 Self::I32 => core::mem::align_of::<i32>(),
96 Self::I64 => core::mem::align_of::<i64>(),
97 Self::Bool => core::mem::align_of::<bool>(),
98 Self::C32 => core::mem::align_of::<Complex32>(),
99 Self::C64 => core::mem::align_of::<Complex64>(),
100 }
101 }
102
103 #[inline]
104 pub const fn requires_valid_byte_values(self) -> bool {
105 matches!(self, Self::Bool)
106 }
107}
108
109fn validate_erased_buffer(dtype: KernelDType, data: &[u8]) -> Result<usize> {
110 let element_count = validate_erased_buffer_extent(dtype, data)?;
111 if element_count != 0 && dtype.requires_valid_byte_values() {
112 validate_bool_bytes(data)?;
113 }
114 Ok(element_count)
115}
116
117/// Validate the length and alignment of erased storage, without reading it.
118///
119/// Typed constructors call this alone: `KernelStorageElement` is sealed, so a
120/// `&[T]` already holds valid values of `T::DTYPE`.
121fn validate_erased_buffer_extent(dtype: KernelDType, data: &[u8]) -> Result<usize> {
122 let element_size = dtype.size_of();
123 if !data.len().is_multiple_of(element_size) {
124 return Err(StridedError::ByteLengthMismatch {
125 dtype: dtype.label(),
126 byte_len: data.len(),
127 element_size,
128 });
129 }
130
131 let element_count = data.len() / element_size;
132 if element_count == 0 {
133 return Ok(0);
134 }
135
136 let alignment = dtype.alignment();
137 if !(data.as_ptr() as usize).is_multiple_of(alignment) {
138 return Err(StridedError::DataAlignmentMismatch {
139 dtype: dtype.label(),
140 alignment,
141 });
142 }
143 Ok(element_count)
144}
145
146/// Reject any byte other than 0 or 1.
147///
148/// The OR fold has no early exit, so it vectorizes and runs at memory
149/// bandwidth; a byte by byte `find` does not, and cost more than the select
150/// kernel it guarded. The slow scan runs only to report the offending byte.
151fn validate_bool_bytes(data: &[u8]) -> Result<()> {
152 if data.iter().fold(0u8, |acc, &value| acc | value) <= 1 {
153 return Ok(());
154 }
155 match data.iter().find(|&&value| value > 1) {
156 Some(&value) => Err(StridedError::InvalidBoolByte { value }),
157 None => Ok(()),
158 }
159}
160
161fn validate_erased_buffer_layout(
162 dtype: KernelDType,
163 data: NonNull<u8>,
164 byte_len: usize,
165) -> Result<usize> {
166 let element_size = dtype.size_of();
167 if !byte_len.is_multiple_of(element_size) {
168 return Err(StridedError::ByteLengthMismatch {
169 dtype: dtype.label(),
170 byte_len,
171 element_size,
172 });
173 }
174 if byte_len != 0 && !(data.as_ptr() as usize).is_multiple_of(dtype.alignment()) {
175 return Err(StridedError::DataAlignmentMismatch {
176 dtype: dtype.label(),
177 alignment: dtype.alignment(),
178 });
179 }
180 Ok(byte_len / element_size)
181}
182
183/// Pointer-backed dtype-erased input used by one-shot write entry points.
184///
185/// Unlike [`ErasedRawStridedRef`], this descriptor does not create a shared
186/// Rust reference at construction time. That lets the entry point reject an
187/// input/output overlap before forming references to either side.
188#[derive(Clone, Copy, Debug)]
189pub struct ErasedRawStridedPtr<'a> {
190 dtype: KernelDType,
191 data: NonNull<u8>,
192 byte_len: usize,
193 dims: &'a [usize],
194 strides: &'a [isize],
195 offset: isize,
196 marker: PhantomData<&'a [MaybeUninit<u8>]>,
197}
198
199impl<'a> ErasedRawStridedPtr<'a> {
200 /// Create a pointer-backed descriptor from erased storage.
201 ///
202 /// # Safety
203 ///
204 /// `data` must point into an allocation whose alignment is suitable for
205 /// `dtype`, independently of the observed address. The allocation must
206 /// provide `byte_len` initialized, readable bytes with valid provenance
207 /// for `'a`, and remain alive for `'a`. For `bool`, the bytes may contain
208 /// invalid values temporarily. They must not be read or used to form a
209 /// typed reference until a caller has rejected overlap and
210 /// [`Self::try_as_ref_after_no_overlap`] has validated the complete extent.
211 /// The allocation may overlap a destination.
212 /// The original owner may perform synchronized sequential mutation through
213 /// the same-provenance raw pointer before conversion. No concurrent
214 /// mutation or conflicting access is permitted during overlap checking,
215 /// conversion, or consumer access.
216 ///
217 /// # Errors
218 ///
219 /// Returns [`StridedError::ByteLengthMismatch`] when `byte_len` is not
220 /// a whole number of `dtype` elements, and
221 /// [`StridedError::DataAlignmentMismatch`] when a non-empty `data` is
222 /// not aligned for `dtype`. Returns
223 /// [`StridedError::StrideLengthMismatch`] when `dims` and `strides`
224 /// have different lengths, and [`StridedError::OffsetOverflow`] when
225 /// a reachable offset is not representable: `dim - 1` of a non-unit
226 /// extent does not fit `isize`, a stride times that last index or the
227 /// accumulated offsets overflow `isize` or go negative, or the highest
228 /// reachable offset is not below the storage length. A layout with a zero
229 /// extent skips this check.
230 /// Bool bytes are not checked here;
231 /// [`Self::try_as_ref_after_no_overlap`] does that.
232 pub unsafe fn from_raw_parts(
233 dtype: KernelDType,
234 data: NonNull<u8>,
235 byte_len: usize,
236 dims: &'a [usize],
237 strides: &'a [isize],
238 offset: isize,
239 ) -> Result<Self> {
240 let element_count = validate_erased_buffer_layout(dtype, data, byte_len)?;
241 validate_bounds(element_count, dims, strides, offset)?;
242 Ok(Self {
243 dtype,
244 data,
245 byte_len,
246 dims,
247 strides,
248 offset,
249 marker: PhantomData,
250 })
251 }
252
253 /// Borrow a safe erased input as a pointer descriptor.
254 pub fn from_ref(input: &ErasedRawStridedRef<'a>) -> Self {
255 Self {
256 dtype: input.dtype,
257 data: NonNull::new(input.data.as_ptr().cast_mut()).unwrap_or_else(NonNull::dangling),
258 byte_len: input.data.len(),
259 dims: input.dims,
260 strides: input.strides,
261 offset: input.offset,
262 marker: PhantomData,
263 }
264 }
265
266 /// Convert this pointer to an initialized descriptor after overlap checks.
267 ///
268 /// # Safety
269 ///
270 /// The caller must have proved that no mutable allocation overlaps this
271 /// pointer's complete byte extent.
272 /// The allocation contract from [`Self::from_raw_parts`] must still hold.
273 /// No mutation or other conflicting access may occur during this conversion
274 /// or for the duration of the returned descriptor's consumer access.
275 /// Bool bytes are validated here, immediately before typed access.
276 ///
277 /// # Errors
278 ///
279 /// Returns [`StridedError::InvalidBoolByte`] when `dtype` is `Bool` and
280 /// a byte of the extent is neither 0 nor 1. It repeats the extent
281 /// length, alignment, stride length and bounds checks
282 /// ([`StridedError::ByteLengthMismatch`],
283 /// [`StridedError::DataAlignmentMismatch`],
284 /// [`StridedError::StrideLengthMismatch`],
285 /// [`StridedError::OffsetOverflow`]) that [`Self::from_raw_parts`] and
286 /// [`Self::from_ref`] already applied to the same values, so those
287 /// cannot fire again here.
288 pub unsafe fn try_as_ref_after_no_overlap(&self) -> Result<ErasedRawStridedRef<'_>> {
289 let bytes = core::slice::from_raw_parts(self.data.as_ptr(), self.byte_len);
290 validate_erased_buffer(self.dtype, bytes)?;
291 ErasedRawStridedRef::from_raw_parts(
292 self.dtype,
293 self.data,
294 self.byte_len,
295 self.dims,
296 self.strides,
297 self.offset,
298 )
299 }
300
301 #[inline]
302 pub fn dtype(&self) -> KernelDType {
303 self.dtype
304 }
305
306 /// Check overlap with initialized mutable storage without reading it.
307 ///
308 /// # Errors
309 ///
310 /// Never returns `Err` in practice: both byte extents come from valid
311 /// descriptors, whose contracts already guarantee an allocation of that
312 /// size, so the end-address arithmetic cannot wrap. The `Result` only
313 /// carries that arithmetic's [`StridedError::OffsetOverflow`].
314 pub fn overlaps_mut(&self, dest: &ErasedRawStridedMut<'_>) -> Result<bool> {
315 ranges_overlap(
316 self.data.as_ptr() as usize,
317 self.byte_len,
318 dest.data.as_ptr() as usize,
319 dest.data.len(),
320 )
321 }
322
323 /// Check overlap with uninitialized mutable storage without reading it.
324 ///
325 /// # Errors
326 ///
327 /// Never returns `Err` in practice: both byte extents come from valid
328 /// descriptors, whose contracts already guarantee an allocation of that
329 /// size, so the end-address arithmetic cannot wrap. The `Result` only
330 /// carries that arithmetic's [`StridedError::OffsetOverflow`].
331 pub fn overlaps_uninit_mut(&self, dest: &ErasedRawStridedUninitMut<'_>) -> Result<bool> {
332 ranges_overlap(
333 self.data.as_ptr() as usize,
334 self.byte_len,
335 dest.data.as_ptr() as usize,
336 dest.data.len(),
337 )
338 }
339
340 #[inline]
341 pub fn dims(&self) -> &'a [usize] {
342 self.dims
343 }
344
345 #[inline]
346 pub fn strides(&self) -> &'a [isize] {
347 self.strides
348 }
349
350 #[inline]
351 pub fn offset(&self) -> isize {
352 self.offset
353 }
354}
355
356/// Borrowed dtype-erased raw strided input layout.
357///
358/// `dims`, `strides`, and `offset` are expressed in dtype elements, not bytes.
359#[derive(Clone, Copy)]
360pub struct ErasedRawStridedRef<'a> {
361 dtype: KernelDType,
362 data: &'a [u8],
363 dims: &'a [usize],
364 strides: &'a [isize],
365 offset: isize,
366}
367
368// A derived `Debug` would print every byte of the backing buffer; the layout
369// metadata is what a reader wants.
370impl core::fmt::Debug for ErasedRawStridedRef<'_> {
371 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
372 f.debug_struct("ErasedRawStridedRef")
373 .field("dtype", &self.dtype)
374 .field("byte_len", &self.data.len())
375 .field("dims", &self.dims)
376 .field("strides", &self.strides)
377 .field("offset", &self.offset)
378 .finish()
379 }
380}
381
382impl<'a> ErasedRawStridedRef<'a> {
383 /// Create an erased input descriptor from typed, initialized storage.
384 ///
385 /// # Errors
386 ///
387 /// Returns [`StridedError::StrideLengthMismatch`] when `dims` and
388 /// `strides` have different lengths, and
389 /// [`StridedError::OffsetOverflow`] when a reachable offset is not
390 /// representable: `dim - 1` of a non-unit extent does not fit `isize`, a
391 /// stride times that last index or the accumulated offsets overflow
392 /// `isize` or go negative, or the highest reachable offset is not below
393 /// the buffer length. A layout with a zero extent skips this check. The
394 /// byte-length and alignment checks
395 /// cannot fail for a typed `&[T]`.
396 pub fn from_slice<T: KernelStorageElement>(
397 data: &'a [T],
398 dims: &'a [usize],
399 strides: &'a [isize],
400 offset: isize,
401 ) -> Result<Self> {
402 let dtype = T::DTYPE;
403 let byte_len = data
404 .len()
405 .checked_mul(core::mem::size_of::<T>())
406 .ok_or(StridedError::OffsetOverflow)?;
407 // SAFETY: `data` is a live `&[T]` and `byte_len` is
408 // `data.len() * size_of::<T>()` from the `checked_mul` above, so the
409 // `u8` view spans exactly that initialized allocation for `data`'s
410 // lifetime. `u8` has no invalid bit patterns, and the resulting length
411 // is re-checked by `validate_erased_buffer_extent` (raw.rs:121).
412 let bytes = unsafe { core::slice::from_raw_parts(data.as_ptr().cast::<u8>(), byte_len) };
413 let element_count = validate_erased_buffer_extent(dtype, bytes)?;
414 validate_bounds(element_count, dims, strides, offset)?;
415 Ok(Self {
416 dtype,
417 data: bytes,
418 dims,
419 strides,
420 offset,
421 })
422 }
423
424 /// Construct from erased storage.
425 ///
426 /// # Safety
427 /// `data` must be the start of an allocation aligned for `dtype`; the
428 /// allocation must contain `byte_len` initialized, readable bytes for
429 /// `'a`, and every byte extent must represent valid values for `dtype`.
430 /// The allocation and metadata must outlive `'a`; no mutable alias may
431 /// exist while this descriptor is used. Alignment is an allocation
432 /// property and must not be inferred from the observed address alone.
433 ///
434 /// # Errors
435 ///
436 /// Returns [`StridedError::ByteLengthMismatch`] when `byte_len` is not
437 /// a whole number of `dtype` elements, and
438 /// [`StridedError::DataAlignmentMismatch`] when a non-empty `data` is
439 /// not aligned for `dtype`. Returns
440 /// [`StridedError::StrideLengthMismatch`] when `dims` and `strides`
441 /// have different lengths, and [`StridedError::OffsetOverflow`] when
442 /// a reachable offset is not representable: `dim - 1` of a non-unit
443 /// extent does not fit `isize`, a stride times that last index or the
444 /// accumulated offsets overflow `isize` or go negative, or the highest
445 /// reachable offset is not below the storage length. A layout with a zero
446 /// extent skips this check.
447 /// Bool bytes are the caller's responsibility here, per the safety
448 /// contract.
449 pub unsafe fn from_raw_parts(
450 dtype: KernelDType,
451 data: NonNull<u8>,
452 byte_len: usize,
453 dims: &'a [usize],
454 strides: &'a [isize],
455 offset: isize,
456 ) -> Result<Self> {
457 let count = validate_erased_buffer_layout(dtype, data, byte_len)?;
458 let bytes = core::slice::from_raw_parts(data.as_ptr(), byte_len);
459 validate_bounds(count, dims, strides, offset)?;
460 Ok(Self {
461 dtype,
462 data: bytes,
463 dims,
464 strides,
465 offset,
466 })
467 }
468
469 /// Borrow the initialized storage as its concrete scalar type.
470 ///
471 /// # Errors
472 ///
473 /// Returns [`StridedError::DTypeMismatch`] when `T` is not the
474 /// descriptor's dtype.
475 pub fn data_as<T: KernelStorageElement>(&self) -> Result<&[T]> {
476 if self.dtype != T::DTYPE {
477 return Err(StridedError::DTypeMismatch {
478 expected: T::DTYPE.label(),
479 actual: self.dtype.label(),
480 });
481 }
482 // SAFETY: the dtype check above proves the stored dtype is `T`, and the
483 // constructor ran `validate_erased_buffer_extent` (raw.rs:121) /
484 // `validate_erased_buffer_layout` (raw.rs:161): the byte length is a
485 // whole multiple of `size_of::<T>()` and the base is `T`-aligned, so
486 // `len / size_of::<T>()` initialized `T` values are in bounds.
487 Ok(unsafe {
488 core::slice::from_raw_parts(
489 self.data.as_ptr().cast::<T>(),
490 self.data.len() / core::mem::size_of::<T>(),
491 )
492 })
493 }
494
495 #[inline]
496 pub fn dtype(&self) -> KernelDType {
497 self.dtype
498 }
499
500 #[inline]
501 pub fn dims(&self) -> &'a [usize] {
502 self.dims
503 }
504
505 #[inline]
506 pub fn strides(&self) -> &'a [isize] {
507 self.strides
508 }
509
510 #[inline]
511 pub fn offset(&self) -> isize {
512 self.offset
513 }
514}
515
516/// Borrowed dtype-erased raw strided output layout.
517///
518/// `dims`, `strides`, and `offset` are expressed in dtype elements, not bytes.
519pub struct ErasedRawStridedMut<'a> {
520 dtype: KernelDType,
521 data: &'a mut [u8],
522 dims: &'a [usize],
523 strides: &'a [isize],
524 offset: isize,
525}
526
527// A derived `Debug` would print every byte of the backing buffer; the layout
528// metadata is what a reader wants.
529impl core::fmt::Debug for ErasedRawStridedMut<'_> {
530 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
531 f.debug_struct("ErasedRawStridedMut")
532 .field("dtype", &self.dtype)
533 .field("byte_len", &self.data.len())
534 .field("dims", &self.dims)
535 .field("strides", &self.strides)
536 .field("offset", &self.offset)
537 .finish()
538 }
539}
540
541impl<'a> ErasedRawStridedMut<'a> {
542 /// Create an erased output descriptor from typed, initialized storage.
543 ///
544 /// # Errors
545 ///
546 /// Returns [`StridedError::StrideLengthMismatch`] when `dims` and
547 /// `strides` have different lengths, and
548 /// [`StridedError::OffsetOverflow`] when a reachable offset is not
549 /// representable: `dim - 1` of a non-unit extent does not fit `isize`, a
550 /// stride times that last index or the accumulated offsets overflow
551 /// `isize` or go negative, or the highest reachable offset is not below
552 /// the buffer length. A layout with a zero extent skips this check. The
553 /// byte-length and alignment checks
554 /// cannot fail for a typed `&mut [T]`.
555 pub fn from_slice_mut<T: KernelStorageElement>(
556 data: &'a mut [T],
557 dims: &'a [usize],
558 strides: &'a [isize],
559 offset: isize,
560 ) -> Result<Self> {
561 let dtype = T::DTYPE;
562 let byte_len = data
563 .len()
564 .checked_mul(core::mem::size_of::<T>())
565 .ok_or(StridedError::OffsetOverflow)?;
566 // SAFETY: `data` is a live, exclusively borrowed `&mut [T]` and
567 // `byte_len` is `data.len() * size_of::<T>()` from the `checked_mul`
568 // above, so the `u8` view spans exactly that initialized allocation for
569 // `'a`. `u8` has no invalid bit patterns, and the resulting length is
570 // re-checked by `validate_erased_buffer_extent` (raw.rs:121).
571 let data =
572 unsafe { core::slice::from_raw_parts_mut(data.as_mut_ptr().cast::<u8>(), byte_len) };
573 let element_count = validate_erased_buffer_extent(dtype, data)?;
574 validate_bounds(element_count, dims, strides, offset)?;
575 Ok(Self {
576 dtype,
577 data,
578 dims,
579 strides,
580 offset,
581 })
582 }
583
584 /// Construct from erased storage.
585 ///
586 /// # Safety
587 /// `data..data + byte_len` must be an aligned, writable allocation valid
588 /// for `'a`, with valid initialized values for `dtype` throughout the
589 /// complete byte extent; its provenance, extent, lifetime, and exclusive
590 /// aliasing must be upheld by the caller.
591 /// The alignment requirement is on the allocation, not merely the observed
592 /// address. The caller must retain exclusive access: no mutable alias or
593 /// concurrent mutation may exist while this descriptor is used.
594 ///
595 /// # Errors
596 ///
597 /// Returns [`StridedError::ByteLengthMismatch`] when `byte_len` is not
598 /// a whole number of `dtype` elements, and
599 /// [`StridedError::DataAlignmentMismatch`] when a non-empty `data` is
600 /// not aligned for `dtype`. Returns
601 /// [`StridedError::InvalidBoolByte`] when `dtype` is `Bool` and a byte
602 /// of the extent is neither 0 nor 1. Returns
603 /// [`StridedError::StrideLengthMismatch`] when `dims` and `strides`
604 /// have different lengths, and [`StridedError::OffsetOverflow`] when
605 /// a reachable offset is not representable: `dim - 1` of a non-unit
606 /// extent does not fit `isize`, a stride times that last index or the
607 /// accumulated offsets overflow `isize` or go negative, or the highest
608 /// reachable offset is not below the storage length. A layout with a zero
609 /// extent skips this check.
610 pub unsafe fn from_raw_parts(
611 dtype: KernelDType,
612 data: NonNull<u8>,
613 byte_len: usize,
614 dims: &'a [usize],
615 strides: &'a [isize],
616 offset: isize,
617 ) -> Result<Self> {
618 let count = validate_erased_buffer_layout(dtype, data, byte_len)?;
619 let data = core::slice::from_raw_parts_mut(data.as_ptr(), byte_len);
620 validate_erased_buffer(dtype, data)?;
621 validate_bounds(count, dims, strides, offset)?;
622 Ok(Self {
623 dtype,
624 data,
625 dims,
626 strides,
627 offset,
628 })
629 }
630
631 /// Borrow initialized storage as its concrete scalar type.
632 ///
633 /// # Errors
634 ///
635 /// Returns [`StridedError::DTypeMismatch`] when `T` is not the
636 /// descriptor's dtype.
637 pub fn data_as<T: KernelStorageElement>(&self) -> Result<&[T]> {
638 if self.dtype != T::DTYPE {
639 return Err(StridedError::DTypeMismatch {
640 expected: T::DTYPE.label(),
641 actual: self.dtype.label(),
642 });
643 }
644 // SAFETY: the dtype check above proves the stored dtype is `T`, and the
645 // constructor ran `validate_erased_buffer_extent` (raw.rs:121) /
646 // `validate_erased_buffer_layout` (raw.rs:161): the byte length is a
647 // whole multiple of `size_of::<T>()` and the base is `T`-aligned, so
648 // `len / size_of::<T>()` initialized `T` values are in bounds.
649 Ok(unsafe {
650 core::slice::from_raw_parts(
651 self.data.as_ptr().cast::<T>(),
652 self.data.len() / core::mem::size_of::<T>(),
653 )
654 })
655 }
656
657 /// Borrow initialized storage mutably as its concrete scalar type.
658 ///
659 /// # Errors
660 ///
661 /// Returns [`StridedError::DTypeMismatch`] when `T` is not the
662 /// descriptor's dtype.
663 pub fn data_as_mut<T: KernelStorageElement>(&mut self) -> Result<&mut [T]> {
664 if self.dtype != T::DTYPE {
665 return Err(StridedError::DTypeMismatch {
666 expected: T::DTYPE.label(),
667 actual: self.dtype.label(),
668 });
669 }
670 // SAFETY: as `data_as` (raw.rs:637) — the dtype check proves `T`, and
671 // `validate_erased_buffer_layout` (raw.rs:161) proved the base is
672 // `T`-aligned with a whole multiple of `size_of::<T>()` bytes. `&mut
673 // self` keeps this reborrow exclusive for its own lifetime.
674 Ok(unsafe {
675 core::slice::from_raw_parts_mut(
676 self.data.as_mut_ptr().cast::<T>(),
677 self.data.len() / core::mem::size_of::<T>(),
678 )
679 })
680 }
681
682 #[inline]
683 pub fn dtype(&self) -> KernelDType {
684 self.dtype
685 }
686
687 #[inline]
688 pub fn dims(&self) -> &'a [usize] {
689 self.dims
690 }
691
692 #[inline]
693 pub fn strides(&self) -> &'a [isize] {
694 self.strides
695 }
696
697 #[inline]
698 pub fn offset(&self) -> isize {
699 self.offset
700 }
701}
702
703/// Borrowed dtype-erased raw strided output whose reachable elements may be
704/// uninitialized.
705///
706/// This descriptor is only accepted by operations that prove and perform a
707/// full overwrite of every reachable logical destination element. The backing
708/// allocation may contain non-reachable holes, which remain uninitialized.
709pub struct ErasedRawStridedUninitMut<'a> {
710 dtype: KernelDType,
711 data: &'a mut [MaybeUninit<u8>],
712 dims: &'a [usize],
713 strides: &'a [isize],
714 offset: isize,
715}
716
717// A derived `Debug` would print one entry per byte of the backing buffer; the
718// layout metadata is what a reader wants.
719impl core::fmt::Debug for ErasedRawStridedUninitMut<'_> {
720 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
721 f.debug_struct("ErasedRawStridedUninitMut")
722 .field("dtype", &self.dtype)
723 .field("byte_len", &self.data.len())
724 .field("dims", &self.dims)
725 .field("strides", &self.strides)
726 .field("offset", &self.offset)
727 .finish()
728 }
729}
730
731impl<'a> ErasedRawStridedUninitMut<'a> {
732 /// Create an uninitialized erased output descriptor from typed storage.
733 ///
734 /// # Errors
735 ///
736 /// Returns [`StridedError::StrideLengthMismatch`] when `dims` and
737 /// `strides` have different lengths, and
738 /// [`StridedError::OffsetOverflow`] when a reachable offset is not
739 /// representable: `dim - 1` of a non-unit extent does not fit `isize`, a
740 /// stride times that last index or the accumulated offsets overflow
741 /// `isize` or go negative, or the highest reachable offset is not below
742 /// the buffer length. A layout with a zero extent skips this check. The
743 /// byte-length and alignment checks
744 /// cannot fail for a typed `&mut [MaybeUninit<T>]`.
745 pub fn from_uninit_slice<T: KernelStorageElement>(
746 data: &'a mut [MaybeUninit<T>],
747 dims: &'a [usize],
748 strides: &'a [isize],
749 offset: isize,
750 ) -> Result<Self> {
751 let dtype = T::DTYPE;
752 let byte_len = data
753 .len()
754 .checked_mul(core::mem::size_of::<T>())
755 .ok_or(StridedError::OffsetOverflow)?;
756 // SAFETY: `data` is a live, exclusively borrowed `&mut
757 // [MaybeUninit<T>]` and `byte_len` is `data.len() * size_of::<T>()` from
758 // the `checked_mul` above, so the view spans exactly that allocation for
759 // `'a`. `MaybeUninit<u8>` accepts every bit pattern, so no
760 // initialization is asserted here.
761 let data = unsafe {
762 core::slice::from_raw_parts_mut(data.as_mut_ptr().cast::<MaybeUninit<u8>>(), byte_len)
763 };
764 let data_ptr =
765 NonNull::new(data.as_mut_ptr().cast::<u8>()).unwrap_or_else(NonNull::dangling);
766 let element_count = validate_erased_buffer_layout(dtype, data_ptr, byte_len)?;
767 validate_bounds(element_count, dims, strides, offset)?;
768 Ok(Self {
769 dtype,
770 data,
771 dims,
772 strides,
773 offset,
774 })
775 }
776
777 /// Construct from erased uninitialized storage.
778 ///
779 /// # Safety
780 /// `data..data + byte_len` must be an aligned, writable allocation valid
781 /// for `'a`, with provenance and extent sufficient for all reachable
782 /// elements. The caller must ensure every reachable element is completely
783 /// overwritten before it is read or exposed as initialized storage. The
784 /// allocation's alignment is an allocation property independent of the
785 /// observed address, and the descriptor must have exclusive access with no
786 /// mutable alias or concurrent mutation during use.
787 ///
788 /// # Errors
789 ///
790 /// Returns [`StridedError::ByteLengthMismatch`] when `byte_len` is not
791 /// a whole number of `dtype` elements, and
792 /// [`StridedError::DataAlignmentMismatch`] when a non-empty `data` is
793 /// not aligned for `dtype`. Returns
794 /// [`StridedError::StrideLengthMismatch`] when `dims` and `strides`
795 /// have different lengths, and [`StridedError::OffsetOverflow`] when
796 /// a reachable offset is not representable: `dim - 1` of a non-unit
797 /// extent does not fit `isize`, a stride times that last index or the
798 /// accumulated offsets overflow `isize` or go negative, or the highest
799 /// reachable offset is not below the storage length. A layout with a zero
800 /// extent skips this check.
801 pub unsafe fn from_raw_parts(
802 dtype: KernelDType,
803 data: NonNull<u8>,
804 byte_len: usize,
805 dims: &'a [usize],
806 strides: &'a [isize],
807 offset: isize,
808 ) -> Result<Self> {
809 let count = validate_erased_buffer_layout(dtype, data, byte_len)?;
810 let data =
811 core::slice::from_raw_parts_mut(data.as_ptr().cast::<MaybeUninit<u8>>(), byte_len);
812 validate_bounds(count, dims, strides, offset)?;
813 Ok(Self {
814 dtype,
815 data,
816 dims,
817 strides,
818 offset,
819 })
820 }
821
822 /// Borrow the uninitialized storage as its concrete scalar type.
823 ///
824 /// # Errors
825 ///
826 /// Returns [`StridedError::DTypeMismatch`] when `T` is not the
827 /// descriptor's dtype.
828 pub fn data_as_uninit_mut<T: KernelStorageElement>(&mut self) -> Result<&mut [MaybeUninit<T>]> {
829 if self.dtype != T::DTYPE {
830 return Err(StridedError::DTypeMismatch {
831 expected: T::DTYPE.label(),
832 actual: self.dtype.label(),
833 });
834 }
835 // SAFETY: the dtype check proves `T`, and
836 // `validate_erased_buffer_layout` (raw.rs:161) proved the base is
837 // `T`-aligned with a whole multiple of `size_of::<T>()` bytes.
838 // `MaybeUninit<T>` accepts every bit pattern, so the partially
839 // initialized extent is not read as `T` here.
840 Ok(unsafe {
841 core::slice::from_raw_parts_mut(
842 self.data.as_mut_ptr().cast::<MaybeUninit<T>>(),
843 self.data.len() / core::mem::size_of::<T>(),
844 )
845 })
846 }
847
848 #[inline]
849 pub fn dtype(&self) -> KernelDType {
850 self.dtype
851 }
852
853 #[inline]
854 pub fn dims(&self) -> &'a [usize] {
855 self.dims
856 }
857
858 #[inline]
859 pub fn strides(&self) -> &'a [isize] {
860 self.strides
861 }
862
863 #[inline]
864 pub fn offset(&self) -> isize {
865 self.offset
866 }
867}
868
869fn ranges_overlap(a_start: usize, a_len: usize, b_start: usize, b_len: usize) -> Result<bool> {
870 let a_end = a_start
871 .checked_add(a_len)
872 .ok_or(StridedError::OffsetOverflow)?;
873 let b_end = b_start
874 .checked_add(b_len)
875 .ok_or(StridedError::OffsetOverflow)?;
876 Ok(a_start < b_end && b_start < a_end)
877}
878
879/// Borrowed raw strided input layout.
880///
881/// Use [`RawStridedRef::new`] for checked construction, or
882/// [`RawStridedRef::new_unchecked`] when a higher-level compiled plan has
883/// already validated bounds.
884#[derive(Clone, Copy)]
885pub struct RawStridedRef<'a, T> {
886 data: &'a [T],
887 dims: &'a [usize],
888 strides: &'a [isize],
889 offset: isize,
890}
891
892// A derived `Debug` would print every element of the backing buffer; the layout
893// metadata is what a reader wants.
894impl<T: core::fmt::Debug> core::fmt::Debug for RawStridedRef<'_, T> {
895 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
896 f.debug_struct("RawStridedRef")
897 .field("data_len", &self.data.len())
898 .field("dims", &self.dims)
899 .field("strides", &self.strides)
900 .field("offset", &self.offset)
901 .finish()
902 }
903}
904
905impl<'a, T> RawStridedRef<'a, T> {
906 /// Create a raw strided input after validating reachable offsets.
907 ///
908 /// # Errors
909 ///
910 /// Returns [`StridedError::StrideLengthMismatch`] when `dims` and
911 /// `strides` have different lengths, and
912 /// [`StridedError::OffsetOverflow`] when a reachable offset is not
913 /// representable: `dim - 1` of a non-unit extent does not fit `isize`, a
914 /// stride times that last index or the accumulated offsets overflow
915 /// `isize` or go negative, or the highest reachable offset is not below
916 /// the buffer length. A layout with a zero extent skips this check.
917 pub fn new(
918 data: &'a [T],
919 dims: &'a [usize],
920 strides: &'a [isize],
921 offset: isize,
922 ) -> Result<Self> {
923 validate_bounds(data.len(), dims, strides, offset)?;
924 Ok(Self {
925 data,
926 dims,
927 strides,
928 offset,
929 })
930 }
931
932 /// Create a raw strided input without bounds checking.
933 ///
934 /// # Safety
935 /// The caller must ensure every index reachable by `dims`/`strides` from
936 /// `offset` lies inside `data`.
937 pub unsafe fn new_unchecked(
938 data: &'a [T],
939 dims: &'a [usize],
940 strides: &'a [isize],
941 offset: isize,
942 ) -> Self {
943 Self {
944 data,
945 dims,
946 strides,
947 offset,
948 }
949 }
950
951 #[inline]
952 pub fn data(&self) -> &'a [T] {
953 self.data
954 }
955
956 #[inline]
957 pub fn dims(&self) -> &'a [usize] {
958 self.dims
959 }
960
961 #[inline]
962 pub fn strides(&self) -> &'a [isize] {
963 self.strides
964 }
965
966 #[inline]
967 pub fn offset(&self) -> isize {
968 self.offset
969 }
970
971 #[inline]
972 pub fn ptr(&self) -> *const T {
973 if self.dims.contains(&0) {
974 NonNull::<T>::dangling().as_ptr()
975 } else {
976 // SAFETY: `offset` was accepted by `validate_bounds`
977 // (strided-view/src/view.rs:186) in `RawStridedRef::new`, or by the
978 // caller contract of `new_unchecked`, so `data.as_ptr() + offset`
979 // stays inside `data`. The empty-layout branch above returns a
980 // dangling pointer instead of offsetting a pointer no element
981 // reaches.
982 unsafe { self.data.as_ptr().offset(self.offset) }
983 }
984 }
985
986 /// Convert to an immutable view, borrowing this descriptor's metadata.
987 ///
988 /// The conversion allocates nothing; hot prepared paths use the raw
989 /// accessors directly all the same.
990 #[inline]
991 pub fn as_view(&self) -> StridedView<'a, T, Identity> {
992 // SAFETY: `new_unchecked` requires every reachable index to stay inside
993 // `data`; this descriptor's `offset`/`dims`/`strides` came from
994 // `validate_bounds` (strided-view/src/view.rs:186) in `RawStridedRef::new`,
995 // or from the same contract on `new_unchecked`.
996 unsafe { StridedView::new_unchecked(self.data, self.dims, self.strides, self.offset) }
997 }
998}
999
1000/// Borrowed raw strided output layout.
1001///
1002/// This is the mutable counterpart to [`RawStridedRef`]. It avoids allocating
1003/// owned shape/stride metadata in prepared replay paths.
1004pub struct RawStridedMut<'a, T> {
1005 data: &'a mut [T],
1006 dims: &'a [usize],
1007 strides: &'a [isize],
1008 offset: isize,
1009}
1010
1011// A derived `Debug` would print every element of the backing buffer; the layout
1012// metadata is what a reader wants.
1013impl<T: core::fmt::Debug> core::fmt::Debug for RawStridedMut<'_, T> {
1014 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1015 f.debug_struct("RawStridedMut")
1016 .field("data_len", &self.data.len())
1017 .field("dims", &self.dims)
1018 .field("strides", &self.strides)
1019 .field("offset", &self.offset)
1020 .finish()
1021 }
1022}
1023
1024impl<'a, T> RawStridedMut<'a, T> {
1025 /// Create a raw strided output after validating reachable offsets.
1026 ///
1027 /// # Errors
1028 ///
1029 /// Returns [`StridedError::StrideLengthMismatch`] when `dims` and
1030 /// `strides` have different lengths, and
1031 /// [`StridedError::OffsetOverflow`] when a reachable offset is not
1032 /// representable: `dim - 1` of a non-unit extent does not fit `isize`, a
1033 /// stride times that last index or the accumulated offsets overflow
1034 /// `isize` or go negative, or the highest reachable offset is not below
1035 /// the buffer length. A layout with a zero extent skips this check.
1036 pub fn new(
1037 data: &'a mut [T],
1038 dims: &'a [usize],
1039 strides: &'a [isize],
1040 offset: isize,
1041 ) -> Result<Self> {
1042 validate_bounds(data.len(), dims, strides, offset)?;
1043 Ok(Self {
1044 data,
1045 dims,
1046 strides,
1047 offset,
1048 })
1049 }
1050
1051 /// Create a raw strided output without bounds checking.
1052 ///
1053 /// # Safety
1054 /// The caller must ensure every index reachable by `dims`/`strides` from
1055 /// `offset` lies inside `data`, and no aliases violate mutable access.
1056 pub unsafe fn new_unchecked(
1057 data: &'a mut [T],
1058 dims: &'a [usize],
1059 strides: &'a [isize],
1060 offset: isize,
1061 ) -> Self {
1062 Self {
1063 data,
1064 dims,
1065 strides,
1066 offset,
1067 }
1068 }
1069
1070 #[inline]
1071 pub fn data(&self) -> &[T] {
1072 self.data
1073 }
1074
1075 #[inline]
1076 pub fn data_mut(&mut self) -> &mut [T] {
1077 self.data
1078 }
1079
1080 #[inline]
1081 pub fn dims(&self) -> &'a [usize] {
1082 self.dims
1083 }
1084
1085 #[inline]
1086 pub fn strides(&self) -> &'a [isize] {
1087 self.strides
1088 }
1089
1090 #[inline]
1091 pub fn offset(&self) -> isize {
1092 self.offset
1093 }
1094
1095 #[inline]
1096 pub fn ptr(&self) -> *const T {
1097 if self.dims.contains(&0) {
1098 NonNull::<T>::dangling().as_ptr()
1099 } else {
1100 // SAFETY: `offset` was accepted by `validate_bounds`
1101 // (strided-view/src/view.rs:186) in `RawStridedMut::new`, or by the
1102 // caller contract of `new_unchecked`, so `data.as_ptr() + offset`
1103 // stays inside `data`. The empty-layout branch above returns a
1104 // dangling pointer instead of offsetting a pointer no element
1105 // reaches.
1106 unsafe { self.data.as_ptr().offset(self.offset) }
1107 }
1108 }
1109
1110 #[inline]
1111 pub fn as_mut_ptr(&mut self) -> *mut T {
1112 if self.dims.contains(&0) {
1113 NonNull::<T>::dangling().as_ptr()
1114 } else {
1115 // SAFETY: as `ptr` above — `offset` was accepted by
1116 // `validate_bounds` (strided-view/src/view.rs:186) in
1117 // `RawStridedMut::new` or by the `new_unchecked` contract, and
1118 // `&mut self` keeps the write provenance exclusive.
1119 unsafe { self.data.as_mut_ptr().offset(self.offset) }
1120 }
1121 }
1122
1123 /// Convert to an immutable view, borrowing this descriptor's metadata.
1124 ///
1125 /// The conversion allocates nothing; hot prepared paths use the raw
1126 /// accessors directly all the same.
1127 #[inline]
1128 pub fn as_view(&self) -> StridedView<'_, T, Identity> {
1129 // SAFETY: `new_unchecked` requires every reachable index to stay inside
1130 // `data`; this descriptor's metadata came from `validate_bounds`
1131 // (strided-view/src/view.rs:186) in `RawStridedMut::new`, or from the
1132 // same contract on `new_unchecked`.
1133 unsafe { StridedView::new_unchecked(self.data, self.dims, self.strides, self.offset) }
1134 }
1135
1136 /// Convert to a mutable view, borrowing this descriptor's metadata.
1137 ///
1138 /// The conversion allocates nothing; hot prepared paths use the raw
1139 /// accessors directly all the same.
1140 #[inline]
1141 pub fn as_view_mut(&mut self) -> StridedViewMut<'_, T> {
1142 // SAFETY: as `as_view` above, and `&mut self` keeps the mutable borrow
1143 // exclusive to the returned view, which `new_unchecked` requires.
1144 unsafe { StridedViewMut::new_unchecked(self.data, self.dims, self.strides, self.offset) }
1145 }
1146}
1147
1148#[cfg(test)]
1149mod tests;