Skip to main content

strided_basic/
erased.rs

1use crate::erased_common::*;
2use crate::*;
3use core::mem::MaybeUninit;
4use num_complex::{Complex32, Complex64};
5use num_traits::{One, Zero};
6mod arg_reduce;
7mod line;
8mod norm;
9mod reduce_kernel;
10mod scan;
11
12pub use arg_reduce::{ArgReduceOp, ErasedArgReducePlan};
13pub use norm::{ErasedNormPlan, NormKind, NormSpec};
14pub use scan::{ErasedScanPlan, ScanOp, ScanOptions};
15
16use reduce_kernel::{
17    reduce_axes_range, reduce_full_range, AxesRange, FullTraversal, MaxKernel, MinKernel,
18    ProductKernel, ReduceKernel, SumKernel, SumSquaresKernel,
19};
20
21trait ReduceWriter<T> {
22    fn offset(&self) -> isize;
23    /// # Safety
24    /// The pointer may only be used within the validated destination extent.
25    unsafe fn ptr(&mut self) -> *mut T;
26    fn extent(&self) -> usize;
27    /// # Safety
28    /// The offset must be an in-bounds logical reduction destination offset.
29    unsafe fn write_at(&mut self, offset: isize, value: T) {
30        debug_assert!(offset >= 0 && (offset as usize) < self.extent());
31        // SAFETY: reduction layout validation proves the logical offset.
32        unsafe { self.ptr().offset(offset).write(value) }
33    }
34}
35
36struct RawReduceWriter<'a, T> {
37    ptr: *mut T,
38    extent: usize,
39    offset: isize,
40    _marker: core::marker::PhantomData<&'a mut [MaybeUninit<T>]>,
41}
42
43impl<'a, T> ReduceWriter<T> for RawReduceWriter<'a, T> {
44    fn offset(&self) -> isize {
45        self.offset
46    }
47    unsafe fn ptr(&mut self) -> *mut T {
48        self.ptr
49    }
50    fn extent(&self) -> usize {
51        self.extent
52    }
53}
54
55/// Dtype-erased wrapper around [`CopyPlan`].
56#[derive(Clone, Debug)]
57pub struct ErasedCopyPlan {
58    dtype: KernelDType,
59    plan: CopyPlan,
60}
61
62/// Dtype-erased concatenate wrapper.
63#[derive(Clone, Debug)]
64pub struct ErasedConcatenatePlan {
65    dtype: KernelDType,
66    plan: ConcatenatePlan,
67}
68
69impl ErasedCopyPlan {
70    /// Compile a copy plan for one dtype and layout pair.
71    pub fn compile(
72        dtype: KernelDType,
73        dims: &[usize],
74        dst_strides: &[isize],
75        src_strides: &[isize],
76    ) -> Result<Self> {
77        Ok(Self {
78            dtype,
79            plan: CopyPlan::compile(dims, dst_strides, src_strides)?,
80        })
81    }
82
83    #[inline]
84    pub fn dtype(&self) -> KernelDType {
85        self.dtype
86    }
87
88    /// `dest = src` through a non-generic dtype-erased replay boundary.
89    pub fn execute(
90        &self,
91        ctx: &ExecContext,
92        dest: &mut ErasedRawStridedMut<'_>,
93        src: &ErasedRawStridedRef<'_>,
94    ) -> Result<()> {
95        self.check_dtype(dest.dtype())?;
96        self.check_dtype(src.dtype())?;
97
98        let result = ctx.run(|| match self.dtype {
99            KernelDType::F32 => execute_copy::<f32>(&self.plan, dest, src),
100            KernelDType::F64 => execute_copy::<f64>(&self.plan, dest, src),
101            KernelDType::I32 => execute_copy::<i32>(&self.plan, dest, src),
102            KernelDType::I64 => execute_copy::<i64>(&self.plan, dest, src),
103            KernelDType::Bool => execute_copy::<bool>(&self.plan, dest, src),
104            KernelDType::C32 => execute_copy::<Complex32>(&self.plan, dest, src),
105            KernelDType::C64 => execute_copy::<Complex64>(&self.plan, dest, src),
106            _ => Err(StridedError::UnsupportedDType {
107                dtype: self.dtype.label(),
108            }),
109        });
110        result
111    }
112
113    fn check_dtype(&self, actual: KernelDType) -> Result<()> {
114        if actual != self.dtype {
115            return Err(StridedError::DTypeMismatch {
116                expected: self.dtype.label(),
117                actual: actual.label(),
118            });
119        }
120        Ok(())
121    }
122}
123
124impl ErasedConcatenatePlan {
125    /// Validate and store a concatenate plan for one dtype and fixed layout set.
126    pub fn compile(
127        dtype: KernelDType,
128        input_dims: &[&[usize]],
129        input_strides: &[&[isize]],
130        dest_dims: &[usize],
131        dest_strides: &[isize],
132        axis: usize,
133    ) -> Result<Self> {
134        check_static_indexing_dtype(dtype)?;
135        Ok(Self {
136            dtype,
137            plan: ConcatenatePlan::compile(
138                input_dims,
139                input_strides,
140                dest_dims,
141                dest_strides,
142                axis,
143            )?,
144        })
145    }
146
147    #[inline]
148    pub fn dtype(&self) -> KernelDType {
149        self.dtype
150    }
151
152    #[inline]
153    pub fn plan(&self) -> &ConcatenatePlan {
154        &self.plan
155    }
156
157    /// Execute concatenate into an erased output descriptor.
158    pub fn execute(
159        &self,
160        ctx: &ExecContext,
161        dest: &mut ErasedRawStridedMut<'_>,
162        inputs: &[ErasedRawStridedRef<'_>],
163    ) -> Result<()> {
164        check_dtype(self.dtype, dest.dtype())?;
165        for input in inputs {
166            check_dtype(self.dtype, input.dtype())?;
167        }
168
169        let result = ctx.run(|| match self.dtype {
170            KernelDType::F32 => execute_concatenate::<f32>(&self.plan, dest, inputs),
171            KernelDType::F64 => execute_concatenate::<f64>(&self.plan, dest, inputs),
172            KernelDType::I32 => execute_concatenate::<i32>(&self.plan, dest, inputs),
173            KernelDType::I64 => execute_concatenate::<i64>(&self.plan, dest, inputs),
174            KernelDType::Bool => execute_concatenate::<bool>(&self.plan, dest, inputs),
175            KernelDType::C32 => execute_concatenate::<Complex32>(&self.plan, dest, inputs),
176            KernelDType::C64 => execute_concatenate::<Complex64>(&self.plan, dest, inputs),
177            _ => Err(StridedError::UnsupportedDType {
178                dtype: self.dtype.label(),
179            }),
180        });
181        result
182    }
183
184    /// Execute concatenate as a full overwrite of uninitialized output storage.
185    /// On success, every reachable destination slot is fully overwritten;
186    /// unreachable holes are neither read nor initialized. Validation errors
187    /// are returned before any destination write. A panic during execution
188    /// may leave a partially initialized `MaybeUninit` destination, which is
189    /// still safely droppable; no readable value is promised for unwritten
190    /// reachable slots.
191    pub fn execute_uninit(
192        &self,
193        ctx: &ExecContext,
194        dest: &mut ErasedRawStridedUninitMut<'_>,
195        inputs: &[ErasedRawStridedPtr<'_>],
196    ) -> Result<()> {
197        check_dtype(self.dtype, dest.dtype())?;
198        if inputs.len() != self.plan.input_count() {
199            return Err(StridedError::RankMismatch(
200                inputs.len(),
201                self.plan.input_count(),
202            ));
203        }
204        for input in inputs {
205            check_dtype(self.dtype, input.dtype())?;
206        }
207        for (position, input) in inputs.iter().enumerate() {
208            validate_uninit_no_overlap(dest, input, position)?;
209        }
210        for input in inputs {
211            // SAFETY: input/output overlap was rejected before forming references.
212            unsafe { input.try_as_ref_after_no_overlap() }?;
213        }
214
215        ctx.run(|| match self.dtype {
216            KernelDType::F32 => execute_concatenate_uninit::<f32>(&self.plan, dest, inputs),
217            KernelDType::F64 => execute_concatenate_uninit::<f64>(&self.plan, dest, inputs),
218            KernelDType::I32 => execute_concatenate_uninit::<i32>(&self.plan, dest, inputs),
219            KernelDType::I64 => execute_concatenate_uninit::<i64>(&self.plan, dest, inputs),
220            KernelDType::Bool => execute_concatenate_uninit::<bool>(&self.plan, dest, inputs),
221            KernelDType::C32 => execute_concatenate_uninit::<Complex32>(&self.plan, dest, inputs),
222            KernelDType::C64 => execute_concatenate_uninit::<Complex64>(&self.plan, dest, inputs),
223            _ => Err(StridedError::UnsupportedDType {
224                dtype: self.dtype.label(),
225            }),
226        })
227    }
228}
229
230/// Runtime reduction operation for dtype-erased full reductions.
231#[non_exhaustive]
232#[derive(Clone, Copy, Debug, Eq, PartialEq)]
233pub enum ReduceOp {
234    Sum,
235    Product,
236    /// Sum of same-dtype rounded squares.
237    ///
238    /// Each input is first multiplied by itself without FMA contraction, then
239    /// accumulated under the same association policy as [`Self::Sum`].
240    SumSquares,
241    /// NaN-propagating maximum for `f32`/`f64`, ordered maximum for
242    /// `i32`/`i64`.
243    ///
244    /// Any NaN operand makes the result the canonical NaN of the dtype. The
245    /// identity written for an empty reduction is `-inf` for floats and the
246    /// dtype minimum for integers. Complex and `bool` dtypes are rejected.
247    /// When the reduced values contain both `-0.0` and `+0.0`, which zero is
248    /// returned is unspecified.
249    Max,
250    /// NaN-propagating minimum for `f32`/`f64`, ordered minimum for
251    /// `i32`/`i64`.
252    ///
253    /// Any NaN operand makes the result the canonical NaN of the dtype. The
254    /// identity written for an empty reduction is `+inf` for floats and the
255    /// dtype maximum for integers. Complex and `bool` dtypes are rejected.
256    /// When the reduced values contain both `-0.0` and `+0.0`, which zero is
257    /// returned is unspecified.
258    Min,
259}
260
261/// Dtype-erased reduction wrapper.
262///
263/// This is the erased replay boundary for full-tensor scalar reductions and
264/// axis reductions with a fixed output layout. It supports only operations with
265/// an unambiguous identity value in the selected dtype.
266///
267/// # Evaluation order
268///
269/// The op is matched once per execution; every loop runs a kernel
270/// specialized for one `(dtype, op)` pair. Integer results are exact
271/// (wrapping) and [`ReduceOp::Max`] / [`ReduceOp::Min`] do not depend on
272/// order, so the rules below only affect the rounding of floating point and
273/// complex [`ReduceOp::Sum`], [`ReduceOp::Product`] and
274/// [`ReduceOp::SumSquares`]. Two details of float max and min follow the
275/// visit order: a NaN input makes the result NaN, but which NaN (payload and
276/// sign) is returned is unspecified, and when the extremum is a zero its sign
277/// is unspecified if both `+0.0` and `-0.0` occur.
278///
279/// * Full reductions (from [`Self::compile`], or [`Self::compile_axes`]
280///   with every axis reduced) visit the source in a layout derived order:
281///   extent one axes are dropped, the rest are ordered by increasing
282///   absolute stride and contiguous axes are fused into runs. Each run is
283///   reduced by the SIMD kernel (unit stride, `simd` feature), a sixteen
284///   lane kernel (other unit stride runs) or an eight lane kernel (strided
285///   runs), and runs are combined left to right. A dense column major
286///   source therefore reduces exactly like one contiguous slice. Exception:
287///   [`ReduceOp::Product`] is not reassociable, so each run is folded in
288///   source order (no SIMD or lane splitting) and one accumulator is carried
289///   across the runs of an output instead of combining per-run partials.
290/// * With a parallel context and more than `MINTHREADLENGTH` elements, a
291///   full reduction that may be reassociated splits the traversal into one
292///   deterministic range per worker thread. Each range runs the same run
293///   kernel as the serial path and the partials are combined in a fixed tree.
294///   The result is bitwise repeatable for a fixed layout and thread count, but
295///   may differ between thread counts and from the serial result. A
296///   non-reassociable [`ReduceOp::Product`] full reduction never splits, so it
297///   stays in source order for every context.
298/// * Axis reductions with kept axes compute every output independently, so
299///   the result never depends on the context or thread count. Each output
300///   folds its reduced elements sequentially in caller reduced axis order,
301///   except that a unit stride leading reduced run (after fusing adjacent
302///   reduced axes) of length at least 16 is reduced by the contiguous run
303///   kernel, with runs folded in order. A non-reassociable
304///   [`ReduceOp::Product`] folds each run in source order and carries one
305///   accumulator across the runs rather than multiplying per-run partials.
306///   When instead the leading kept axis
307///   has unit stride, adjacent outputs are accumulated as a block from one
308///   contiguous source run per reduced element; each output still folds in
309///   caller reduced axis order.
310#[derive(Clone, Debug)]
311pub struct ErasedReducePlan {
312    dtype: KernelDType,
313    op: ReduceOp,
314    layout: ReduceLayout,
315}
316
317#[derive(Clone, Debug)]
318enum ReduceLayout {
319    Full {
320        dims: Vec<usize>,
321        src_strides: Vec<isize>,
322        traversal: FullTraversal,
323    },
324    Axes {
325        src_dims: Vec<usize>,
326        src_strides: Vec<isize>,
327        dest_dims: Vec<usize>,
328        dest_strides: Vec<isize>,
329        outer_axes: Vec<ReduceOuterAxis>,
330        inner_axes: Vec<ReduceInnerAxis>,
331        dest_total: usize,
332        reduce_total: usize,
333        /// Set when every source axis is reduced; the plan then runs the
334        /// full-reduction traversal.
335        full: Option<FullTraversal>,
336    },
337}
338
339#[derive(Clone, Copy, Debug)]
340struct ReduceOuterAxis {
341    extent: usize,
342    source_step: isize,
343    source_reset: isize,
344    dest_step: isize,
345    dest_reset: isize,
346}
347#[derive(Clone, Copy, Debug)]
348struct ReduceInnerAxis {
349    extent: usize,
350    source_step: isize,
351    source_reset: isize,
352}
353impl ReduceLayout {
354    fn src_dims(&self) -> &[usize] {
355        match self {
356            Self::Full { dims, .. } => dims,
357            Self::Axes { src_dims, .. } => src_dims,
358        }
359    }
360
361    fn src_strides(&self) -> &[isize] {
362        match self {
363            Self::Full { src_strides, .. } | Self::Axes { src_strides, .. } => src_strides,
364        }
365    }
366
367    fn check_src_layout(&self, src: &ErasedRawStridedRef<'_>) -> Result<()> {
368        if src.dims() != self.src_dims() || src.strides() != self.src_strides() {
369            return Err(StridedError::PlanLayoutMismatch);
370        }
371        Ok(())
372    }
373}
374
375#[derive(Clone, Copy, Debug)]
376struct AxesLayout<'a> {
377    outer_axes: &'a [ReduceOuterAxis],
378    inner_axes: &'a [ReduceInnerAxis],
379    dest_total: usize,
380    reduce_total: usize,
381}
382
383impl ErasedReducePlan {
384    /// Validate and store a full-reduction plan for one dtype and source layout.
385    pub fn compile(
386        dtype: KernelDType,
387        op: ReduceOp,
388        dims: &[usize],
389        src_strides: &[isize],
390    ) -> Result<Self> {
391        check_reduce_op_dtype(dtype, op)?;
392        if dims.len() != src_strides.len() {
393            return Err(StridedError::StrideLengthMismatch);
394        }
395        checked_total_len(dims)?;
396        let traversal = FullTraversal::compile(dims, src_strides)?;
397        Ok(Self {
398            dtype,
399            op,
400            layout: ReduceLayout::Full {
401                dims: dims.to_vec(),
402                src_strides: src_strides.to_vec(),
403                traversal,
404            },
405        })
406    }
407
408    /// Validate and store an axis-reduction plan for one dtype and fixed source/output layouts.
409    ///
410    /// `axes` names the source axes reduced away. Output dimensions must be the
411    /// remaining source dimensions in source-axis order. When all axes are
412    /// reduced, any output layout with exactly one reachable element is accepted.
413    #[allow(clippy::too_many_arguments)]
414    pub fn compile_axes(
415        dtype: KernelDType,
416        op: ReduceOp,
417        src_dims: &[usize],
418        src_strides: &[isize],
419        dest_dims: &[usize],
420        dest_strides: &[isize],
421        axes: &[usize],
422    ) -> Result<Self> {
423        check_reduce_op_dtype(dtype, op)?;
424        if src_dims.len() != src_strides.len() || dest_dims.len() != dest_strides.len() {
425            return Err(StridedError::StrideLengthMismatch);
426        }
427        checked_total_len(src_dims)?;
428        check_reduce_layout_offset_arithmetic(src_dims, src_strides)?;
429        let dest_total = checked_total_len(dest_dims)?;
430        check_reduce_layout_offset_arithmetic(dest_dims, dest_strides)?;
431        if !crate::layout_check::is_injective_layout(dest_dims, dest_strides) {
432            return Err(StridedError::NonInjectiveOutputLayout);
433        }
434        validate_unique_axes(axes, src_dims.len())?;
435
436        let kept_axes: Vec<usize> = (0..src_dims.len())
437            .filter(|axis| !axes.contains(axis))
438            .collect();
439        let expected_dest_dims: Vec<usize> = kept_axes.iter().map(|&axis| src_dims[axis]).collect();
440        if expected_dest_dims.is_empty() {
441            if dest_total != 1 {
442                return Err(StridedError::ShapeMismatch(
443                    dest_dims.to_vec(),
444                    expected_dest_dims,
445                ));
446            }
447        } else if dest_dims != expected_dest_dims.as_slice() {
448            return Err(StridedError::ShapeMismatch(
449                dest_dims.to_vec(),
450                expected_dest_dims,
451            ));
452        }
453
454        let reduce_total = axes
455            .iter()
456            .try_fold(1usize, |total, &axis| total.checked_mul(src_dims[axis]))
457            .ok_or(StridedError::OffsetOverflow)?;
458        let outer_axes = compress_reduce_outer_axes(
459            kept_axes
460                .iter()
461                .enumerate()
462                .map(|(dest_axis, &src_axis)| {
463                    let extent = src_dims[src_axis];
464                    Ok(ReduceOuterAxis {
465                        extent,
466                        source_step: src_strides[src_axis],
467                        source_reset: checked_reduce_reset(extent, src_strides[src_axis])?,
468                        dest_step: dest_strides[dest_axis],
469                        dest_reset: checked_reduce_reset(extent, dest_strides[dest_axis])?,
470                    })
471                })
472                .collect::<Result<Vec<_>>>()?,
473        )?;
474        let inner_axes = compress_reduce_inner_axes(
475            axes.iter()
476                .map(|&src_axis| {
477                    let extent = src_dims[src_axis];
478                    Ok(ReduceInnerAxis {
479                        extent,
480                        source_step: src_strides[src_axis],
481                        source_reset: checked_reduce_reset(extent, src_strides[src_axis])?,
482                    })
483                })
484                .collect::<Result<Vec<_>>>()?,
485        )?;
486        let full = if kept_axes.is_empty() {
487            Some(FullTraversal::compile(src_dims, src_strides)?)
488        } else {
489            None
490        };
491        Ok(Self {
492            dtype,
493            op,
494            layout: ReduceLayout::Axes {
495                src_dims: src_dims.to_vec(),
496                src_strides: src_strides.to_vec(),
497                dest_dims: dest_dims.to_vec(),
498                dest_strides: dest_strides.to_vec(),
499                outer_axes,
500                inner_axes,
501                dest_total,
502                reduce_total,
503                full,
504            },
505        })
506    }
507
508    #[inline]
509    pub fn dtype(&self) -> KernelDType {
510        self.dtype
511    }
512
513    #[inline]
514    pub fn op(&self) -> ReduceOp {
515        self.op
516    }
517
518    /// Execute the reduction into an erased output descriptor.
519    pub fn execute(
520        &self,
521        ctx: &ExecContext,
522        dest: &mut ErasedRawStridedMut<'_>,
523        src: &ErasedRawStridedRef<'_>,
524    ) -> Result<()> {
525        check_dtype(self.dtype, dest.dtype())?;
526        check_dtype(self.dtype, src.dtype())?;
527        self.layout.check_src_layout(src)?;
528        match &self.layout {
529            ReduceLayout::Full { .. } => {
530                let dest_len = checked_total_len(dest.dims())?;
531                if dest_len != 1 {
532                    return Err(StridedError::RankMismatch(dest_len, 1));
533                }
534            }
535            ReduceLayout::Axes {
536                dest_dims,
537                dest_strides,
538                ..
539            } => {
540                if dest.dims() != dest_dims.as_slice() || dest.strides() != dest_strides.as_slice()
541                {
542                    return Err(StridedError::PlanLayoutMismatch);
543                }
544            }
545        }
546
547        let result = match self.dtype {
548            KernelDType::F32 => {
549                let mut writer = reduce_writer::<f32>(dest)?;
550                dispatch_reduce::<f32, _>(self.op, &self.layout, ctx, &mut writer, src)
551            }
552            KernelDType::F64 => {
553                let mut writer = reduce_writer::<f64>(dest)?;
554                dispatch_reduce::<f64, _>(self.op, &self.layout, ctx, &mut writer, src)
555            }
556            KernelDType::I32 => {
557                let mut writer = reduce_writer::<i32>(dest)?;
558                dispatch_reduce::<i32, _>(self.op, &self.layout, ctx, &mut writer, src)
559            }
560            KernelDType::I64 => {
561                let mut writer = reduce_writer::<i64>(dest)?;
562                dispatch_reduce::<i64, _>(self.op, &self.layout, ctx, &mut writer, src)
563            }
564            KernelDType::C32 => {
565                let mut writer = reduce_writer::<Complex32>(dest)?;
566                dispatch_reduce::<Complex32, _>(self.op, &self.layout, ctx, &mut writer, src)
567            }
568            KernelDType::C64 => {
569                let mut writer = reduce_writer::<Complex64>(dest)?;
570                dispatch_reduce::<Complex64, _>(self.op, &self.layout, ctx, &mut writer, src)
571            }
572            _ => Err(StridedError::UnsupportedDType {
573                dtype: self.dtype.label(),
574            }),
575        };
576        result
577    }
578
579    /// On success, every reachable destination slot is fully overwritten;
580    /// unreachable holes are neither read nor initialized. Validation errors
581    /// are returned before any destination write. A panic during execution
582    /// may leave a partially initialized `MaybeUninit` destination, which is
583    /// still safely droppable; no readable value is promised for unwritten
584    /// reachable slots.
585    pub fn execute_uninit(
586        &self,
587        ctx: &ExecContext,
588        dest: &mut ErasedRawStridedUninitMut<'_>,
589        src: &ErasedRawStridedPtr<'_>,
590    ) -> Result<()> {
591        check_dtype(self.dtype, dest.dtype())?;
592        check_dtype(self.dtype, src.dtype())?;
593        validate_uninit_no_overlap(dest, src, 0)?;
594        // SAFETY: the owning erased entry rejected all input/output overlap before conversion.
595        let src = unsafe { src.try_as_ref_after_no_overlap() }?;
596        self.layout.check_src_layout(&src)?;
597        match &self.layout {
598            ReduceLayout::Full { .. } => {
599                let total = checked_total_len(dest.dims())?;
600                if total != 1 {
601                    return Err(StridedError::RankMismatch(total, 1));
602                }
603            }
604            ReduceLayout::Axes {
605                dest_dims,
606                dest_strides,
607                ..
608            } => {
609                if dest.dims() != dest_dims.as_slice() || dest.strides() != dest_strides.as_slice()
610                {
611                    return Err(StridedError::PlanLayoutMismatch);
612                }
613            }
614        }
615        macro_rules! run {
616            ($ty:ty) => {{
617                let mut writer = reduce_uninit_writer::<$ty>(dest)?;
618                dispatch_reduce::<$ty, _>(self.op, &self.layout, ctx, &mut writer, &src)
619            }};
620        }
621        match self.dtype {
622            KernelDType::F32 => run!(f32),
623            KernelDType::F64 => run!(f64),
624            KernelDType::I32 => run!(i32),
625            KernelDType::I64 => run!(i64),
626            KernelDType::C32 => run!(Complex32),
627            KernelDType::C64 => run!(Complex64),
628            _ => Err(StridedError::UnsupportedDType {
629                dtype: self.dtype.label(),
630            }),
631        }
632    }
633}
634
635fn reduce_writer<'a, T>(dest: &'a mut ErasedRawStridedMut<'_>) -> Result<RawReduceWriter<'a, T>>
636where
637    T: KernelStorageElement,
638{
639    let offset = dest.offset();
640    let data = dest.data_as_mut::<T>()?;
641    let ptr = data.as_mut_ptr();
642    let extent = data.len();
643    Ok(RawReduceWriter {
644        ptr,
645        extent,
646        offset,
647        _marker: core::marker::PhantomData,
648    })
649}
650
651fn reduce_uninit_writer<'a, T>(
652    dest: &'a mut ErasedRawStridedUninitMut<'_>,
653) -> Result<RawReduceWriter<'a, T>>
654where
655    T: KernelStorageElement,
656{
657    let offset = dest.offset();
658    let data = dest.data_as_uninit_mut::<T>()?;
659    let ptr = data.as_mut_ptr().cast::<T>();
660    let extent = data.len();
661    Ok(RawReduceWriter {
662        ptr,
663        extent,
664        offset,
665        _marker: core::marker::PhantomData,
666    })
667}
668
669fn check_reduce_dtype(dtype: KernelDType) -> Result<()> {
670    match dtype {
671        KernelDType::F32
672        | KernelDType::F64
673        | KernelDType::I32
674        | KernelDType::I64
675        | KernelDType::C32
676        | KernelDType::C64 => Ok(()),
677        _ => Err(StridedError::UnsupportedDType {
678            dtype: dtype.label(),
679        }),
680    }
681}
682
683fn check_reduce_op_dtype(dtype: KernelDType, op: ReduceOp) -> Result<()> {
684    if op == ReduceOp::SumSquares && !matches!(dtype, KernelDType::F32 | KernelDType::F64) {
685        return Err(StridedError::UnsupportedDType {
686            dtype: dtype.label(),
687        });
688    }
689    if matches!(op, ReduceOp::Max | ReduceOp::Min)
690        && !matches!(
691            dtype,
692            KernelDType::F32 | KernelDType::F64 | KernelDType::I32 | KernelDType::I64
693        )
694    {
695        return Err(StridedError::UnsupportedDType {
696            dtype: dtype.label(),
697        });
698    }
699    check_reduce_dtype(dtype)
700}
701
702fn checked_total_len(dims: &[usize]) -> Result<usize> {
703    if dims.is_empty() {
704        return Ok(1);
705    }
706    dims.iter()
707        .try_fold(1usize, |acc, &dim| acc.checked_mul(dim))
708        .ok_or(StridedError::OffsetOverflow)
709}
710
711fn execute_copy<T>(
712    plan: &CopyPlan,
713    dest: &mut ErasedRawStridedMut<'_>,
714    src: &ErasedRawStridedRef<'_>,
715) -> Result<()>
716where
717    T: Copy + crate::MaybeSendSync + KernelStorageElement,
718{
719    let source_data = src.data_as::<T>()?;
720    let dest_dims = dest.dims();
721    let dest_strides = dest.strides();
722    let dest_offset = dest.offset();
723    let dest_data = dest.data_as_mut::<T>()?;
724    let source = unsafe {
725        RawStridedRef::new_unchecked(source_data, src.dims(), src.strides(), src.offset())
726    };
727    let mut dest =
728        unsafe { RawStridedMut::new_unchecked(dest_data, dest_dims, dest_strides, dest_offset) };
729    plan.execute(&mut dest, &source)
730}
731
732fn execute_concatenate<T>(
733    plan: &ConcatenatePlan,
734    dest: &mut ErasedRawStridedMut<'_>,
735    inputs: &[ErasedRawStridedRef<'_>],
736) -> Result<()>
737where
738    T: Copy + crate::MaybeSendSync + KernelStorageElement,
739{
740    if inputs.len() != plan.input_count() {
741        return Err(StridedError::RankMismatch(inputs.len(), plan.input_count()));
742    }
743    let dest_dims = dest.dims();
744    let dest_strides = dest.strides();
745    let dest_offset = dest.offset();
746    let dest_data = dest.data_as_mut::<T>()?;
747    let mut dest_ref =
748        unsafe { RawStridedMut::new_unchecked(dest_data, dest_dims, dest_strides, dest_offset) };
749    plan.check_dest_layout(&dest_ref)?;
750
751    for (position, input) in inputs.iter().enumerate() {
752        let input_data = input.data_as::<T>()?;
753        let input_ref = unsafe {
754            RawStridedRef::new_unchecked(input_data, input.dims(), input.strides(), input.offset())
755        };
756        plan.check_input_layout(position, &input_ref)?;
757        plan.segment_offset(position, dest_offset)?;
758    }
759    if plan.prefers_whole_plan() {
760        let input_refs = inputs
761            .iter()
762            .map(|input| {
763                let input_data = input.data_as::<T>()?;
764                Ok(unsafe {
765                    RawStridedRef::new_unchecked(
766                        input_data,
767                        input.dims(),
768                        input.strides(),
769                        input.offset(),
770                    )
771                })
772            })
773            .collect::<Result<Vec<_>>>()?;
774        // Splits the concatenated index space across and within segments.
775        return plan.execute(&mut dest_ref, &input_refs);
776    }
777    for (position, input) in inputs.iter().enumerate() {
778        let input_data = input.data_as::<T>()?;
779        let input_ref = unsafe {
780            RawStridedRef::new_unchecked(input_data, input.dims(), input.strides(), input.offset())
781        };
782        plan.execute_segment(position, &mut dest_ref, &input_ref)?;
783    }
784    Ok(())
785}
786
787fn execute_concatenate_uninit<T>(
788    plan: &ConcatenatePlan,
789    dest: &mut ErasedRawStridedUninitMut<'_>,
790    inputs: &[ErasedRawStridedPtr<'_>],
791) -> Result<()>
792where
793    T: Copy + crate::MaybeSendSync + KernelStorageElement,
794{
795    let dest_dims = dest.dims();
796    let dest_strides = dest.strides();
797    let dest_offset = dest.offset();
798    let dest_data = dest.data_as_uninit_mut::<T>()?;
799    let mut dest_ref =
800        unsafe { RawStridedMut::new_unchecked(dest_data, dest_dims, dest_strides, dest_offset) };
801    plan.check_dest_layout(&dest_ref)?;
802
803    for (position, input) in inputs.iter().enumerate() {
804        // SAFETY: input/output overlap was rejected before forming references.
805        let input = unsafe { input.try_as_ref_after_no_overlap() }?;
806        let input_data = input.data_as::<T>()?;
807        let input_ref = unsafe {
808            RawStridedRef::new_unchecked(input_data, input.dims(), input.strides(), input.offset())
809        };
810        plan.check_input_layout(position, &input_ref)?;
811        plan.segment_offset(position, dest_offset)?;
812    }
813    if plan.prefers_whole_plan() {
814        let inputs = inputs
815            .iter()
816            // SAFETY: input/output overlap was rejected before forming references.
817            .map(|input| unsafe { input.try_as_ref_after_no_overlap() })
818            .collect::<Result<Vec<_>>>()?;
819        let input_refs = inputs
820            .iter()
821            .map(|input| {
822                let input_data = input.data_as::<T>()?;
823                Ok(unsafe {
824                    RawStridedRef::new_unchecked(
825                        input_data,
826                        input.dims(),
827                        input.strides(),
828                        input.offset(),
829                    )
830                })
831            })
832            .collect::<Result<Vec<_>>>()?;
833        // Splits the concatenated index space across and within segments.
834        return plan.execute_uninit(&mut dest_ref, &input_refs);
835    }
836    for (position, input) in inputs.iter().enumerate() {
837        // SAFETY: input/output overlap was rejected before forming references.
838        let input = unsafe { input.try_as_ref_after_no_overlap() }?;
839        let input_data = input.data_as::<T>()?;
840        let input_ref = unsafe {
841            RawStridedRef::new_unchecked(input_data, input.dims(), input.strides(), input.offset())
842        };
843        plan.execute_segment_uninit(position, &mut dest_ref, &input_ref)?;
844    }
845    Ok(())
846}
847
848/// Whether a reduction context runs on the caller thread without entering
849/// the scheduler.
850fn reduce_context_is_serial(ctx: &ExecContext) -> bool {
851    ctx.is_serial()
852        || ctx
853            .max_threads_limit()
854            .is_some_and(|max_threads| max_threads.get() == 1)
855}
856
857fn execute_reduce_full<T, W, K>(
858    ctx: &ExecContext,
859    dest: &mut W,
860    src: &ErasedRawStridedRef<'_>,
861    traversal: &FullTraversal,
862) -> Result<()>
863where
864    T: ErasedReduceScalar,
865    W: ReduceWriter<T>,
866    K: ReduceKernel<T>,
867{
868    let source = src.data_as::<T>()?;
869    let total = traversal.total();
870    let value = if total == 0 {
871        K::identity()
872    } else if reduce_context_is_serial(ctx) {
873        // INVARIANT: (1) FullTraversal::compile checked every step/reset of
874        // the source layout; (2) the raw source descriptor validated every
875        // reachable offset; (3) execute checked exact plan-layout equality.
876        // SAFETY: the three-link invariant proves every traversal offset is
877        // readable, and `0..total` is non-empty.
878        unsafe { reduce_full_range::<T, K>(source.as_ptr(), src.offset(), traversal, 0..total)? }
879    } else {
880        ctx.run(|| reduce_full_policy::<T, K>(source, src.offset(), traversal))?
881    };
882
883    // SAFETY: validated rank-zero destination layout proves the offset.
884    unsafe { dest.write_at(dest.offset(), value) };
885    Ok(())
886}
887
888fn reduce_full_policy<T, K>(
889    source: &[T],
890    source_base: isize,
891    traversal: &FullTraversal,
892) -> Result<T>
893where
894    T: ErasedReduceScalar,
895    K: ReduceKernel<T>,
896{
897    let total = traversal.total();
898    #[cfg(feature = "parallel")]
899    {
900        // Splitting reassociates one output element's factors across worker
901        // ranges, so only a kernel that may be reassociated takes it
902        // (tenferro-rs#2150).
903        let nthreads = if K::reassociable() {
904            crate::threading::parallel_threads_for_len(total)
905        } else {
906            1
907        };
908        if nthreads > 1 {
909            let source_ptr = crate::threading::SendPtr(source.as_ptr() as *mut T);
910            return crate::threading::parallel_map_reduce(
911                0..total,
912                nthreads,
913                &|range| {
914                    // SAFETY: same three-link invariant as the serial call in
915                    // `execute_reduce_full`; each worker range is non-empty.
916                    unsafe {
917                        reduce_full_range::<T, K>(
918                            source_ptr.as_const(),
919                            source_base,
920                            traversal,
921                            range,
922                        )
923                    }
924                },
925                &|left, right| Ok(K::combine(left?, right?)),
926            );
927        }
928    }
929    // SAFETY: same three-link invariant as the serial call in
930    // `execute_reduce_full`; `0..total` is non-empty.
931    unsafe { reduce_full_range::<T, K>(source.as_ptr(), source_base, traversal, 0..total) }
932}
933
934fn dispatch_reduce<T, W>(
935    op: ReduceOp,
936    layout: &ReduceLayout,
937    ctx: &ExecContext,
938    dest: &mut W,
939    src: &ErasedRawStridedRef<'_>,
940) -> Result<()>
941where
942    T: ErasedReduceScalar,
943    W: ReduceWriter<T>,
944{
945    // The op is matched once per execution; every loop below is generic over
946    // the selected kernel type.
947    match op {
948        ReduceOp::Sum => dispatch_reduce_kernel::<T, W, SumKernel>(layout, ctx, dest, src),
949        ReduceOp::Product => dispatch_reduce_kernel::<T, W, ProductKernel>(layout, ctx, dest, src),
950        ReduceOp::SumSquares => {
951            dispatch_reduce_kernel::<T, W, SumSquaresKernel>(layout, ctx, dest, src)
952        }
953        ReduceOp::Max => dispatch_reduce_kernel::<T, W, MaxKernel>(layout, ctx, dest, src),
954        ReduceOp::Min => dispatch_reduce_kernel::<T, W, MinKernel>(layout, ctx, dest, src),
955    }
956}
957
958fn dispatch_reduce_kernel<T, W, K>(
959    layout: &ReduceLayout,
960    ctx: &ExecContext,
961    dest: &mut W,
962    src: &ErasedRawStridedRef<'_>,
963) -> Result<()>
964where
965    T: ErasedReduceScalar,
966    W: ReduceWriter<T>,
967    K: ReduceKernel<T>,
968{
969    match layout {
970        ReduceLayout::Full { traversal, .. } => {
971            execute_reduce_full::<T, W, K>(ctx, dest, src, traversal)
972        }
973        ReduceLayout::Axes {
974            outer_axes,
975            inner_axes,
976            dest_total,
977            reduce_total,
978            full,
979            ..
980        } => {
981            if let Some(traversal) = full {
982                return execute_reduce_full::<T, W, K>(ctx, dest, src, traversal);
983            }
984            execute_reduce_axes::<T, W, K>(
985                ctx,
986                dest,
987                src,
988                AxesLayout {
989                    outer_axes,
990                    inner_axes,
991                    dest_total: *dest_total,
992                    reduce_total: *reduce_total,
993                },
994            )
995        }
996    }
997}
998
999fn execute_reduce_axes<T, W, K>(
1000    ctx: &ExecContext,
1001    dest: &mut W,
1002    src: &ErasedRawStridedRef<'_>,
1003    layout: AxesLayout<'_>,
1004) -> Result<()>
1005where
1006    T: ErasedReduceScalar,
1007    W: ReduceWriter<T>,
1008    K: ReduceKernel<T>,
1009{
1010    if layout.dest_total == 0 {
1011        return Ok(());
1012    }
1013
1014    if layout.reduce_total == 0 {
1015        if ctx.is_serial() {
1016            execute_reduce_axes_identity_serial::<T, W, K>(dest, layout)
1017        } else {
1018            ctx.run(|| execute_reduce_axes_identity_policy::<T, W, K>(dest, layout))
1019        }
1020    } else if reduce_context_is_serial(ctx) {
1021        execute_reduce_axes_policy::<T, W, K>(dest, src, layout, false)
1022    } else {
1023        ctx.run(|| execute_reduce_axes_policy::<T, W, K>(dest, src, layout, true))
1024    }
1025}
1026
1027fn execute_reduce_axes_policy<T, W, K>(
1028    dest: &mut W,
1029    src: &ErasedRawStridedRef<'_>,
1030    layout: AxesLayout<'_>,
1031    allow_parallel: bool,
1032) -> Result<()>
1033where
1034    T: ErasedReduceScalar,
1035    W: ReduceWriter<T>,
1036    K: ReduceKernel<T>,
1037{
1038    let source_data = src.data_as::<T>()?;
1039    let source_base = src.offset();
1040    let dest_base = dest.offset();
1041    // SAFETY: the validated reduction writer owns the destination allocation.
1042    let dest_ptr = unsafe { dest.ptr() };
1043    #[cfg(feature = "parallel")]
1044    if allow_parallel {
1045        // The threshold counts source elements read (outputs times reduced
1046        // elements), as documented in docs/design/erased-execution-policy.md,
1047        // so a few thousand outputs with long reductions still split. Worker
1048        // ranges partition the independent outputs; with the output block
1049        // kernel this splits along the contiguous kept axis.
1050        let work = layout.dest_total.saturating_mul(layout.reduce_total);
1051        let nthreads = crate::threading::parallel_threads_for_len(work).min(layout.dest_total);
1052        if nthreads > 1 {
1053            let dest_ptr = crate::threading::SendPtr(dest_ptr);
1054            let source_ptr = crate::threading::SendPtr(source_data.as_ptr() as *mut T);
1055            return crate::threading::parallel_map_reduce(
1056                0..layout.dest_total,
1057                nthreads,
1058                &|range| {
1059                    let parts = AxesRange {
1060                        source: source_ptr.as_const(),
1061                        source_base,
1062                        dest: dest_ptr.as_ptr(),
1063                        dest_base,
1064                        outer_axes: layout.outer_axes,
1065                        inner_axes: layout.inner_axes,
1066                        reduce_total: layout.reduce_total,
1067                    };
1068                    // SAFETY: see `execute_reduce_axes_policy` serial call.
1069                    unsafe { reduce_axes_range::<T, K>(parts, range) }
1070                },
1071                &|left, right| left.and(right),
1072            );
1073        }
1074    }
1075    #[cfg(not(feature = "parallel"))]
1076    let _ = allow_parallel;
1077
1078    let parts = AxesRange {
1079        source: source_data.as_ptr(),
1080        source_base,
1081        dest: dest_ptr,
1082        dest_base,
1083        outer_axes: layout.outer_axes,
1084        inner_axes: layout.inner_axes,
1085        reduce_total: layout.reduce_total,
1086    };
1087    // INVARIANT: (1) compile_axes checked signed source/destination spans and
1088    // every cursor step/reset; (2) raw input and output descriptors validated
1089    // every reachable offset; (3) execute checked exact plan-layout equality
1090    // before dispatch. Output ranges are disjoint, and the initialized entry
1091    // holds distinct borrows while the uninit entry rejected overlap.
1092    // SAFETY: the three-link invariant above; `reduce_total` is nonzero.
1093    unsafe { reduce_axes_range::<T, K>(parts, 0..layout.dest_total) }
1094}
1095
1096fn execute_reduce_axes_identity_serial<T, W, K>(dest: &mut W, layout: AxesLayout<'_>) -> Result<()>
1097where
1098    T: ErasedReduceScalar,
1099    W: ReduceWriter<T>,
1100    K: ReduceKernel<T>,
1101{
1102    let mut outer = ReduceOuterCursor::decode(0, 0, dest.offset(), layout.outer_axes)?;
1103    for output in 0..layout.dest_total {
1104        // INVARIANT: (1) compile_axes checked the destination span and every
1105        // destination step/reset, including -(extent-1)*stride; (2) the raw
1106        // destination descriptor validated every reachable offset; (3) execute
1107        // checked exact plan-layout equality before dispatch.
1108        // SAFETY: the three-link layout invariant proves this destination
1109        // cursor offset is in bounds; the source pointer is intentionally never
1110        // formed for an empty reduction domain.
1111        unsafe { dest.write_at(outer.dest_offset, K::identity()) };
1112        if output + 1 < layout.dest_total {
1113            outer.advance();
1114        }
1115    }
1116    Ok(())
1117}
1118fn execute_reduce_axes_identity_policy<T, W, K>(dest: &mut W, layout: AxesLayout<'_>) -> Result<()>
1119where
1120    T: ErasedReduceScalar,
1121    W: ReduceWriter<T>,
1122    K: ReduceKernel<T>,
1123{
1124    #[cfg(feature = "parallel")]
1125    {
1126        let nthreads = crate::threading::parallel_threads_for_len(layout.dest_total);
1127        if nthreads > 1 {
1128            return execute_reduce_axes_identity_parallel::<T, W, K>(dest, layout, nthreads);
1129        }
1130    }
1131    execute_reduce_axes_identity_serial::<T, W, K>(dest, layout)
1132}
1133#[cfg(feature = "parallel")]
1134fn execute_reduce_axes_identity_parallel<T, W, K>(
1135    dest: &mut W,
1136    layout: AxesLayout<'_>,
1137    nthreads: usize,
1138) -> Result<()>
1139where
1140    T: ErasedReduceScalar,
1141    W: ReduceWriter<T>,
1142    K: ReduceKernel<T>,
1143{
1144    // SAFETY: the validated reduction writer owns the destination allocation.
1145    let dest_ptr = crate::threading::SendPtr(unsafe { dest.ptr() });
1146    let dest_offset_base = dest.offset();
1147    crate::threading::parallel_map_reduce(
1148        0..layout.dest_total,
1149        nthreads,
1150        &|range| {
1151            let range_end = range.end;
1152            let mut outer =
1153                ReduceOuterCursor::decode(range.start, 0, dest_offset_base, layout.outer_axes)?;
1154            let dest_ptr = dest_ptr.as_ptr();
1155            for output in range {
1156                // INVARIANT: (1) compile_axes checked the destination span and
1157                // every destination step/reset, including
1158                // -(extent-1)*stride; (2) the raw destination descriptor
1159                // validated every reachable pointer offset; (3) execute checked
1160                // exact plan-layout equality before dispatch.
1161                // SAFETY: the three-link layout invariant proves this
1162                // destination offset is in bounds; no source pointer is formed.
1163                unsafe { dest_ptr.offset(outer.dest_offset).write(K::identity()) };
1164                if output + 1 < range_end {
1165                    outer.advance();
1166                }
1167            }
1168            Ok(())
1169        },
1170        &|left, right| left.and(right),
1171    )
1172}
1173
1174trait ErasedReduceScalar:
1175    KernelStorageElement
1176    + Copy
1177    + One
1178    + Zero
1179    + crate::MaybeSendSync
1180    + crate::simd::MaybeSimdOps
1181    + crate::simd::MaybeSimdProduct
1182    + crate::simd::MaybeSimdSumSquares
1183    + crate::simd::MaybeSimdBlockFold
1184{
1185    fn reduce_sum(lhs: Self, rhs: Self) -> Self;
1186    fn reduce_product(lhs: Self, rhs: Self) -> Self;
1187    /// Identity of [`ReduceOp::Max`]; unreachable for dtypes the plan rejects.
1188    fn max_identity() -> Self;
1189    /// Identity of [`ReduceOp::Min`]; unreachable for dtypes the plan rejects.
1190    fn min_identity() -> Self;
1191    fn reduce_max(lhs: Self, rhs: Self) -> Self;
1192    fn reduce_min(lhs: Self, rhs: Self) -> Self;
1193}
1194
1195macro_rules! impl_float_erased_reduce_scalar {
1196    ($($ty:ty),* $(,)?) => {
1197        $(
1198            impl ErasedReduceScalar for $ty {
1199                #[inline(always)]
1200                fn reduce_sum(lhs: Self, rhs: Self) -> Self {
1201                    lhs + rhs
1202                }
1203
1204                #[inline(always)]
1205                fn reduce_product(lhs: Self, rhs: Self) -> Self {
1206                    lhs * rhs
1207                }
1208
1209                #[inline(always)]
1210                fn max_identity() -> Self {
1211                    <$ty>::NEG_INFINITY
1212                }
1213
1214                #[inline(always)]
1215                fn min_identity() -> Self {
1216                    <$ty>::INFINITY
1217                }
1218
1219                #[inline(always)]
1220                fn reduce_max(lhs: Self, rhs: Self) -> Self {
1221                    // Select form: both sides are evaluated and the NaN test
1222                    // picks the result, so lane loops lower to compare and
1223                    // blend instead of a data dependent branch.
1224                    // A NaN `lhs` is kept by the second test and a NaN
1225                    // `rhs` fails the comparison, so NaN propagates from
1226                    // either side. Equal values (including +0.0 and -0.0)
1227                    // return `rhs`.
1228                    if (lhs > rhs) | lhs.is_nan() {
1229                        lhs
1230                    } else {
1231                        rhs
1232                    }
1233                }
1234
1235                #[inline(always)]
1236                fn reduce_min(lhs: Self, rhs: Self) -> Self {
1237                    // Select form: both sides are evaluated and the NaN test
1238                    // picks the result, so lane loops lower to compare and
1239                    // blend instead of a data dependent branch.
1240                    // A NaN `lhs` is kept by the second test and a NaN
1241                    // `rhs` fails the comparison, so NaN propagates from
1242                    // either side. Equal values (including +0.0 and -0.0)
1243                    // return `rhs`.
1244                    if (lhs < rhs) | lhs.is_nan() {
1245                        lhs
1246                    } else {
1247                        rhs
1248                    }
1249                }
1250            }
1251        )*
1252    };
1253}
1254
1255macro_rules! impl_complex_erased_reduce_scalar {
1256    ($($ty:ty),* $(,)?) => {
1257        $(
1258            impl ErasedReduceScalar for $ty {
1259                #[inline(always)]
1260                fn reduce_sum(lhs: Self, rhs: Self) -> Self {
1261                    lhs + rhs
1262                }
1263
1264                #[inline(always)]
1265                fn reduce_product(lhs: Self, rhs: Self) -> Self {
1266                    lhs * rhs
1267                }
1268
1269                fn max_identity() -> Self {
1270                    // INVARIANT: check_reduce_op_dtype rejects complex Max at compile time.
1271                    unreachable!("complex max reduction is rejected at plan compile")
1272                }
1273
1274                fn min_identity() -> Self {
1275                    // INVARIANT: check_reduce_op_dtype rejects complex Min at compile time.
1276                    unreachable!("complex min reduction is rejected at plan compile")
1277                }
1278
1279                fn reduce_max(_lhs: Self, _rhs: Self) -> Self {
1280                    // INVARIANT: check_reduce_op_dtype rejects complex Max at compile time.
1281                    unreachable!("complex max reduction is rejected at plan compile")
1282                }
1283
1284                fn reduce_min(_lhs: Self, _rhs: Self) -> Self {
1285                    // INVARIANT: check_reduce_op_dtype rejects complex Min at compile time.
1286                    unreachable!("complex min reduction is rejected at plan compile")
1287                }
1288            }
1289        )*
1290    };
1291}
1292
1293macro_rules! impl_wrapping_erased_reduce_scalar {
1294    ($($ty:ty),* $(,)?) => {
1295        $(
1296            impl ErasedReduceScalar for $ty {
1297                #[inline(always)]
1298                fn reduce_sum(lhs: Self, rhs: Self) -> Self {
1299                    lhs.wrapping_add(rhs)
1300                }
1301
1302                #[inline(always)]
1303                fn reduce_product(lhs: Self, rhs: Self) -> Self {
1304                    lhs.wrapping_mul(rhs)
1305                }
1306
1307                #[inline(always)]
1308                fn max_identity() -> Self {
1309                    <$ty>::MIN
1310                }
1311
1312                #[inline(always)]
1313                fn min_identity() -> Self {
1314                    <$ty>::MAX
1315                }
1316
1317                #[inline(always)]
1318                fn reduce_max(lhs: Self, rhs: Self) -> Self {
1319                    lhs.max(rhs)
1320                }
1321
1322                #[inline(always)]
1323                fn reduce_min(lhs: Self, rhs: Self) -> Self {
1324                    lhs.min(rhs)
1325                }
1326            }
1327        )*
1328    };
1329}
1330
1331impl_float_erased_reduce_scalar!(f32, f64);
1332
1333impl_complex_erased_reduce_scalar!(Complex32, Complex64);
1334
1335impl_wrapping_erased_reduce_scalar!(i32, i64);
1336
1337fn validate_unique_axes(axes: &[usize], rank: usize) -> Result<()> {
1338    let mut seen = vec![false; rank];
1339    for &axis in axes {
1340        if axis >= rank {
1341            return Err(StridedError::InvalidAxis { axis, rank });
1342        }
1343        if seen[axis] {
1344            return Err(StridedError::InvalidAxis { axis, rank });
1345        }
1346        seen[axis] = true;
1347    }
1348    Ok(())
1349}
1350
1351struct ReduceOuterCursor<'a> {
1352    axes: &'a [ReduceOuterAxis],
1353    coords: CoordScratch,
1354    source_offset: isize,
1355    dest_offset: isize,
1356}
1357impl<'a> ReduceOuterCursor<'a> {
1358    fn decode(
1359        mut linear: usize,
1360        source_base: isize,
1361        dest_base: isize,
1362        axes: &'a [ReduceOuterAxis],
1363    ) -> Result<Self> {
1364        let mut coords = CoordScratch::new(axes.len());
1365        let mut source_offset = source_base;
1366        let mut dest_offset = dest_base;
1367        for (coord, axis) in coords.as_mut_slice().iter_mut().zip(axes) {
1368            // INVARIANT: a non-empty destination domain implies every outer
1369            // extent is nonzero before decode; compile-time span checks make
1370            // these one-time checked additions represent valid layout offsets.
1371            debug_assert!(axis.extent != 0);
1372            *coord = linear % axis.extent;
1373            linear /= axis.extent;
1374            source_offset = checked_offset_add(source_offset, axis.source_step, *coord)?;
1375            dest_offset = checked_offset_add(dest_offset, axis.dest_step, *coord)?;
1376        }
1377        Ok(Self {
1378            axes,
1379            coords,
1380            source_offset,
1381            dest_offset,
1382        })
1383    }
1384
1385    /// Coordinate along the first (fastest) compressed outer axis.
1386    #[inline]
1387    fn leading_coord(&self) -> usize {
1388        self.coords.first()
1389    }
1390
1391    #[inline]
1392    fn advance(&mut self) {
1393        // INVARIANT: compile_axes checked every signed step and reset delta;
1394        // descriptor validation plus exact layout equality proves each cursor
1395        // state is a reachable source/destination offset.
1396        for (coord, axis) in self.coords.as_mut_slice().iter_mut().zip(self.axes) {
1397            let next = *coord + 1;
1398            if next < axis.extent {
1399                *coord = next;
1400                self.source_offset += axis.source_step;
1401                self.dest_offset += axis.dest_step;
1402                return;
1403            }
1404            *coord = 0;
1405            self.source_offset += axis.source_reset;
1406            self.dest_offset += axis.dest_reset;
1407        }
1408    }
1409}
1410struct ReduceInnerCursor<'a> {
1411    axes: &'a [ReduceInnerAxis],
1412    coords: CoordScratch,
1413    source_offset: isize,
1414}
1415impl<'a> ReduceInnerCursor<'a> {
1416    fn new(source_base: isize, axes: &'a [ReduceInnerAxis]) -> Self {
1417        Self {
1418            axes,
1419            coords: CoordScratch::new(axes.len()),
1420            source_offset: source_base,
1421        }
1422    }
1423
1424    #[inline]
1425    fn reset(&mut self, source_base: isize) {
1426        self.coords.as_mut_slice().fill(0);
1427        self.source_offset = source_base;
1428    }
1429
1430    #[inline]
1431    fn advance(&mut self) {
1432        // INVARIANT: compile_axes checked every signed source step and reset
1433        // delta, and the validated descriptor/layout chain proves each value
1434        // offset is reachable from the current outer source base.
1435        for (coord, axis) in self.coords.as_mut_slice().iter_mut().zip(self.axes) {
1436            let next = *coord + 1;
1437            if next < axis.extent {
1438                *coord = next;
1439                self.source_offset += axis.source_step;
1440                return;
1441            }
1442            *coord = 0;
1443            self.source_offset += axis.source_reset;
1444        }
1445    }
1446}
1447fn check_reduce_layout_offset_arithmetic(dims: &[usize], strides: &[isize]) -> Result<()> {
1448    if dims.len() != strides.len() {
1449        return Err(StridedError::StrideLengthMismatch);
1450    }
1451    let mut min_offset = 0isize;
1452    let mut max_offset = 0isize;
1453    for (&dim, &stride) in dims.iter().zip(strides) {
1454        let last =
1455            isize::try_from(dim.saturating_sub(1)).map_err(|_| StridedError::OffsetOverflow)?;
1456        let extent = stride
1457            .checked_mul(last)
1458            .ok_or(StridedError::OffsetOverflow)?;
1459        if extent < 0 {
1460            min_offset = min_offset
1461                .checked_add(extent)
1462                .ok_or(StridedError::OffsetOverflow)?;
1463        } else {
1464            max_offset = max_offset
1465                .checked_add(extent)
1466                .ok_or(StridedError::OffsetOverflow)?;
1467        }
1468    }
1469    let _ = (min_offset, max_offset);
1470    Ok(())
1471}
1472fn compress_reduce_outer_axes(axes: Vec<ReduceOuterAxis>) -> Result<Vec<ReduceOuterAxis>> {
1473    let mut compressed: Vec<ReduceOuterAxis> = Vec::with_capacity(axes.len());
1474    for axis in axes {
1475        if let Some(previous) = compressed.last_mut() {
1476            let previous_extent =
1477                isize::try_from(previous.extent).map_err(|_| StridedError::OffsetOverflow)?;
1478            let expected_source = previous
1479                .source_step
1480                .checked_mul(previous_extent)
1481                .ok_or(StridedError::OffsetOverflow)?;
1482            let expected_dest = previous
1483                .dest_step
1484                .checked_mul(previous_extent)
1485                .ok_or(StridedError::OffsetOverflow)?;
1486            if axis.source_step == expected_source && axis.dest_step == expected_dest {
1487                let fused_extent = previous
1488                    .extent
1489                    .checked_mul(axis.extent)
1490                    .ok_or(StridedError::OffsetOverflow)?;
1491                previous.extent = fused_extent;
1492                previous.source_reset = checked_reduce_reset(fused_extent, previous.source_step)?;
1493                previous.dest_reset = checked_reduce_reset(fused_extent, previous.dest_step)?;
1494                continue;
1495            }
1496        }
1497        compressed.push(axis);
1498    }
1499    Ok(compressed)
1500}
1501fn compress_reduce_inner_axes(axes: Vec<ReduceInnerAxis>) -> Result<Vec<ReduceInnerAxis>> {
1502    let mut compressed: Vec<ReduceInnerAxis> = Vec::with_capacity(axes.len());
1503    for axis in axes {
1504        if let Some(previous) = compressed.last_mut() {
1505            let previous_extent =
1506                isize::try_from(previous.extent).map_err(|_| StridedError::OffsetOverflow)?;
1507            let expected_source = previous
1508                .source_step
1509                .checked_mul(previous_extent)
1510                .ok_or(StridedError::OffsetOverflow)?;
1511            if axis.source_step == expected_source {
1512                let fused_extent = previous
1513                    .extent
1514                    .checked_mul(axis.extent)
1515                    .ok_or(StridedError::OffsetOverflow)?;
1516                previous.extent = fused_extent;
1517                previous.source_reset = checked_reduce_reset(fused_extent, previous.source_step)?;
1518                continue;
1519            }
1520        }
1521        compressed.push(axis);
1522    }
1523    Ok(compressed)
1524}
1525fn checked_reduce_reset(extent: usize, stride: isize) -> Result<isize> {
1526    if extent == 0 {
1527        return Ok(0);
1528    }
1529    let last = isize::try_from(extent - 1).map_err(|_| StridedError::OffsetOverflow)?;
1530    stride
1531        .checked_mul(last)
1532        .and_then(isize::checked_neg)
1533        .ok_or(StridedError::OffsetOverflow)
1534}
1535fn checked_offset_add(base: isize, stride: isize, coord: usize) -> Result<isize> {
1536    let coord = isize::try_from(coord).map_err(|_| StridedError::OffsetOverflow)?;
1537    let scaled = stride
1538        .checked_mul(coord)
1539        .ok_or(StridedError::OffsetOverflow)?;
1540    base.checked_add(scaled).ok_or(StridedError::OffsetOverflow)
1541}
1542
1543struct CoordScratch {
1544    inline: [usize; RAW_FUSED_RANK_LIMIT],
1545    heap: Option<Vec<usize>>,
1546    len: usize,
1547}
1548
1549impl CoordScratch {
1550    fn new(len: usize) -> Self {
1551        if len <= RAW_FUSED_RANK_LIMIT {
1552            Self {
1553                inline: [0; RAW_FUSED_RANK_LIMIT],
1554                heap: None,
1555                len,
1556            }
1557        } else {
1558            Self {
1559                inline: [0; RAW_FUSED_RANK_LIMIT],
1560                heap: Some(vec![0; len]),
1561                len,
1562            }
1563        }
1564    }
1565
1566    #[inline]
1567    fn first(&self) -> usize {
1568        match &self.heap {
1569            Some(heap) => heap.first().copied().unwrap_or(0),
1570            None => self.inline[0],
1571        }
1572    }
1573
1574    fn as_mut_slice(&mut self) -> &mut [usize] {
1575        match &mut self.heap {
1576            Some(heap) => heap,
1577            None => &mut self.inline[..self.len],
1578        }
1579    }
1580}