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