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: crate::inline::InlineVec<usize>,
344        src_strides: crate::inline::InlineVec<isize>,
345        traversal: FullTraversal,
346    },
347    Axes {
348        src_dims: crate::inline::InlineVec<usize>,
349        src_strides: crate::inline::InlineVec<isize>,
350        dest_dims: crate::inline::InlineVec<usize>,
351        dest_strides: crate::inline::InlineVec<isize>,
352        outer_axes: crate::inline::InlineVec<ReduceOuterAxis>,
353        inner_axes: crate::inline::InlineVec<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, Default)]
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, Default)]
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.into(),
425                src_strides: src_strides.into(),
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: crate::inline::InlineVec<usize> = (0..src_dims.len())
460            .filter(|axis| !axes.contains(axis))
461            .collect();
462        let expected_dest_dims: crate::inline::InlineVec<usize> =
463            kept_axes.iter().map(|&axis| src_dims[axis]).collect();
464        if expected_dest_dims.is_empty() {
465            if dest_total != 1 {
466                return Err(StridedError::ShapeMismatch(
467                    dest_dims.to_vec(),
468                    expected_dest_dims.to_vec(),
469                ));
470            }
471        } else if dest_dims != expected_dest_dims.as_slice() {
472            return Err(StridedError::ShapeMismatch(
473                dest_dims.to_vec(),
474                expected_dest_dims.to_vec(),
475            ));
476        }
477
478        let reduce_total = axes
479            .iter()
480            .try_fold(1usize, |total, &axis| total.checked_mul(src_dims[axis]))
481            .ok_or(StridedError::OffsetOverflow)?;
482        let outer_axes = compress_reduce_outer_axes(
483            kept_axes
484                .iter()
485                .enumerate()
486                .map(|(dest_axis, &src_axis)| {
487                    let extent = src_dims[src_axis];
488                    Ok(ReduceOuterAxis {
489                        extent,
490                        source_step: src_strides[src_axis],
491                        source_reset: checked_reduce_reset(extent, src_strides[src_axis])?,
492                        dest_step: dest_strides[dest_axis],
493                        dest_reset: checked_reduce_reset(extent, dest_strides[dest_axis])?,
494                    })
495                })
496                .collect::<Result<crate::inline::InlineVec<_>>>()?,
497        )?;
498        let inner_axes = compress_reduce_inner_axes(
499            axes.iter()
500                .map(|&src_axis| {
501                    let extent = src_dims[src_axis];
502                    Ok(ReduceInnerAxis {
503                        extent,
504                        source_step: src_strides[src_axis],
505                        source_reset: checked_reduce_reset(extent, src_strides[src_axis])?,
506                    })
507                })
508                .collect::<Result<crate::inline::InlineVec<_>>>()?,
509        )?;
510        let full = if kept_axes.is_empty() {
511            Some(FullTraversal::compile(src_dims, src_strides)?)
512        } else {
513            None
514        };
515        Ok(Self {
516            dtype,
517            op,
518            layout: ReduceLayout::Axes {
519                src_dims: src_dims.into(),
520                src_strides: src_strides.into(),
521                dest_dims: dest_dims.into(),
522                dest_strides: dest_strides.into(),
523                outer_axes,
524                inner_axes,
525                dest_total,
526                reduce_total,
527                full,
528            },
529        })
530    }
531
532    #[inline]
533    pub fn dtype(&self) -> KernelDType {
534        self.dtype
535    }
536
537    #[inline]
538    pub fn op(&self) -> ReduceOp {
539        self.op
540    }
541
542    /// Execute the reduction into an erased output descriptor.
543    pub fn execute(
544        &self,
545        ctx: &ExecContext,
546        dest: &mut ErasedRawStridedMut<'_>,
547        src: &ErasedRawStridedRef<'_>,
548    ) -> Result<()> {
549        check_dtype(self.dtype, dest.dtype())?;
550        check_dtype(self.dtype, src.dtype())?;
551        self.layout.check_src_layout(src)?;
552        match &self.layout {
553            ReduceLayout::Full { .. } => {
554                let dest_len = checked_total_len(dest.dims())?;
555                if dest_len != 1 {
556                    return Err(StridedError::RankMismatch(dest_len, 1));
557                }
558            }
559            ReduceLayout::Axes {
560                dest_dims,
561                dest_strides,
562                ..
563            } => {
564                if dest.dims() != dest_dims.as_slice() || dest.strides() != dest_strides.as_slice()
565                {
566                    return Err(StridedError::PlanLayoutMismatch);
567                }
568            }
569        }
570
571        let result = match self.dtype {
572            KernelDType::F32 => {
573                let mut writer = reduce_writer::<f32>(dest)?;
574                dispatch_reduce::<f32, _>(self.op, &self.layout, ctx, &mut writer, src)
575            }
576            KernelDType::F64 => {
577                let mut writer = reduce_writer::<f64>(dest)?;
578                dispatch_reduce::<f64, _>(self.op, &self.layout, ctx, &mut writer, src)
579            }
580            KernelDType::I32 => {
581                let mut writer = reduce_writer::<i32>(dest)?;
582                dispatch_reduce::<i32, _>(self.op, &self.layout, ctx, &mut writer, src)
583            }
584            KernelDType::I64 => {
585                let mut writer = reduce_writer::<i64>(dest)?;
586                dispatch_reduce::<i64, _>(self.op, &self.layout, ctx, &mut writer, src)
587            }
588            KernelDType::C32 => {
589                let mut writer = reduce_writer::<Complex32>(dest)?;
590                dispatch_reduce::<Complex32, _>(self.op, &self.layout, ctx, &mut writer, src)
591            }
592            KernelDType::C64 => {
593                let mut writer = reduce_writer::<Complex64>(dest)?;
594                dispatch_reduce::<Complex64, _>(self.op, &self.layout, ctx, &mut writer, src)
595            }
596            _ => Err(StridedError::UnsupportedDType {
597                dtype: self.dtype.label(),
598            }),
599        };
600        result
601    }
602
603    /// On success, every reachable destination slot is fully overwritten;
604    /// unreachable holes are neither read nor initialized. Validation errors
605    /// are returned before any destination write. A panic during execution
606    /// may leave a partially initialized `MaybeUninit` destination, which is
607    /// still safely droppable; no readable value is promised for unwritten
608    /// reachable slots.
609    pub fn execute_uninit(
610        &self,
611        ctx: &ExecContext,
612        dest: &mut ErasedRawStridedUninitMut<'_>,
613        src: &ErasedRawStridedPtr<'_>,
614    ) -> Result<()> {
615        check_dtype(self.dtype, dest.dtype())?;
616        check_dtype(self.dtype, src.dtype())?;
617        validate_uninit_no_overlap(dest, src, 0)?;
618        // SAFETY: the owning erased entry rejected all input/output overlap before conversion.
619        let src = unsafe { src.try_as_ref_after_no_overlap() }?;
620        self.layout.check_src_layout(&src)?;
621        match &self.layout {
622            ReduceLayout::Full { .. } => {
623                let total = checked_total_len(dest.dims())?;
624                if total != 1 {
625                    return Err(StridedError::RankMismatch(total, 1));
626                }
627            }
628            ReduceLayout::Axes {
629                dest_dims,
630                dest_strides,
631                ..
632            } => {
633                if dest.dims() != dest_dims.as_slice() || dest.strides() != dest_strides.as_slice()
634                {
635                    return Err(StridedError::PlanLayoutMismatch);
636                }
637            }
638        }
639        macro_rules! run {
640            ($ty:ty) => {{
641                let mut writer = reduce_uninit_writer::<$ty>(dest)?;
642                dispatch_reduce::<$ty, _>(self.op, &self.layout, ctx, &mut writer, &src)
643            }};
644        }
645        match self.dtype {
646            KernelDType::F32 => run!(f32),
647            KernelDType::F64 => run!(f64),
648            KernelDType::I32 => run!(i32),
649            KernelDType::I64 => run!(i64),
650            KernelDType::C32 => run!(Complex32),
651            KernelDType::C64 => run!(Complex64),
652            _ => Err(StridedError::UnsupportedDType {
653                dtype: self.dtype.label(),
654            }),
655        }
656    }
657}
658
659fn reduce_writer<'a, T>(dest: &'a mut ErasedRawStridedMut<'_>) -> Result<RawReduceWriter<'a, T>>
660where
661    T: KernelStorageElement,
662{
663    let offset = dest.offset();
664    let data = dest.data_as_mut::<T>()?;
665    let ptr = data.as_mut_ptr();
666    let extent = data.len();
667    Ok(RawReduceWriter {
668        ptr,
669        extent,
670        offset,
671        _marker: core::marker::PhantomData,
672    })
673}
674
675fn reduce_uninit_writer<'a, T>(
676    dest: &'a mut ErasedRawStridedUninitMut<'_>,
677) -> Result<RawReduceWriter<'a, T>>
678where
679    T: KernelStorageElement,
680{
681    let offset = dest.offset();
682    let data = dest.data_as_uninit_mut::<T>()?;
683    let ptr = data.as_mut_ptr().cast::<T>();
684    let extent = data.len();
685    Ok(RawReduceWriter {
686        ptr,
687        extent,
688        offset,
689        _marker: core::marker::PhantomData,
690    })
691}
692
693fn check_reduce_dtype(dtype: KernelDType) -> Result<()> {
694    match dtype {
695        KernelDType::F32
696        | KernelDType::F64
697        | KernelDType::I32
698        | KernelDType::I64
699        | KernelDType::C32
700        | KernelDType::C64 => Ok(()),
701        _ => Err(StridedError::UnsupportedDType {
702            dtype: dtype.label(),
703        }),
704    }
705}
706
707fn check_reduce_op_dtype(dtype: KernelDType, op: ReduceOp) -> Result<()> {
708    if op == ReduceOp::SumSquares && !matches!(dtype, KernelDType::F32 | KernelDType::F64) {
709        return Err(StridedError::UnsupportedDType {
710            dtype: dtype.label(),
711        });
712    }
713    if matches!(op, ReduceOp::Max | ReduceOp::Min)
714        && !matches!(
715            dtype,
716            KernelDType::F32 | KernelDType::F64 | KernelDType::I32 | KernelDType::I64
717        )
718    {
719        return Err(StridedError::UnsupportedDType {
720            dtype: dtype.label(),
721        });
722    }
723    check_reduce_dtype(dtype)
724}
725
726fn checked_total_len(dims: &[usize]) -> Result<usize> {
727    if dims.is_empty() {
728        return Ok(1);
729    }
730    dims.iter()
731        .try_fold(1usize, |acc, &dim| acc.checked_mul(dim))
732        .ok_or(StridedError::OffsetOverflow)
733}
734
735fn execute_copy<T>(
736    plan: &CopyPlan,
737    dest: &mut ErasedRawStridedMut<'_>,
738    src: &ErasedRawStridedRef<'_>,
739) -> Result<()>
740where
741    T: Copy + crate::MaybeSendSync + KernelStorageElement,
742{
743    let source_data = src.data_as::<T>()?;
744    let dest_dims = dest.dims();
745    let dest_strides = dest.strides();
746    let dest_offset = dest.offset();
747    let dest_data = dest.data_as_mut::<T>()?;
748    let source = unsafe {
749        RawStridedRef::new_unchecked(source_data, src.dims(), src.strides(), src.offset())
750    };
751    let mut dest =
752        unsafe { RawStridedMut::new_unchecked(dest_data, dest_dims, dest_strides, dest_offset) };
753    plan.execute(&mut dest, &source)
754}
755
756fn execute_concatenate<T>(
757    plan: &ConcatenatePlan,
758    dest: &mut ErasedRawStridedMut<'_>,
759    inputs: &[ErasedRawStridedRef<'_>],
760) -> Result<()>
761where
762    T: Copy + crate::MaybeSendSync + KernelStorageElement,
763{
764    if inputs.len() != plan.input_count() {
765        return Err(StridedError::RankMismatch(inputs.len(), plan.input_count()));
766    }
767    let dest_dims = dest.dims();
768    let dest_strides = dest.strides();
769    let dest_offset = dest.offset();
770    let dest_data = dest.data_as_mut::<T>()?;
771    let mut dest_ref =
772        unsafe { RawStridedMut::new_unchecked(dest_data, dest_dims, dest_strides, dest_offset) };
773    plan.check_dest_layout(&dest_ref)?;
774
775    for (position, input) in inputs.iter().enumerate() {
776        let input_data = input.data_as::<T>()?;
777        let input_ref = unsafe {
778            RawStridedRef::new_unchecked(input_data, input.dims(), input.strides(), input.offset())
779        };
780        plan.check_input_layout(position, &input_ref)?;
781        plan.segment_offset(position, dest_offset)?;
782    }
783    if plan.prefers_whole_plan() && !inputs.is_empty() && inputs.len() <= crate::inline::OPERAND_CAP
784    {
785        // Build the typed descriptors on the stack: `RawStridedRef` is `Copy`,
786        // so this stays allocation-free within the operand bound. The
787        // whole-plan entry then splits the concatenated space and the typed
788        // `try_execute_parallel` reads each segment straight from the plan.
789        let first_data = inputs[0].data_as::<T>()?;
790        let first = unsafe {
791            RawStridedRef::new_unchecked(
792                first_data,
793                inputs[0].dims(),
794                inputs[0].strides(),
795                inputs[0].offset(),
796            )
797        };
798        let mut input_refs = [first; crate::inline::OPERAND_CAP];
799        for (slot, input) in input_refs.iter_mut().zip(inputs) {
800            let input_data = input.data_as::<T>()?;
801            *slot = unsafe {
802                RawStridedRef::new_unchecked(
803                    input_data,
804                    input.dims(),
805                    input.strides(),
806                    input.offset(),
807                )
808            };
809        }
810        return plan.execute(&mut dest_ref, &input_refs[..inputs.len()]);
811    }
812    for (position, input) in inputs.iter().enumerate() {
813        let input_data = input.data_as::<T>()?;
814        let input_ref = unsafe {
815            RawStridedRef::new_unchecked(input_data, input.dims(), input.strides(), input.offset())
816        };
817        plan.execute_segment(position, &mut dest_ref, &input_ref)?;
818    }
819    Ok(())
820}
821
822fn execute_concatenate_uninit<T>(
823    plan: &ConcatenatePlan,
824    dest: &mut ErasedRawStridedUninitMut<'_>,
825    inputs: &[ErasedRawStridedPtr<'_>],
826) -> Result<()>
827where
828    T: Copy + crate::MaybeSendSync + KernelStorageElement,
829{
830    let dest_dims = dest.dims();
831    let dest_strides = dest.strides();
832    let dest_offset = dest.offset();
833    let dest_data = dest.data_as_uninit_mut::<T>()?;
834    let mut dest_ref =
835        unsafe { RawStridedMut::new_unchecked(dest_data, dest_dims, dest_strides, dest_offset) };
836    plan.check_dest_layout(&dest_ref)?;
837
838    for (position, input) in inputs.iter().enumerate() {
839        // SAFETY: input/output overlap was rejected before forming references.
840        let input = unsafe { input.try_as_ref_after_no_overlap() }?;
841        let input_data = input.data_as::<T>()?;
842        let input_ref = unsafe {
843            RawStridedRef::new_unchecked(input_data, input.dims(), input.strides(), input.offset())
844        };
845        plan.check_input_layout(position, &input_ref)?;
846        plan.segment_offset(position, dest_offset)?;
847    }
848    if plan.prefers_whole_plan() && !inputs.is_empty() && inputs.len() <= crate::inline::OPERAND_CAP
849    {
850        // SAFETY: input/output overlap was rejected before forming references.
851        let first_erased = unsafe { inputs[0].try_as_ref_after_no_overlap() }?;
852        let mut erased_refs = [first_erased; crate::inline::OPERAND_CAP];
853        for (slot, input) in erased_refs.iter_mut().zip(inputs) {
854            // SAFETY: input/output overlap was rejected before forming references.
855            *slot = unsafe { input.try_as_ref_after_no_overlap() }?;
856        }
857        let first_data = erased_refs[0].data_as::<T>()?;
858        let first = unsafe {
859            RawStridedRef::new_unchecked(
860                first_data,
861                erased_refs[0].dims(),
862                erased_refs[0].strides(),
863                erased_refs[0].offset(),
864            )
865        };
866        let mut input_refs = [first; crate::inline::OPERAND_CAP];
867        for (slot, erased) in input_refs.iter_mut().zip(&erased_refs) {
868            let input_data = erased.data_as::<T>()?;
869            *slot = unsafe {
870                RawStridedRef::new_unchecked(
871                    input_data,
872                    erased.dims(),
873                    erased.strides(),
874                    erased.offset(),
875                )
876            };
877        }
878        return plan.execute_uninit(&mut dest_ref, &input_refs[..inputs.len()]);
879    }
880    for (position, input) in inputs.iter().enumerate() {
881        // SAFETY: input/output overlap was rejected before forming references.
882        let input = unsafe { input.try_as_ref_after_no_overlap() }?;
883        let input_data = input.data_as::<T>()?;
884        let input_ref = unsafe {
885            RawStridedRef::new_unchecked(input_data, input.dims(), input.strides(), input.offset())
886        };
887        plan.execute_segment_uninit(position, &mut dest_ref, &input_ref)?;
888    }
889    Ok(())
890}
891
892/// Runtime unary operation applied to the source of an [`ErasedUpdatePlan`].
893///
894/// The real dtypes have only [`Self::Identity`]; the complex dtypes also have
895/// [`Self::Conj`], matching the `ElementOp` choices a beta accumulator uses.
896#[non_exhaustive]
897#[derive(Clone, Copy, Debug, Eq, PartialEq)]
898pub enum UpdateOp {
899    /// `op(x) = x`.
900    Identity,
901    /// `op(x) = conj(x)`.
902    Conj,
903}
904
905/// Dtype-erased wrapper around [`UpdatePlan`].
906///
907/// The layout traversal is compiled once and stored without a scalar type;
908/// each [`execute`](Self::execute) names the concrete scalar type, resolves the
909/// runtime element op once per call, and then replays monomorphized loops.
910#[derive(Clone, Debug)]
911pub struct ErasedUpdatePlan {
912    dtype: KernelDType,
913    plan: UpdatePlan,
914}
915
916impl ErasedUpdatePlan {
917    /// Compile an update plan for one dtype and layout pair.
918    pub fn compile(
919        dtype: KernelDType,
920        dims: &[usize],
921        dst_strides: &[isize],
922        src_strides: &[isize],
923    ) -> Result<Self> {
924        check_static_indexing_dtype(dtype)?;
925        Ok(Self {
926            dtype,
927            plan: UpdatePlan::compile(dims, dst_strides, src_strides)?,
928        })
929    }
930
931    #[inline]
932    pub fn dtype(&self) -> KernelDType {
933        self.dtype
934    }
935
936    /// The compiled typed traversal.
937    #[inline]
938    pub fn plan(&self) -> &UpdatePlan {
939        &self.plan
940    }
941
942    /// `dest = alpha * op(src) + beta * dest` into an erased destination.
943    ///
944    /// `alpha`, `beta`, and `op` are resolved once per call; the layout check
945    /// and replay are the same allocation-free [`UpdatePlan`] path.
946    ///
947    /// # Examples
948    ///
949    /// ```
950    /// use strided_basic::{
951    ///     ErasedRawStridedMut, ErasedRawStridedRef, ErasedUpdatePlan, ExecContext, KernelDType,
952    ///     UpdateOp,
953    /// };
954    ///
955    /// let plan = ErasedUpdatePlan::compile(KernelDType::F64, &[2], &[1], &[1]).unwrap();
956    /// let src_data = [1.0_f64, 2.0];
957    /// let mut dst_data = [10.0_f64, 20.0];
958    /// let src = ErasedRawStridedRef::from_slice(&src_data, &[2], &[1], 0).unwrap();
959    /// let mut dst = ErasedRawStridedMut::from_slice_mut(&mut dst_data, &[2], &[1], 0).unwrap();
960    /// // dst = 2 * src + 3 * dst
961    /// plan.execute(&ExecContext::serial(), &mut dst, &src, 2.0, 3.0, UpdateOp::Identity)
962    ///     .unwrap();
963    /// drop(dst);
964    /// assert_eq!(dst_data, [32.0, 64.0]);
965    /// ```
966    pub fn execute<T>(
967        &self,
968        ctx: &ExecContext,
969        dest: &mut ErasedRawStridedMut<'_>,
970        src: &ErasedRawStridedRef<'_>,
971        alpha: T,
972        beta: T,
973        op: UpdateOp,
974    ) -> Result<()>
975    where
976        T: Copy
977            + crate::MaybeSendSync
978            + KernelStorageElement
979            + core::ops::Add<Output = T>
980            + core::ops::Mul<Output = T>
981            + Zero
982            + One
983            + PartialEq
984            + ElementOpApply,
985    {
986        check_dtype(self.dtype, dest.dtype())?;
987        check_dtype(self.dtype, src.dtype())?;
988        let source_data = src.data_as::<T>()?;
989        let dest_dims = dest.dims();
990        let dest_strides = dest.strides();
991        let dest_offset = dest.offset();
992        ctx.run(|| {
993            let dest_data = dest.data_as_mut::<T>()?;
994            let source = unsafe {
995                RawStridedRef::new_unchecked(source_data, src.dims(), src.strides(), src.offset())
996            };
997            let mut dest_ref = unsafe {
998                RawStridedMut::new_unchecked(dest_data, dest_dims, dest_strides, dest_offset)
999            };
1000            match op {
1001                UpdateOp::Identity => self
1002                    .plan
1003                    .execute(&mut dest_ref, &source, alpha, beta, |x| x),
1004                UpdateOp::Conj => self
1005                    .plan
1006                    .execute(&mut dest_ref, &source, alpha, beta, |x| x.conj()),
1007            }
1008        })
1009    }
1010
1011    /// `dest = alpha * op(src)` into an uninitialized erased destination.
1012    ///
1013    /// This is [`execute`](Self::execute) with `beta = 0`: the destination is
1014    /// never read, so a NaN-filled or uninitialized output cannot contaminate
1015    /// the result. On success every reachable destination element is written;
1016    /// validation errors are returned before any write. A panic during
1017    /// execution may leave a partially initialized `MaybeUninit` destination,
1018    /// which is still safely droppable; no readable value is promised for
1019    /// unwritten reachable slots.
1020    ///
1021    /// # Examples
1022    ///
1023    /// ```
1024    /// use core::mem::MaybeUninit;
1025    /// use strided_basic::{
1026    ///     ErasedRawStridedPtr, ErasedRawStridedRef, ErasedRawStridedUninitMut, ErasedUpdatePlan,
1027    ///     ExecContext, KernelDType, UpdateOp,
1028    /// };
1029    ///
1030    /// let plan = ErasedUpdatePlan::compile(KernelDType::F64, &[2], &[1], &[1]).unwrap();
1031    /// let src_data = [1.0_f64, 2.0];
1032    /// let mut dst_data = [MaybeUninit::<f64>::uninit(); 2];
1033    /// let src = ErasedRawStridedRef::from_slice(&src_data, &[2], &[1], 0).unwrap();
1034    /// let src_ptr = ErasedRawStridedPtr::from_ref(&src);
1035    /// let mut dst =
1036    ///     ErasedRawStridedUninitMut::from_uninit_slice(&mut dst_data, &[2], &[1], 0).unwrap();
1037    /// // dst = 2 * src
1038    /// plan.execute_uninit(&ExecContext::serial(), &mut dst, &src_ptr, 2.0, UpdateOp::Identity)
1039    ///     .unwrap();
1040    /// let values: Vec<f64> = dst_data.iter().map(|v| unsafe { v.assume_init() }).collect();
1041    /// assert_eq!(values, [2.0, 4.0]);
1042    /// ```
1043    ///
1044    /// # Errors
1045    ///
1046    /// Returns `DTypeMismatch` or `PlanLayoutMismatch` when a descriptor does
1047    /// not match the plan, and `OverlappingInputOutput` when the source
1048    /// overlaps the destination allocation, all before any destination write.
1049    pub fn execute_uninit<T>(
1050        &self,
1051        ctx: &ExecContext,
1052        dest: &mut ErasedRawStridedUninitMut<'_>,
1053        src: &ErasedRawStridedPtr<'_>,
1054        alpha: T,
1055        op: UpdateOp,
1056    ) -> Result<()>
1057    where
1058        T: Copy
1059            + crate::MaybeSendSync
1060            + KernelStorageElement
1061            + core::ops::Mul<Output = T>
1062            + One
1063            + PartialEq
1064            + ElementOpApply,
1065    {
1066        check_dtype(self.dtype, dest.dtype())?;
1067        check_dtype(self.dtype, src.dtype())?;
1068        validate_uninit_no_overlap(dest, src, 0)?;
1069        // SAFETY: the owning erased entry rejected all input/output overlap before conversion.
1070        let src = unsafe { src.try_as_ref_after_no_overlap() }?;
1071        let source_data = src.data_as::<T>()?;
1072        let dest_dims = dest.dims();
1073        let dest_strides = dest.strides();
1074        let dest_offset = dest.offset();
1075        ctx.run(|| {
1076            let dest_data = dest.data_as_uninit_mut::<T>()?;
1077            let source = unsafe {
1078                RawStridedRef::new_unchecked(source_data, src.dims(), src.strides(), src.offset())
1079            };
1080            let mut dest_ref = unsafe {
1081                RawStridedMut::new_unchecked(dest_data, dest_dims, dest_strides, dest_offset)
1082            };
1083            match op {
1084                UpdateOp::Identity => {
1085                    self.plan
1086                        .execute_uninit(&mut dest_ref, &source, alpha, |x| x)
1087                }
1088                UpdateOp::Conj => self
1089                    .plan
1090                    .execute_uninit(&mut dest_ref, &source, alpha, |x| x.conj()),
1091            }
1092        })
1093    }
1094}
1095
1096/// Whether a reduction context runs on the caller thread without entering
1097/// the scheduler.
1098fn reduce_context_is_serial(ctx: &ExecContext) -> bool {
1099    ctx.is_serial()
1100        || ctx
1101            .max_threads_limit()
1102            .is_some_and(|max_threads| max_threads.get() == 1)
1103}
1104
1105fn execute_reduce_full<T, W, K>(
1106    ctx: &ExecContext,
1107    dest: &mut W,
1108    src: &ErasedRawStridedRef<'_>,
1109    traversal: &FullTraversal,
1110) -> Result<bool>
1111where
1112    T: ErasedReduceScalar,
1113    W: ReduceWriter<T>,
1114    K: ReduceKernel<T>,
1115{
1116    let source = src.data_as::<T>()?;
1117    let total = traversal.total();
1118    if total == 0 {
1119        // SAFETY: validated rank-zero destination layout proves the offset.
1120        unsafe { dest.write_at(dest.offset(), K::finish(K::identity())) };
1121        return Ok(false);
1122    }
1123    let acc = if reduce_context_is_serial(ctx) {
1124        // INVARIANT: (1) FullTraversal::compile checked every step/reset of
1125        // the source layout; (2) the raw source descriptor validated every
1126        // reachable offset; (3) execute checked exact plan-layout equality.
1127        // SAFETY: the three-link invariant proves every traversal offset is
1128        // readable.
1129        unsafe { reduce_full_range::<T, K>(source.as_ptr(), src.offset(), traversal, 0..total)? }
1130    } else {
1131        ctx.run(|| reduce_full_policy::<T, K>(source, src.offset(), traversal))?
1132    };
1133
1134    if K::needs_ordered_fallback(&acc) {
1135        // The caller re-runs this layout with `K::Fallback`; the destination is
1136        // left untouched so nothing observable is written twice.
1137        return Ok(true);
1138    }
1139    // SAFETY: validated rank-zero destination layout proves the offset.
1140    unsafe { dest.write_at(dest.offset(), K::finish(acc)) };
1141    Ok(false)
1142}
1143
1144fn reduce_full_policy<T, K>(
1145    source: &[T],
1146    source_base: isize,
1147    traversal: &FullTraversal,
1148) -> Result<K::Acc>
1149where
1150    T: ErasedReduceScalar,
1151    K: ReduceKernel<T>,
1152{
1153    let total = traversal.total();
1154    #[cfg(feature = "parallel")]
1155    {
1156        // Splitting reassociates one output element's factors across worker
1157        // ranges, so only a kernel that may be reassociated takes it
1158        // (tenferro-rs#2150). A stable accumulator that detects an
1159        // unrepresentable factor sets the fallback flag, and the caller then
1160        // recomputes the whole output in source order.
1161        let nthreads = if K::reassociable() {
1162            crate::threading::parallel_threads_for_len(total)
1163        } else {
1164            1
1165        };
1166        if nthreads > 1 {
1167            let source_ptr = crate::threading::SendPtr(source.as_ptr() as *mut T);
1168            return crate::threading::parallel_map_reduce(
1169                0..total,
1170                nthreads,
1171                &|range| {
1172                    // SAFETY: same three-link invariant as the serial call in
1173                    // `execute_reduce_full`; each worker range is non-empty.
1174                    unsafe {
1175                        reduce_full_range::<T, K>(
1176                            source_ptr.as_const(),
1177                            source_base,
1178                            traversal,
1179                            range,
1180                        )
1181                    }
1182                },
1183                &|left, right| Ok(K::combine(left?, right?)),
1184            );
1185        }
1186    }
1187    // SAFETY: same three-link invariant as the serial call in
1188    // `execute_reduce_full`.
1189    unsafe { reduce_full_range::<T, K>(source.as_ptr(), source_base, traversal, 0..total) }
1190}
1191
1192fn dispatch_reduce<T, W>(
1193    op: ReduceOp,
1194    layout: &ReduceLayout,
1195    ctx: &ExecContext,
1196    dest: &mut W,
1197    src: &ErasedRawStridedRef<'_>,
1198) -> Result<()>
1199where
1200    T: ErasedReduceScalar,
1201    W: ReduceWriter<T>,
1202{
1203    // The op is matched once per execution; every loop below is generic over
1204    // the selected kernel type.
1205    match op {
1206        ReduceOp::Sum => dispatch_reduce_kernel::<T, W, SumKernel>(layout, ctx, dest, src),
1207        ReduceOp::Product => {
1208            dispatch_reduce_kernel::<T, W, ReassociatedProductKernel>(layout, ctx, dest, src)
1209        }
1210        ReduceOp::StableProduct => {
1211            dispatch_reduce_kernel::<T, W, StableProductKernel>(layout, ctx, dest, src)
1212        }
1213        ReduceOp::SumSquares => {
1214            dispatch_reduce_kernel::<T, W, SumSquaresKernel>(layout, ctx, dest, src)
1215        }
1216        ReduceOp::Max => dispatch_reduce_kernel::<T, W, MaxKernel>(layout, ctx, dest, src),
1217        ReduceOp::Min => dispatch_reduce_kernel::<T, W, MinKernel>(layout, ctx, dest, src),
1218    }
1219}
1220
1221fn dispatch_reduce_kernel<T, W, K>(
1222    layout: &ReduceLayout,
1223    ctx: &ExecContext,
1224    dest: &mut W,
1225    src: &ErasedRawStridedRef<'_>,
1226) -> Result<()>
1227where
1228    T: ErasedReduceScalar,
1229    W: ReduceWriter<T>,
1230    K: ReduceKernel<T>,
1231{
1232    let fallback = match layout {
1233        ReduceLayout::Full { traversal, .. } => {
1234            execute_reduce_full::<T, W, K>(ctx, dest, src, traversal)?
1235        }
1236        ReduceLayout::Axes {
1237            outer_axes,
1238            inner_axes,
1239            dest_total,
1240            reduce_total,
1241            full,
1242            ..
1243        } => {
1244            let axes = AxesLayout {
1245                outer_axes,
1246                inner_axes,
1247                dest_total: *dest_total,
1248                reduce_total: *reduce_total,
1249            };
1250            match full {
1251                Some(traversal) => execute_reduce_full::<T, W, K>(ctx, dest, src, traversal)?,
1252                None => {
1253                    // An axis reduction falls back per output inside
1254                    // `reduce_axes_range`; it never asks for a layout-wide
1255                    // recompute.
1256                    execute_reduce_axes::<T, W, K>(ctx, dest, src, axes)?;
1257                    false
1258                }
1259            }
1260        }
1261    };
1262    if fallback {
1263        // Only a full reduction (one output) requests the layout-wide
1264        // recompute; `K::Fallback` must not itself request a fallback, so this
1265        // recurses at most once. Scalar kernels use `Fallback = Self` and never
1266        // set the flag, so the branch is unreachable for them.
1267        return dispatch_reduce_kernel::<T, W, K::Fallback>(layout, ctx, dest, src);
1268    }
1269    Ok(())
1270}
1271
1272fn execute_reduce_axes<T, W, K>(
1273    ctx: &ExecContext,
1274    dest: &mut W,
1275    src: &ErasedRawStridedRef<'_>,
1276    layout: AxesLayout<'_>,
1277) -> Result<()>
1278where
1279    T: ErasedReduceScalar,
1280    W: ReduceWriter<T>,
1281    K: ReduceKernel<T>,
1282{
1283    if layout.dest_total == 0 {
1284        return Ok(());
1285    }
1286
1287    if layout.reduce_total == 0 {
1288        if ctx.is_serial() {
1289            execute_reduce_axes_identity_serial::<T, W, K>(dest, layout)
1290        } else {
1291            ctx.run(|| execute_reduce_axes_identity_policy::<T, W, K>(dest, layout))
1292        }
1293    } else if reduce_context_is_serial(ctx) {
1294        execute_reduce_axes_policy::<T, W, K>(dest, src, layout, false)
1295    } else {
1296        ctx.run(|| execute_reduce_axes_policy::<T, W, K>(dest, src, layout, true))
1297    }
1298}
1299
1300fn execute_reduce_axes_policy<T, W, K>(
1301    dest: &mut W,
1302    src: &ErasedRawStridedRef<'_>,
1303    layout: AxesLayout<'_>,
1304    allow_parallel: bool,
1305) -> Result<()>
1306where
1307    T: ErasedReduceScalar,
1308    W: ReduceWriter<T>,
1309    K: ReduceKernel<T>,
1310{
1311    let source_data = src.data_as::<T>()?;
1312    let source_base = src.offset();
1313    let dest_base = dest.offset();
1314    // SAFETY: the validated reduction writer owns the destination allocation.
1315    let dest_ptr = unsafe { dest.ptr() };
1316    #[cfg(feature = "parallel")]
1317    if allow_parallel {
1318        // The threshold counts source elements read (outputs times reduced
1319        // elements), as documented in docs/design/erased-execution-policy.md,
1320        // so a few thousand outputs with long reductions still split. Worker
1321        // ranges partition the independent outputs; with the output block
1322        // kernel this splits along the contiguous kept axis.
1323        let work = layout.dest_total.saturating_mul(layout.reduce_total);
1324        let nthreads = crate::threading::parallel_threads_for_len(work).min(layout.dest_total);
1325        if nthreads > 1 {
1326            let dest_ptr = crate::threading::SendPtr(dest_ptr);
1327            let source_ptr = crate::threading::SendPtr(source_data.as_ptr() as *mut T);
1328            return crate::threading::parallel_map_reduce(
1329                0..layout.dest_total,
1330                nthreads,
1331                &|range| {
1332                    let parts = AxesRange {
1333                        source: source_ptr.as_const(),
1334                        source_base,
1335                        dest: dest_ptr.as_ptr(),
1336                        dest_base,
1337                        outer_axes: layout.outer_axes,
1338                        inner_axes: layout.inner_axes,
1339                        reduce_total: layout.reduce_total,
1340                    };
1341                    // SAFETY: see `execute_reduce_axes_policy` serial call.
1342                    unsafe { reduce_axes_range::<T, K>(parts, range) }
1343                },
1344                &|left, right| left.and(right),
1345            );
1346        }
1347    }
1348    #[cfg(not(feature = "parallel"))]
1349    let _ = allow_parallel;
1350
1351    let parts = AxesRange {
1352        source: source_data.as_ptr(),
1353        source_base,
1354        dest: dest_ptr,
1355        dest_base,
1356        outer_axes: layout.outer_axes,
1357        inner_axes: layout.inner_axes,
1358        reduce_total: layout.reduce_total,
1359    };
1360    // INVARIANT: (1) compile_axes checked signed source/destination spans and
1361    // every cursor step/reset; (2) raw input and output descriptors validated
1362    // every reachable offset; (3) execute checked exact plan-layout equality
1363    // before dispatch. Output ranges are disjoint, and the initialized entry
1364    // holds distinct borrows while the uninit entry rejected overlap.
1365    // SAFETY: the three-link invariant above; `reduce_total` is nonzero.
1366    unsafe { reduce_axes_range::<T, K>(parts, 0..layout.dest_total) }
1367}
1368
1369fn execute_reduce_axes_identity_serial<T, W, K>(dest: &mut W, layout: AxesLayout<'_>) -> Result<()>
1370where
1371    T: ErasedReduceScalar,
1372    W: ReduceWriter<T>,
1373    K: ReduceKernel<T>,
1374{
1375    let mut outer = ReduceOuterCursor::decode(0, 0, dest.offset(), layout.outer_axes)?;
1376    for output in 0..layout.dest_total {
1377        // INVARIANT: (1) compile_axes checked the destination span and every
1378        // destination step/reset, including -(extent-1)*stride; (2) the raw
1379        // destination descriptor validated every reachable offset; (3) execute
1380        // checked exact plan-layout equality before dispatch.
1381        // SAFETY: the three-link layout invariant proves this destination
1382        // cursor offset is in bounds; the source pointer is intentionally never
1383        // formed for an empty reduction domain.
1384        unsafe { dest.write_at(outer.dest_offset, K::finish(K::identity())) };
1385        if output + 1 < layout.dest_total {
1386            outer.advance();
1387        }
1388    }
1389    Ok(())
1390}
1391fn execute_reduce_axes_identity_policy<T, W, K>(dest: &mut W, layout: AxesLayout<'_>) -> Result<()>
1392where
1393    T: ErasedReduceScalar,
1394    W: ReduceWriter<T>,
1395    K: ReduceKernel<T>,
1396{
1397    #[cfg(feature = "parallel")]
1398    {
1399        let nthreads = crate::threading::parallel_threads_for_len(layout.dest_total);
1400        if nthreads > 1 {
1401            return execute_reduce_axes_identity_parallel::<T, W, K>(dest, layout, nthreads);
1402        }
1403    }
1404    execute_reduce_axes_identity_serial::<T, W, K>(dest, layout)
1405}
1406#[cfg(feature = "parallel")]
1407fn execute_reduce_axes_identity_parallel<T, W, K>(
1408    dest: &mut W,
1409    layout: AxesLayout<'_>,
1410    nthreads: usize,
1411) -> Result<()>
1412where
1413    T: ErasedReduceScalar,
1414    W: ReduceWriter<T>,
1415    K: ReduceKernel<T>,
1416{
1417    // SAFETY: the validated reduction writer owns the destination allocation.
1418    let dest_ptr = crate::threading::SendPtr(unsafe { dest.ptr() });
1419    let dest_offset_base = dest.offset();
1420    crate::threading::parallel_map_reduce(
1421        0..layout.dest_total,
1422        nthreads,
1423        &|range| {
1424            let range_end = range.end;
1425            let mut outer =
1426                ReduceOuterCursor::decode(range.start, 0, dest_offset_base, layout.outer_axes)?;
1427            let dest_ptr = dest_ptr.as_ptr();
1428            for output in range {
1429                // INVARIANT: (1) compile_axes checked the destination span and
1430                // every destination step/reset, including
1431                // -(extent-1)*stride; (2) the raw destination descriptor
1432                // validated every reachable pointer offset; (3) execute checked
1433                // exact plan-layout equality before dispatch.
1434                // SAFETY: the three-link layout invariant proves this
1435                // destination offset is in bounds; no source pointer is formed.
1436                unsafe {
1437                    dest_ptr
1438                        .offset(outer.dest_offset)
1439                        .write(K::finish(K::identity()))
1440                };
1441                if output + 1 < range_end {
1442                    outer.advance();
1443                }
1444            }
1445            Ok(())
1446        },
1447        &|left, right| left.and(right),
1448    )
1449}
1450
1451trait ErasedReduceScalar:
1452    KernelStorageElement
1453    + Copy
1454    + One
1455    + Zero
1456    + crate::MaybeSendSync
1457    + crate::simd::MaybeSimdOps
1458    + crate::simd::MaybeSimdProduct
1459    + crate::simd::MaybeSimdSumSquares
1460    + crate::simd::MaybeSimdBlockFold
1461    + StableProductScalar
1462{
1463    fn reduce_sum(lhs: Self, rhs: Self) -> Self;
1464    fn reduce_product(lhs: Self, rhs: Self) -> Self;
1465    /// Identity of [`ReduceOp::Max`]; unreachable for dtypes the plan rejects.
1466    fn max_identity() -> Self;
1467    /// Identity of [`ReduceOp::Min`]; unreachable for dtypes the plan rejects.
1468    fn min_identity() -> Self;
1469    fn reduce_max(lhs: Self, rhs: Self) -> Self;
1470    fn reduce_min(lhs: Self, rhs: Self) -> Self;
1471}
1472
1473macro_rules! impl_float_erased_reduce_scalar {
1474    ($($ty:ty),* $(,)?) => {
1475        $(
1476            impl ErasedReduceScalar for $ty {
1477                #[inline(always)]
1478                fn reduce_sum(lhs: Self, rhs: Self) -> Self {
1479                    lhs + rhs
1480                }
1481
1482                #[inline(always)]
1483                fn reduce_product(lhs: Self, rhs: Self) -> Self {
1484                    lhs * rhs
1485                }
1486
1487                #[inline(always)]
1488                fn max_identity() -> Self {
1489                    <$ty>::NEG_INFINITY
1490                }
1491
1492                #[inline(always)]
1493                fn min_identity() -> Self {
1494                    <$ty>::INFINITY
1495                }
1496
1497                #[inline(always)]
1498                fn reduce_max(lhs: Self, rhs: Self) -> Self {
1499                    // Select form: both sides are evaluated and the NaN test
1500                    // picks the result, so lane loops lower to compare and
1501                    // blend instead of a data dependent branch.
1502                    // A NaN `lhs` is kept by the second test and a NaN
1503                    // `rhs` fails the comparison, so NaN propagates from
1504                    // either side. Equal values (including +0.0 and -0.0)
1505                    // return `rhs`.
1506                    if (lhs > rhs) | lhs.is_nan() {
1507                        lhs
1508                    } else {
1509                        rhs
1510                    }
1511                }
1512
1513                #[inline(always)]
1514                fn reduce_min(lhs: Self, rhs: Self) -> Self {
1515                    // Select form: both sides are evaluated and the NaN test
1516                    // picks the result, so lane loops lower to compare and
1517                    // blend instead of a data dependent branch.
1518                    // A NaN `lhs` is kept by the second test and a NaN
1519                    // `rhs` fails the comparison, so NaN propagates from
1520                    // either side. Equal values (including +0.0 and -0.0)
1521                    // return `rhs`.
1522                    if (lhs < rhs) | lhs.is_nan() {
1523                        lhs
1524                    } else {
1525                        rhs
1526                    }
1527                }
1528            }
1529        )*
1530    };
1531}
1532
1533macro_rules! impl_complex_erased_reduce_scalar {
1534    ($($ty:ty),* $(,)?) => {
1535        $(
1536            impl ErasedReduceScalar for $ty {
1537                #[inline(always)]
1538                fn reduce_sum(lhs: Self, rhs: Self) -> Self {
1539                    lhs + rhs
1540                }
1541
1542                #[inline(always)]
1543                fn reduce_product(lhs: Self, rhs: Self) -> Self {
1544                    lhs * rhs
1545                }
1546
1547                fn max_identity() -> Self {
1548                    // INVARIANT: check_reduce_op_dtype rejects complex Max at compile time.
1549                    unreachable!("complex max reduction is rejected at plan compile")
1550                }
1551
1552                fn min_identity() -> Self {
1553                    // INVARIANT: check_reduce_op_dtype rejects complex Min at compile time.
1554                    unreachable!("complex min reduction is rejected at plan compile")
1555                }
1556
1557                fn reduce_max(_lhs: Self, _rhs: Self) -> Self {
1558                    // INVARIANT: check_reduce_op_dtype rejects complex Max at compile time.
1559                    unreachable!("complex max reduction is rejected at plan compile")
1560                }
1561
1562                fn reduce_min(_lhs: Self, _rhs: Self) -> Self {
1563                    // INVARIANT: check_reduce_op_dtype rejects complex Min at compile time.
1564                    unreachable!("complex min reduction is rejected at plan compile")
1565                }
1566            }
1567        )*
1568    };
1569}
1570
1571macro_rules! impl_wrapping_erased_reduce_scalar {
1572    ($($ty:ty),* $(,)?) => {
1573        $(
1574            impl ErasedReduceScalar for $ty {
1575                #[inline(always)]
1576                fn reduce_sum(lhs: Self, rhs: Self) -> Self {
1577                    lhs.wrapping_add(rhs)
1578                }
1579
1580                #[inline(always)]
1581                fn reduce_product(lhs: Self, rhs: Self) -> Self {
1582                    lhs.wrapping_mul(rhs)
1583                }
1584
1585                #[inline(always)]
1586                fn max_identity() -> Self {
1587                    <$ty>::MIN
1588                }
1589
1590                #[inline(always)]
1591                fn min_identity() -> Self {
1592                    <$ty>::MAX
1593                }
1594
1595                #[inline(always)]
1596                fn reduce_max(lhs: Self, rhs: Self) -> Self {
1597                    lhs.max(rhs)
1598                }
1599
1600                #[inline(always)]
1601                fn reduce_min(lhs: Self, rhs: Self) -> Self {
1602                    lhs.min(rhs)
1603                }
1604            }
1605        )*
1606    };
1607}
1608
1609impl_float_erased_reduce_scalar!(f32, f64);
1610
1611impl_complex_erased_reduce_scalar!(Complex32, Complex64);
1612
1613impl_wrapping_erased_reduce_scalar!(i32, i64);
1614
1615fn validate_unique_axes(axes: &[usize], rank: usize) -> Result<()> {
1616    // Inline for the rank bound (P4); above it the spill may allocate.
1617    let mut inline = [false; strided_view::INLINE_RANK];
1618    let mut spill: Vec<bool> = Vec::new();
1619    let seen: &mut [bool] = if rank <= strided_view::INLINE_RANK {
1620        &mut inline[..rank]
1621    } else {
1622        spill.resize(rank, false);
1623        &mut spill
1624    };
1625    for &axis in axes {
1626        if axis >= rank {
1627            return Err(StridedError::InvalidAxis { axis, rank });
1628        }
1629        if seen[axis] {
1630            return Err(StridedError::InvalidAxis { axis, rank });
1631        }
1632        seen[axis] = true;
1633    }
1634    Ok(())
1635}
1636
1637struct ReduceOuterCursor<'a> {
1638    axes: &'a [ReduceOuterAxis],
1639    coords: CoordScratch,
1640    source_offset: isize,
1641    dest_offset: isize,
1642}
1643impl<'a> ReduceOuterCursor<'a> {
1644    fn decode(
1645        mut linear: usize,
1646        source_base: isize,
1647        dest_base: isize,
1648        axes: &'a [ReduceOuterAxis],
1649    ) -> Result<Self> {
1650        let mut coords = CoordScratch::new(axes.len());
1651        let mut source_offset = source_base;
1652        let mut dest_offset = dest_base;
1653        for (coord, axis) in coords.as_mut_slice().iter_mut().zip(axes) {
1654            // INVARIANT: a non-empty destination domain implies every outer
1655            // extent is nonzero before decode; compile-time span checks make
1656            // these one-time checked additions represent valid layout offsets.
1657            debug_assert!(axis.extent != 0);
1658            *coord = linear % axis.extent;
1659            linear /= axis.extent;
1660            source_offset = checked_offset_add(source_offset, axis.source_step, *coord)?;
1661            dest_offset = checked_offset_add(dest_offset, axis.dest_step, *coord)?;
1662        }
1663        Ok(Self {
1664            axes,
1665            coords,
1666            source_offset,
1667            dest_offset,
1668        })
1669    }
1670
1671    /// Coordinate along the first (fastest) compressed outer axis.
1672    #[inline]
1673    fn leading_coord(&self) -> usize {
1674        self.coords.first()
1675    }
1676
1677    #[inline]
1678    fn advance(&mut self) {
1679        // INVARIANT: compile_axes checked every signed step and reset delta;
1680        // descriptor validation plus exact layout equality proves each cursor
1681        // state is a reachable source/destination offset.
1682        for (coord, axis) in self.coords.as_mut_slice().iter_mut().zip(self.axes) {
1683            let next = *coord + 1;
1684            if next < axis.extent {
1685                *coord = next;
1686                self.source_offset += axis.source_step;
1687                self.dest_offset += axis.dest_step;
1688                return;
1689            }
1690            *coord = 0;
1691            self.source_offset += axis.source_reset;
1692            self.dest_offset += axis.dest_reset;
1693        }
1694    }
1695}
1696struct ReduceInnerCursor<'a> {
1697    axes: &'a [ReduceInnerAxis],
1698    coords: CoordScratch,
1699    source_offset: isize,
1700}
1701impl<'a> ReduceInnerCursor<'a> {
1702    fn new(source_base: isize, axes: &'a [ReduceInnerAxis]) -> Self {
1703        Self {
1704            axes,
1705            coords: CoordScratch::new(axes.len()),
1706            source_offset: source_base,
1707        }
1708    }
1709
1710    #[inline]
1711    fn reset(&mut self, source_base: isize) {
1712        self.coords.as_mut_slice().fill(0);
1713        self.source_offset = source_base;
1714    }
1715
1716    #[inline]
1717    fn advance(&mut self) {
1718        // INVARIANT: compile_axes checked every signed source step and reset
1719        // delta, and the validated descriptor/layout chain proves each value
1720        // offset is reachable from the current outer source base.
1721        for (coord, axis) in self.coords.as_mut_slice().iter_mut().zip(self.axes) {
1722            let next = *coord + 1;
1723            if next < axis.extent {
1724                *coord = next;
1725                self.source_offset += axis.source_step;
1726                return;
1727            }
1728            *coord = 0;
1729            self.source_offset += axis.source_reset;
1730        }
1731    }
1732}
1733fn check_reduce_layout_offset_arithmetic(dims: &[usize], strides: &[isize]) -> Result<()> {
1734    if dims.len() != strides.len() {
1735        return Err(StridedError::StrideLengthMismatch);
1736    }
1737    let mut min_offset = 0isize;
1738    let mut max_offset = 0isize;
1739    for (&dim, &stride) in dims.iter().zip(strides) {
1740        let last =
1741            isize::try_from(dim.saturating_sub(1)).map_err(|_| StridedError::OffsetOverflow)?;
1742        let extent = stride
1743            .checked_mul(last)
1744            .ok_or(StridedError::OffsetOverflow)?;
1745        if extent < 0 {
1746            min_offset = min_offset
1747                .checked_add(extent)
1748                .ok_or(StridedError::OffsetOverflow)?;
1749        } else {
1750            max_offset = max_offset
1751                .checked_add(extent)
1752                .ok_or(StridedError::OffsetOverflow)?;
1753        }
1754    }
1755    let _ = (min_offset, max_offset);
1756    Ok(())
1757}
1758fn compress_reduce_outer_axes(
1759    axes: crate::inline::InlineVec<ReduceOuterAxis>,
1760) -> Result<crate::inline::InlineVec<ReduceOuterAxis>> {
1761    let mut compressed: crate::inline::InlineVec<ReduceOuterAxis> =
1762        crate::inline::InlineVec::with_capacity(axes.len());
1763    for axis in axes {
1764        if let Some(previous) = compressed.last_mut() {
1765            let previous_extent =
1766                isize::try_from(previous.extent).map_err(|_| StridedError::OffsetOverflow)?;
1767            let expected_source = previous
1768                .source_step
1769                .checked_mul(previous_extent)
1770                .ok_or(StridedError::OffsetOverflow)?;
1771            let expected_dest = previous
1772                .dest_step
1773                .checked_mul(previous_extent)
1774                .ok_or(StridedError::OffsetOverflow)?;
1775            if axis.source_step == expected_source && axis.dest_step == expected_dest {
1776                let fused_extent = previous
1777                    .extent
1778                    .checked_mul(axis.extent)
1779                    .ok_or(StridedError::OffsetOverflow)?;
1780                previous.extent = fused_extent;
1781                previous.source_reset = checked_reduce_reset(fused_extent, previous.source_step)?;
1782                previous.dest_reset = checked_reduce_reset(fused_extent, previous.dest_step)?;
1783                continue;
1784            }
1785        }
1786        compressed.push(axis);
1787    }
1788    Ok(compressed)
1789}
1790fn compress_reduce_inner_axes(
1791    axes: crate::inline::InlineVec<ReduceInnerAxis>,
1792) -> Result<crate::inline::InlineVec<ReduceInnerAxis>> {
1793    let mut compressed: crate::inline::InlineVec<ReduceInnerAxis> =
1794        crate::inline::InlineVec::with_capacity(axes.len());
1795    for axis in axes {
1796        if let Some(previous) = compressed.last_mut() {
1797            let previous_extent =
1798                isize::try_from(previous.extent).map_err(|_| StridedError::OffsetOverflow)?;
1799            let expected_source = previous
1800                .source_step
1801                .checked_mul(previous_extent)
1802                .ok_or(StridedError::OffsetOverflow)?;
1803            if axis.source_step == expected_source {
1804                let fused_extent = previous
1805                    .extent
1806                    .checked_mul(axis.extent)
1807                    .ok_or(StridedError::OffsetOverflow)?;
1808                previous.extent = fused_extent;
1809                previous.source_reset = checked_reduce_reset(fused_extent, previous.source_step)?;
1810                continue;
1811            }
1812        }
1813        compressed.push(axis);
1814    }
1815    Ok(compressed)
1816}
1817fn checked_reduce_reset(extent: usize, stride: isize) -> Result<isize> {
1818    if extent == 0 {
1819        return Ok(0);
1820    }
1821    let last = isize::try_from(extent - 1).map_err(|_| StridedError::OffsetOverflow)?;
1822    stride
1823        .checked_mul(last)
1824        .and_then(isize::checked_neg)
1825        .ok_or(StridedError::OffsetOverflow)
1826}
1827fn checked_offset_add(base: isize, stride: isize, coord: usize) -> Result<isize> {
1828    let coord = isize::try_from(coord).map_err(|_| StridedError::OffsetOverflow)?;
1829    let scaled = stride
1830        .checked_mul(coord)
1831        .ok_or(StridedError::OffsetOverflow)?;
1832    base.checked_add(scaled).ok_or(StridedError::OffsetOverflow)
1833}
1834
1835struct CoordScratch {
1836    inline: [usize; RAW_FUSED_RANK_LIMIT],
1837    heap: Option<Vec<usize>>,
1838    len: usize,
1839}
1840
1841impl CoordScratch {
1842    fn new(len: usize) -> Self {
1843        if len <= RAW_FUSED_RANK_LIMIT {
1844            Self {
1845                inline: [0; RAW_FUSED_RANK_LIMIT],
1846                heap: None,
1847                len,
1848            }
1849        } else {
1850            Self {
1851                inline: [0; RAW_FUSED_RANK_LIMIT],
1852                heap: Some(vec![0; len]),
1853                len,
1854            }
1855        }
1856    }
1857
1858    #[inline]
1859    fn first(&self) -> usize {
1860        match &self.heap {
1861            Some(heap) => heap.first().copied().unwrap_or(0),
1862            None => self.inline[0],
1863        }
1864    }
1865
1866    fn as_mut_slice(&mut self) -> &mut [usize] {
1867        match &mut self.heap {
1868            Some(heap) => heap,
1869            None => &mut self.inline[..self.len],
1870        }
1871    }
1872}