Skip to main content

tenferro_cpu/
indexing.rs

1use std::ops::Add;
2
3use num_traits::Zero;
4
5use super::indexing_alloc::pooled_uninit_tensor;
6use super::typed_host_data;
7use crate::buffer_pool::{BufferPool, PoolScalar};
8use tenferro_tensor::{GatherConfig, PadConfig, ScatterConfig, SliceConfig};
9use tenferro_tensor::{Tensor, TypedTensor};
10
11// Indexing-family kernels stay as dedicated sequential loops for now. Their
12// per-output gather/scatter/slice/pad/concatenate/reverse index transforms do
13// not currently map to a strided-kernel or backend-native parallel primitive.
14// Backend entrypoints still run these loops inside CpuContext::install, so a
15// future parallel implementation can use the same CPU threading policy.
16
17trait TensorAsTyped<T> {
18    fn as_typed(&self) -> Option<&TypedTensor<T>>;
19}
20
21macro_rules! impl_tensor_as_typed {
22    ($(($ty:ty, $variant:ident)),+ $(,)?) => {
23        $(
24            impl TensorAsTyped<$ty> for Tensor {
25                fn as_typed(&self) -> Option<&TypedTensor<$ty>> {
26                    match self {
27                        Tensor::$variant(tensor) => Some(tensor),
28                        _ => None,
29                    }
30                }
31            }
32        )+
33    };
34}
35
36impl_tensor_as_typed!(
37    (f32, F32),
38    (f64, F64),
39    (i32, I32),
40    (i64, I64),
41    (bool, Bool),
42    (num_complex::Complex<f32>, C32),
43    (num_complex::Complex<f64>, C64),
44);
45
46macro_rules! dispatch_tensor_unary_result {
47    ($input:expr, |$tensor:ident| $body:expr) => {
48        match $input {
49            Tensor::F32($tensor) => Ok(Tensor::F32($body?)),
50            Tensor::F64($tensor) => Ok(Tensor::F64($body?)),
51            Tensor::I32($tensor) => Ok(Tensor::I32($body?)),
52            Tensor::I64($tensor) => Ok(Tensor::I64($body?)),
53            Tensor::Bool($tensor) => Ok(Tensor::Bool($body?)),
54            Tensor::C32($tensor) => Ok(Tensor::C32($body?)),
55            Tensor::C64($tensor) => Ok(Tensor::C64($body?)),
56        }
57    };
58}
59
60macro_rules! dispatch_tensor_unary_with_bool_special_result {
61    ($input:expr, |$tensor:ident| $body:expr, bool |$bool_tensor:ident| $bool_body:expr) => {
62        match $input {
63            Tensor::F32($tensor) => Ok(Tensor::F32($body?)),
64            Tensor::F64($tensor) => Ok(Tensor::F64($body?)),
65            Tensor::I32($tensor) => Ok(Tensor::I32($body?)),
66            Tensor::I64($tensor) => Ok(Tensor::I64($body?)),
67            Tensor::Bool($bool_tensor) => Ok(Tensor::Bool($bool_body?)),
68            Tensor::C32($tensor) => Ok(Tensor::C32($body?)),
69            Tensor::C64($tensor) => Ok(Tensor::C64($body?)),
70        }
71    };
72}
73
74macro_rules! dispatch_same_dtype_result {
75    ($op:literal, $lhs:expr, $rhs:expr, |$lhs_t:ident, $rhs_t:ident| $body:expr) => {
76        match ($lhs, $rhs) {
77            (Tensor::F32($lhs_t), Tensor::F32($rhs_t)) => Ok(Tensor::F32($body?)),
78            (Tensor::F64($lhs_t), Tensor::F64($rhs_t)) => Ok(Tensor::F64($body?)),
79            (Tensor::I32($lhs_t), Tensor::I32($rhs_t)) => Ok(Tensor::I32($body?)),
80            (Tensor::I64($lhs_t), Tensor::I64($rhs_t)) => Ok(Tensor::I64($body?)),
81            (Tensor::Bool($lhs_t), Tensor::Bool($rhs_t)) => Ok(Tensor::Bool($body?)),
82            (Tensor::C32($lhs_t), Tensor::C32($rhs_t)) => Ok(Tensor::C32($body?)),
83            (Tensor::C64($lhs_t), Tensor::C64($rhs_t)) => Ok(Tensor::C64($body?)),
84            _ => Err(crate::Error::DTypeMismatch {
85                op: $op,
86                lhs: $lhs.dtype(),
87                rhs: $rhs.dtype(),
88            }),
89        }
90    };
91}
92
93macro_rules! dispatch_same_dtype_without_bool_result {
94    ($op:literal, $lhs:expr, $rhs:expr, $bool_message:literal, |$lhs_t:ident, $rhs_t:ident| $body:expr) => {
95        match ($lhs, $rhs) {
96            (Tensor::F32($lhs_t), Tensor::F32($rhs_t)) => Ok(Tensor::F32($body?)),
97            (Tensor::F64($lhs_t), Tensor::F64($rhs_t)) => Ok(Tensor::F64($body?)),
98            (Tensor::I32($lhs_t), Tensor::I32($rhs_t)) => Ok(Tensor::I32($body?)),
99            (Tensor::I64($lhs_t), Tensor::I64($rhs_t)) => Ok(Tensor::I64($body?)),
100            (Tensor::C32($lhs_t), Tensor::C32($rhs_t)) => Ok(Tensor::C32($body?)),
101            (Tensor::C64($lhs_t), Tensor::C64($rhs_t)) => Ok(Tensor::C64($body?)),
102            (Tensor::Bool(_), Tensor::Bool(_)) => {
103                Err(crate::Error::backend_failure($op, $bool_message))
104            }
105            _ => Err(crate::Error::DTypeMismatch {
106                op: $op,
107                lhs: $lhs.dtype(),
108                rhs: $rhs.dtype(),
109            }),
110        }
111    };
112}
113
114fn with_local_pool<T>(f: impl FnOnce(&mut BufferPool) -> T) -> T {
115    let mut buffers = BufferPool::new();
116    f(&mut buffers)
117}
118
119fn advance_col_major_index(index: &mut [usize], shape: &[usize]) {
120    debug_assert_eq!(index.len(), shape.len());
121    for axis in 0..index.len() {
122        if shape[axis] == 0 {
123            index[axis] = 0;
124            continue;
125        }
126        index[axis] += 1;
127        if index[axis] < shape[axis] {
128            break;
129        }
130        index[axis] = 0;
131    }
132}
133
134fn pooled_filled_tensor<T>(
135    buffers: &mut BufferPool,
136    shape: Vec<usize>,
137    fill: T,
138) -> crate::Result<TypedTensor<T>>
139where
140    T: Copy + Clone + PoolScalar,
141{
142    // SAFETY: the following fill writes every pooled output element.
143    let mut out = pooled_uninit_tensor(buffers, shape)?;
144    out.host_data_mut()?.fill(fill);
145    Ok(out)
146}
147
148fn clone_host_tensor_from_pool<T>(
149    buffers: &mut BufferPool,
150    op: &'static str,
151    tensor: &TypedTensor<T>,
152) -> crate::Result<TypedTensor<T>>
153where
154    T: Copy + Clone + PoolScalar,
155{
156    // SAFETY: copy_from_slice writes every pooled output element.
157    let mut out = pooled_uninit_tensor(buffers, tensor.shape().to_vec())?;
158    out.host_data_mut()?
159        .copy_from_slice(typed_host_data(op, tensor)?);
160    Ok(out)
161}
162
163pub fn gather(
164    operand: &Tensor,
165    start_indices: &Tensor,
166    config: &GatherConfig,
167) -> crate::Result<Tensor> {
168    with_local_pool(|buffers| gather_with_pool(buffers, operand, start_indices, config))
169}
170
171pub(crate) fn gather_with_pool(
172    buffers: &mut BufferPool,
173    operand: &Tensor,
174    start_indices: &Tensor,
175    config: &GatherConfig,
176) -> crate::Result<Tensor> {
177    let start_indices = try_index_tensor(start_indices)?;
178    dispatch_tensor_unary_result!(operand, |t| typed_gather(
179        buffers,
180        t,
181        &start_indices,
182        config
183    ))
184}
185
186pub fn scatter(
187    operand: &Tensor,
188    scatter_indices: &Tensor,
189    updates: &Tensor,
190    config: &ScatterConfig,
191) -> crate::Result<Tensor> {
192    with_local_pool(|buffers| scatter_with_pool(buffers, operand, scatter_indices, updates, config))
193}
194
195pub(crate) fn scatter_with_pool(
196    buffers: &mut BufferPool,
197    operand: &Tensor,
198    scatter_indices: &Tensor,
199    updates: &Tensor,
200    config: &ScatterConfig,
201) -> crate::Result<Tensor> {
202    let scatter_indices = try_index_tensor(scatter_indices)?;
203    dispatch_same_dtype_without_bool_result!(
204        "scatter",
205        operand,
206        updates,
207        "Bool data tensors are not supported by additive scatter",
208        |op, upd| typed_scatter(buffers, op, &scatter_indices, upd, config)
209    )
210}
211
212pub(crate) fn try_slice_with_pool(
213    buffers: &mut BufferPool,
214    input: &Tensor,
215    config: &SliceConfig,
216) -> crate::Result<Tensor> {
217    dispatch_tensor_unary_result!(input, |t| typed_slice(buffers, t, config))
218}
219
220pub fn dynamic_slice(
221    input: &Tensor,
222    starts: &Tensor,
223    slice_sizes: &[usize],
224) -> crate::Result<Tensor> {
225    with_local_pool(|buffers| dynamic_slice_with_pool(buffers, input, starts, slice_sizes))
226}
227
228pub(crate) fn dynamic_slice_with_pool(
229    buffers: &mut BufferPool,
230    input: &Tensor,
231    starts: &Tensor,
232    slice_sizes: &[usize],
233) -> crate::Result<Tensor> {
234    let starts = try_index_tensor(starts)?;
235    dispatch_tensor_unary_result!(input, |t| typed_dynamic_slice(
236        buffers,
237        t,
238        &starts,
239        slice_sizes
240    ))
241}
242
243/// Return `operand` with `update` written at dynamic `starts`.
244///
245/// Starts are clamped so the whole update window fits inside the operand,
246/// matching `dynamic_slice` behavior.
247///
248/// # Examples
249///
250/// ```
251/// use tenferro_cpu as cpu;
252/// use tenferro_tensor::{Tensor, TypedTensor};
253///
254/// let operand = Tensor::F64(TypedTensor::from_vec_col_major(vec![5], vec![0.0; 5])?);
255/// let update = Tensor::F64(TypedTensor::from_vec_col_major(vec![2], vec![3.0, 4.0])?);
256/// let starts = Tensor::I64(TypedTensor::from_vec_col_major(vec![1], vec![4])?);
257///
258/// let out = cpu::dynamic_update_slice(&operand, &update, &starts).unwrap();
259/// assert_eq!(out.as_slice::<f64>().unwrap(), &[0.0, 0.0, 0.0, 3.0, 4.0]);
260/// # Ok::<(), tenferro_tensor::Error>(())
261/// ```
262pub fn dynamic_update_slice(
263    operand: &Tensor,
264    update: &Tensor,
265    starts: &Tensor,
266) -> crate::Result<Tensor> {
267    with_local_pool(|buffers| dynamic_update_slice_with_pool(buffers, operand, update, starts))
268}
269
270pub(crate) fn dynamic_update_slice_with_pool(
271    buffers: &mut BufferPool,
272    operand: &Tensor,
273    update: &Tensor,
274    starts: &Tensor,
275) -> crate::Result<Tensor> {
276    let starts = try_index_tensor(starts)?;
277    dispatch_same_dtype_result!("dynamic_update_slice", operand, update, |op, upd| {
278        typed_dynamic_update_slice(buffers, op, upd, &starts)
279    })
280}
281
282pub fn pad(input: &Tensor, config: &PadConfig) -> crate::Result<Tensor> {
283    try_pad(input, config)
284}
285
286fn try_pad(input: &Tensor, config: &PadConfig) -> crate::Result<Tensor> {
287    with_local_pool(|buffers| try_pad_with_pool(buffers, input, config))
288}
289
290pub(crate) fn try_pad_with_pool(
291    buffers: &mut BufferPool,
292    input: &Tensor,
293    config: &PadConfig,
294) -> crate::Result<Tensor> {
295    dispatch_tensor_unary_with_bool_special_result!(
296        input,
297        |t| typed_pad(buffers, t, config),
298        bool | t | typed_pad_with_fill(buffers, t, config, false)
299    )
300}
301
302pub(crate) fn try_concatenate_with_pool(
303    buffers: &mut BufferPool,
304    inputs: &[&Tensor],
305    axis: usize,
306) -> crate::Result<Tensor> {
307    let first = inputs
308        .first()
309        .copied()
310        .ok_or_else(|| crate::Error::InvalidConfig {
311            op: "concatenate",
312            message: "concatenate requires at least one input".into(),
313        })?;
314    dispatch_tensor_unary_result!(first, |t| typed_concatenate_from_dyn_inputs(
315        buffers, t, inputs, axis
316    ))
317}
318
319pub(crate) fn reverse_with_pool(
320    buffers: &mut BufferPool,
321    input: &Tensor,
322    axes: &[usize],
323) -> crate::Result<Tensor> {
324    dispatch_tensor_unary_result!(input, |t| typed_reverse(buffers, t, axes))
325}
326
327fn typed_slice<T: Copy + Clone + PoolScalar>(
328    buffers: &mut BufferPool,
329    input: &TypedTensor<T>,
330    config: &SliceConfig,
331) -> crate::Result<TypedTensor<T>> {
332    let input_shape = input.shape();
333    let rank = input_shape.len();
334    if config.starts.len() != rank {
335        return Err(crate::Error::RankMismatch {
336            op: "slice",
337            expected: rank,
338            actual: config.starts.len(),
339        });
340    }
341    if config.limits.len() != rank {
342        return Err(crate::Error::RankMismatch {
343            op: "slice",
344            expected: rank,
345            actual: config.limits.len(),
346        });
347    }
348    if config.strides.len() != rank {
349        return Err(crate::Error::RankMismatch {
350            op: "slice",
351            expected: rank,
352            actual: config.strides.len(),
353        });
354    }
355
356    let out_shape: Vec<usize> = input
357        .shape()
358        .iter()
359        .enumerate()
360        .map(|(axis, &dim)| {
361            let start = config.starts[axis];
362            let limit = config.limits[axis];
363            let stride = config.strides[axis];
364            if start > limit {
365                return Err(crate::Error::InvalidConfig {
366                    op: "slice",
367                    message: format!("start exceeds limit on axis {axis}"),
368                });
369            }
370            if limit > dim {
371                return Err(crate::Error::AxisOutOfBounds {
372                    op: "slice",
373                    axis,
374                    rank,
375                });
376            }
377            if stride == 0 {
378                return Err(crate::Error::InvalidConfig {
379                    op: "slice",
380                    message: format!("stride must be positive on axis {axis}"),
381                });
382            }
383            let span = limit - start;
384            Ok(span.div_ceil(stride))
385        })
386        .collect::<crate::Result<Vec<_>>>()?;
387
388    // SAFETY: the slice loop below assigns every output coordinate exactly once.
389    let mut out = pooled_uninit_tensor(buffers, out_shape.clone())?;
390    let mut out_idx = vec![0usize; rank];
391    let mut in_idx = vec![0usize; rank];
392
393    for out_value in out.host_data_mut()?.iter_mut() {
394        for axis in 0..rank {
395            in_idx[axis] = config.starts[axis] + out_idx[axis] * config.strides[axis];
396        }
397        *out_value = *input.get(&in_idx)?;
398        advance_col_major_index(&mut out_idx, &out_shape);
399    }
400
401    Ok(out)
402}
403
404fn typed_concatenate_from_dyn_inputs<T>(
405    buffers: &mut BufferPool,
406    _first: &TypedTensor<T>,
407    inputs: &[&Tensor],
408    axis: usize,
409) -> crate::Result<TypedTensor<T>>
410where
411    T: Copy + Clone + PoolScalar,
412    Tensor: TensorAsTyped<T>,
413{
414    let first_dtype = inputs
415        .first()
416        .ok_or_else(|| crate::Error::InvalidConfig {
417            op: "concatenate",
418            message: "concatenate requires at least one input".into(),
419        })?
420        .dtype();
421    let typed_inputs = collect_typed_inputs(first_dtype, inputs)?;
422    typed_concatenate(buffers, &typed_inputs, axis)
423}
424
425fn collect_typed_inputs<'a, T>(
426    first_dtype: crate::DType,
427    inputs: &[&'a Tensor],
428) -> crate::Result<Vec<&'a TypedTensor<T>>>
429where
430    Tensor: TensorAsTyped<T>,
431{
432    inputs
433        .iter()
434        .map(|tensor| {
435            TensorAsTyped::<T>::as_typed(*tensor).ok_or_else(|| crate::Error::DTypeMismatch {
436                op: "concatenate",
437                lhs: first_dtype,
438                rhs: tensor.dtype(),
439            })
440        })
441        .collect()
442}
443
444fn typed_concatenate<T: Copy + Clone + PoolScalar>(
445    buffers: &mut BufferPool,
446    inputs: &[&TypedTensor<T>],
447    axis: usize,
448) -> crate::Result<TypedTensor<T>> {
449    let first = inputs
450        .first()
451        .copied()
452        .ok_or_else(|| crate::Error::InvalidConfig {
453            op: "concatenate",
454            message: "concatenate requires at least one input".into(),
455        })?;
456    let first_shape = first.shape();
457    let rank = first_shape.len();
458    if axis >= rank {
459        return Err(crate::Error::AxisOutOfBounds {
460            op: "concatenate",
461            axis,
462            rank,
463        });
464    }
465
466    let mut out_shape = first_shape.to_vec();
467    let mut axis_extent = 0usize;
468    for input in inputs {
469        let input_shape = input.shape();
470        if input_shape.len() != rank {
471            return Err(crate::Error::RankMismatch {
472                op: "concatenate",
473                expected: rank,
474                actual: input_shape.len(),
475            });
476        }
477        for dim in 0..rank {
478            if dim == axis {
479                axis_extent = axis_extent.checked_add(input_shape[dim]).ok_or_else(|| {
480                    crate::Error::InvalidConfig {
481                        op: "concatenate",
482                        message: "concatenate axis extent overflows usize".to_string(),
483                    }
484                })?;
485            } else if input_shape[dim] != first_shape[dim] {
486                return Err(crate::Error::ShapeMismatch {
487                    op: "concatenate",
488                    lhs: first_shape.to_vec(),
489                    rhs: input_shape.to_vec(),
490                });
491            }
492        }
493    }
494    out_shape[axis] = axis_extent;
495
496    let mut segment_ends = Vec::with_capacity(inputs.len());
497    let mut segment_end = 0usize;
498    for input in inputs {
499        segment_end = segment_end
500            .checked_add(input.shape()[axis])
501            .ok_or_else(|| crate::Error::InvalidConfig {
502                op: "concatenate",
503                message: "concatenate segment offset overflows usize".to_string(),
504            })?;
505        segment_ends.push(segment_end);
506    }
507
508    // SAFETY: the concatenate loop below assigns every output coordinate exactly once.
509    let mut out = pooled_uninit_tensor(buffers, out_shape.clone())?;
510    let mut out_idx = vec![0usize; rank];
511    let mut in_idx = vec![0usize; rank];
512
513    for out_value in out.host_data_mut()?.iter_mut() {
514        let concat_idx = out_idx[axis];
515        let input_pos = segment_ends.partition_point(|&end| concat_idx >= end);
516        if input_pos == segment_ends.len() {
517            return Err(crate::Error::InvalidConfig {
518                op: "concatenate",
519                message: "output index must map to an input".to_string(),
520            });
521        }
522        let axis_base = if input_pos == 0 {
523            0
524        } else {
525            segment_ends[input_pos - 1]
526        };
527
528        in_idx.copy_from_slice(&out_idx);
529        in_idx[axis] -= axis_base;
530        *out_value = *inputs[input_pos].get(&in_idx)?;
531        advance_col_major_index(&mut out_idx, &out_shape);
532    }
533
534    Ok(out)
535}
536
537fn typed_reverse<T: Copy + Clone + PoolScalar>(
538    buffers: &mut BufferPool,
539    input: &TypedTensor<T>,
540    axes: &[usize],
541) -> crate::Result<TypedTensor<T>> {
542    let input_shape = input.shape();
543    let rank = input_shape.len();
544    let mut reverse_axis = vec![false; rank];
545    for &axis in axes {
546        if axis >= rank {
547            return Err(crate::Error::AxisOutOfBounds {
548                op: "reverse",
549                axis,
550                rank,
551            });
552        }
553        reverse_axis[axis] = true;
554    }
555
556    // SAFETY: the reverse loop below assigns every output coordinate exactly once.
557    let mut out = pooled_uninit_tensor(buffers, input_shape.to_vec())?;
558    let mut out_idx = vec![0usize; rank];
559    let mut in_idx = vec![0usize; rank];
560
561    for out_value in out.host_data_mut()?.iter_mut() {
562        for axis in 0..rank {
563            in_idx[axis] = if reverse_axis[axis] {
564                input_shape[axis] - 1 - out_idx[axis]
565            } else {
566                out_idx[axis]
567            };
568        }
569        *out_value = *input.get(&in_idx)?;
570        advance_col_major_index(&mut out_idx, input_shape);
571    }
572
573    Ok(out)
574}
575
576struct IndexTensor {
577    shape: Vec<usize>,
578    values: Vec<i64>,
579}
580
581/// Maximum exact integer representable by f32 (2^24).
582const F32_MAX_EXACT_INT: f32 = 16_777_216.0;
583/// Maximum exact integer representable by f64 (2^53).
584const F64_MAX_EXACT_INT: f64 = 9_007_199_254_740_992.0;
585
586fn f32_index_to_i64(value: f32) -> crate::Result<i64> {
587    if !value.is_finite() || value.fract() != 0.0 || value.abs() > F32_MAX_EXACT_INT {
588        return Err(crate::Error::InvalidConfig {
589            op: "index_tensor",
590            message: format!("index value {value} is not an exactly representable i64"),
591        });
592    }
593    Ok(value as i64)
594}
595
596fn f64_index_to_i64(value: f64) -> crate::Result<i64> {
597    if !value.is_finite() || value.fract() != 0.0 || value.abs() > F64_MAX_EXACT_INT {
598        return Err(crate::Error::InvalidConfig {
599            op: "index_tensor",
600            message: format!("index value {value} is not an exactly representable i64"),
601        });
602    }
603    Ok(value as i64)
604}
605
606fn try_index_tensor(tensor: &Tensor) -> crate::Result<IndexTensor> {
607    match tensor {
608        Tensor::I32(t) => Ok(IndexTensor {
609            shape: t.shape().to_vec(),
610            values: typed_host_data("index_tensor", t)?
611                .iter()
612                .map(|&value| value as i64)
613                .collect(),
614        }),
615        Tensor::I64(t) => Ok(IndexTensor {
616            shape: t.shape().to_vec(),
617            values: typed_host_data("index_tensor", t)?.to_vec(),
618        }),
619        Tensor::F32(t) => {
620            let values: crate::Result<Vec<i64>> = typed_host_data("index_tensor", t)?
621                .iter()
622                .map(|&value| f32_index_to_i64(value))
623                .collect();
624            Ok(IndexTensor {
625                shape: t.shape().to_vec(),
626                values: values?,
627            })
628        }
629        Tensor::F64(t) => {
630            let values: crate::Result<Vec<i64>> = typed_host_data("index_tensor", t)?
631                .iter()
632                .map(|&value| f64_index_to_i64(value))
633                .collect();
634            Ok(IndexTensor {
635                shape: t.shape().to_vec(),
636                values: values?,
637            })
638        }
639        Tensor::Bool(_) => Err(crate::Error::InvalidConfig {
640            op: "index_tensor",
641            message: "bool index tensors are not supported".into(),
642        }),
643        Tensor::C32(_) | Tensor::C64(_) => Err(crate::Error::InvalidConfig {
644            op: "index_tensor",
645            message: "complex index tensors are not supported".into(),
646        }),
647    }
648}
649
650fn checked_product(op: &'static str, role: &'static str, shape: &[usize]) -> crate::Result<usize> {
651    shape.iter().try_fold(1usize, |acc, &dim| {
652        acc.checked_mul(dim)
653            .ok_or_else(|| crate::Error::InvalidConfig {
654                op,
655                message: format!("{role} element count overflows usize"),
656            })
657    })
658}
659
660fn linear_offset(op: &'static str, shape: &[usize], indices: &[usize]) -> crate::Result<usize> {
661    if indices.len() != shape.len() {
662        return Err(crate::Error::RankMismatch {
663            op,
664            expected: shape.len(),
665            actual: indices.len(),
666        });
667    }
668    let mut offset = 0usize;
669    let mut stride = 1usize;
670    for (axis, &index) in indices.iter().enumerate() {
671        if index >= shape[axis] {
672            return Err(crate::Error::AxisOutOfBounds {
673                op,
674                axis,
675                rank: shape.len(),
676            });
677        }
678        let scaled = index
679            .checked_mul(stride)
680            .ok_or_else(|| crate::Error::InvalidConfig {
681                op,
682                message: format!("linear index component overflows usize on axis {axis}"),
683            })?;
684        offset = offset
685            .checked_add(scaled)
686            .ok_or_else(|| crate::Error::InvalidConfig {
687                op,
688                message: format!("linear offset overflows usize on axis {axis}"),
689            })?;
690        stride = stride
691            .checked_mul(shape[axis])
692            .ok_or_else(|| crate::Error::InvalidConfig {
693                op,
694                message: format!("linear stride overflows usize after axis {axis}"),
695            })?;
696    }
697    Ok(offset)
698}
699
700fn try_index_vector_size(
701    op: &'static str,
702    shape: &[usize],
703    index_vector_dim: usize,
704) -> crate::Result<usize> {
705    if index_vector_dim > shape.len() {
706        return Err(crate::Error::AxisOutOfBounds {
707            op,
708            axis: index_vector_dim,
709            rank: shape.len(),
710        });
711    }
712    Ok(if index_vector_dim == shape.len() {
713        1
714    } else {
715        shape[index_vector_dim]
716    })
717}
718
719fn try_index_batch_shape(
720    op: &'static str,
721    shape: &[usize],
722    index_vector_dim: usize,
723) -> crate::Result<Vec<usize>> {
724    if index_vector_dim > shape.len() {
725        return Err(crate::Error::AxisOutOfBounds {
726            op,
727            axis: index_vector_dim,
728            rank: shape.len(),
729        });
730    }
731    if index_vector_dim == shape.len() {
732        return Ok(shape.to_vec());
733    }
734    Ok(shape
735        .iter()
736        .enumerate()
737        .filter_map(|(axis, &dim)| (axis != index_vector_dim).then_some(dim))
738        .collect())
739}
740
741fn index_component(
742    op: &'static str,
743    indices: &IndexTensor,
744    batch_idx: &[usize],
745    index_vector_dim: usize,
746    component: usize,
747    index_scratch: &mut [usize],
748) -> crate::Result<i64> {
749    if index_vector_dim == indices.shape.len() {
750        if component != 0 {
751            return Err(crate::Error::InvalidConfig {
752                op,
753                message: "implicit index_vector_dim only supports scalar index vectors".into(),
754            });
755        }
756        return Ok(indices.values[linear_offset(op, &indices.shape, batch_idx)?]);
757    }
758
759    if index_scratch.len() != indices.shape.len() {
760        return Err(crate::Error::InvalidConfig {
761            op,
762            message: format!(
763                "index scratch length {} must match index tensor rank {}",
764                index_scratch.len(),
765                indices.shape.len()
766            ),
767        });
768    }
769    if batch_idx.len() + 1 != indices.shape.len() {
770        return Err(crate::Error::InvalidConfig {
771            op,
772            message: format!(
773                "batch index rank {} must be one less than index tensor rank {}",
774                batch_idx.len(),
775                indices.shape.len()
776            ),
777        });
778    }
779    let mut batch_axis = 0usize;
780    for (axis, slot) in index_scratch.iter_mut().enumerate() {
781        if axis == index_vector_dim {
782            *slot = component;
783        } else {
784            *slot = batch_idx[batch_axis];
785            batch_axis += 1;
786        }
787    }
788    Ok(indices.values[linear_offset(op, &indices.shape, index_scratch)?])
789}
790
791fn clamp_window_start(
792    op: &'static str,
793    start: i64,
794    dim_size: usize,
795    window_size: usize,
796) -> crate::Result<usize> {
797    if window_size > dim_size {
798        return Err(crate::Error::InvalidConfig {
799            op,
800            message: format!("window size {window_size} exceeds dimension size {dim_size}"),
801        });
802    }
803    let max_start = dim_size.saturating_sub(window_size) as i64;
804    Ok(start.clamp(0, max_start) as usize)
805}
806
807fn operand_window_dims(rank: usize, collapsed_or_inserted: &[usize]) -> Vec<usize> {
808    (0..rank)
809        .filter(|dim| !collapsed_or_inserted.contains(dim))
810        .collect()
811}
812
813fn typed_gather<T: Copy + Clone + PoolScalar>(
814    buffers: &mut BufferPool,
815    operand: &TypedTensor<T>,
816    start_indices: &IndexTensor,
817    config: &GatherConfig,
818) -> crate::Result<TypedTensor<T>> {
819    let operand_shape = operand.shape();
820    let rank = operand_shape.len();
821    if config.slice_sizes.len() != rank {
822        return Err(crate::Error::RankMismatch {
823            op: "gather",
824            expected: rank,
825            actual: config.slice_sizes.len(),
826        });
827    }
828
829    for &dim in &config.collapsed_slice_dims {
830        if dim >= rank {
831            return Err(crate::Error::AxisOutOfBounds {
832                op: "gather",
833                axis: dim,
834                rank,
835            });
836        }
837    }
838    {
839        let mut seen = vec![false; rank];
840        for &dim in &config.collapsed_slice_dims {
841            if seen[dim] {
842                return Err(crate::Error::DuplicateAxis {
843                    op: "gather",
844                    axis: dim,
845                    role: "collapsed_slice_dims",
846                });
847            }
848            seen[dim] = true;
849        }
850    }
851    for &dim in &config.collapsed_slice_dims {
852        if config.slice_sizes[dim] != 1 {
853            return Err(crate::Error::InvalidConfig {
854                op: "gather",
855                message: format!(
856                    "collapsed slice dimension {dim} must have slice_size == 1, got {}",
857                    config.slice_sizes[dim]
858                ),
859            });
860        }
861    }
862
863    let index_size =
864        try_index_vector_size("gather", &start_indices.shape, config.index_vector_dim)?;
865    if index_size != config.start_index_map.len() {
866        return Err(crate::Error::InvalidConfig {
867            op: "gather",
868            message: format!(
869                "start_index_map length {} does not match index vector size {}",
870                config.start_index_map.len(),
871                index_size
872            ),
873        });
874    }
875    for &operand_dim in &config.start_index_map {
876        if operand_dim >= rank {
877            return Err(crate::Error::AxisOutOfBounds {
878                op: "gather",
879                axis: operand_dim,
880                rank,
881            });
882        }
883    }
884    {
885        let mut seen = vec![false; rank];
886        for &operand_dim in &config.start_index_map {
887            if seen[operand_dim] {
888                return Err(crate::Error::DuplicateAxis {
889                    op: "gather",
890                    axis: operand_dim,
891                    role: "start_index_map",
892                });
893            }
894            seen[operand_dim] = true;
895        }
896    }
897
898    let window_dims = operand_window_dims(rank, &config.collapsed_slice_dims);
899    if config.offset_dims.len() != window_dims.len() {
900        return Err(crate::Error::InvalidConfig {
901            op: "gather",
902            message: format!(
903                "offset_dims length {} does not match window dims count {}",
904                config.offset_dims.len(),
905                window_dims.len()
906            ),
907        });
908    }
909
910    let batch_shape =
911        try_index_batch_shape("gather", &start_indices.shape, config.index_vector_dim)?;
912    let out_rank = batch_shape.len() + config.offset_dims.len();
913    for &out_axis in &config.offset_dims {
914        if out_axis >= out_rank {
915            return Err(crate::Error::AxisOutOfBounds {
916                op: "gather",
917                axis: out_axis,
918                rank: out_rank,
919            });
920        }
921    }
922    {
923        let mut seen = vec![false; out_rank];
924        for &out_axis in &config.offset_dims {
925            if seen[out_axis] {
926                return Err(crate::Error::DuplicateAxis {
927                    op: "gather",
928                    axis: out_axis,
929                    role: "offset_dims",
930                });
931            }
932            seen[out_axis] = true;
933        }
934    }
935
936    let mut out_axis_to_operand_dim = vec![None; out_rank];
937    for (offset_axis, &out_axis) in config.offset_dims.iter().enumerate() {
938        out_axis_to_operand_dim[out_axis] = Some(window_dims[offset_axis]);
939    }
940
941    let mut out_shape = vec![0usize; out_rank];
942    let mut batch_axis = 0usize;
943    for out_axis in 0..out_rank {
944        if let Some(operand_dim) = out_axis_to_operand_dim[out_axis] {
945            out_shape[out_axis] = config.slice_sizes[operand_dim];
946        } else {
947            out_shape[out_axis] = batch_shape[batch_axis];
948            batch_axis += 1;
949        }
950    }
951
952    for &operand_dim in &config.start_index_map {
953        let _ = clamp_window_start(
954            "gather",
955            0,
956            operand_shape[operand_dim],
957            config.slice_sizes[operand_dim],
958        )?;
959    }
960
961    // SAFETY: the gather loop below assigns every output coordinate exactly once.
962    let mut out = pooled_uninit_tensor(buffers, out_shape.clone())?;
963    let mut out_idx = vec![0usize; out_rank];
964    let mut batch_idx = vec![0usize; batch_shape.len()];
965    let mut operand_idx = vec![0usize; rank];
966    let mut window_offsets = vec![0usize; rank];
967    let mut index_scratch = vec![0usize; start_indices.shape.len()];
968
969    for out_value in out.host_data_mut()?.iter_mut() {
970        batch_axis = 0;
971        window_offsets.fill(0);
972        for out_axis in 0..out_rank {
973            if let Some(operand_dim) = out_axis_to_operand_dim[out_axis] {
974                window_offsets[operand_dim] = out_idx[out_axis];
975            } else {
976                batch_idx[batch_axis] = out_idx[out_axis];
977                batch_axis += 1;
978            }
979        }
980
981        operand_idx.fill(0);
982        for (component, &operand_dim) in config.start_index_map.iter().enumerate() {
983            let start = index_component(
984                "gather",
985                start_indices,
986                &batch_idx,
987                config.index_vector_dim,
988                component,
989                &mut index_scratch,
990            )?;
991            operand_idx[operand_dim] = clamp_window_start(
992                "gather",
993                start,
994                operand_shape[operand_dim],
995                config.slice_sizes[operand_dim],
996            )?;
997        }
998
999        for axis in 0..operand_idx.len() {
1000            operand_idx[axis] += window_offsets[axis];
1001        }
1002
1003        *out_value = *operand.get(&operand_idx)?;
1004        advance_col_major_index(&mut out_idx, &out_shape);
1005    }
1006
1007    Ok(out)
1008}
1009
1010fn typed_scatter<T>(
1011    buffers: &mut BufferPool,
1012    operand: &TypedTensor<T>,
1013    scatter_indices: &IndexTensor,
1014    updates: &TypedTensor<T>,
1015    config: &ScatterConfig,
1016) -> crate::Result<TypedTensor<T>>
1017where
1018    T: Copy + Clone + Zero + Add<Output = T> + PoolScalar,
1019{
1020    let operand_shape = operand.shape();
1021    let updates_shape = updates.shape();
1022    let op_rank = operand_shape.len();
1023    for &dim in &config.inserted_window_dims {
1024        if dim >= op_rank {
1025            return Err(crate::Error::AxisOutOfBounds {
1026                op: "scatter",
1027                axis: dim,
1028                rank: op_rank,
1029            });
1030        }
1031    }
1032    {
1033        let mut seen = vec![false; op_rank];
1034        for &dim in &config.inserted_window_dims {
1035            if seen[dim] {
1036                return Err(crate::Error::DuplicateAxis {
1037                    op: "scatter",
1038                    axis: dim,
1039                    role: "inserted_window_dims",
1040                });
1041            }
1042            seen[dim] = true;
1043        }
1044    }
1045
1046    let index_size =
1047        try_index_vector_size("scatter", &scatter_indices.shape, config.index_vector_dim)?;
1048    if index_size != config.scatter_dims_to_operand_dims.len() {
1049        return Err(crate::Error::InvalidConfig {
1050            op: "scatter",
1051            message: format!(
1052                "scatter_dims_to_operand_dims length {} does not match index vector size {}",
1053                config.scatter_dims_to_operand_dims.len(),
1054                index_size
1055            ),
1056        });
1057    }
1058    for &operand_dim in &config.scatter_dims_to_operand_dims {
1059        if operand_dim >= op_rank {
1060            return Err(crate::Error::AxisOutOfBounds {
1061                op: "scatter",
1062                axis: operand_dim,
1063                rank: op_rank,
1064            });
1065        }
1066    }
1067    {
1068        let mut seen = vec![false; op_rank];
1069        for &operand_dim in &config.scatter_dims_to_operand_dims {
1070            if seen[operand_dim] {
1071                return Err(crate::Error::DuplicateAxis {
1072                    op: "scatter",
1073                    axis: operand_dim,
1074                    role: "scatter_dims_to_operand_dims",
1075                });
1076            }
1077            seen[operand_dim] = true;
1078        }
1079    }
1080
1081    let batch_shape =
1082        try_index_batch_shape("scatter", &scatter_indices.shape, config.index_vector_dim)?;
1083    let window_dims = operand_window_dims(op_rank, &config.inserted_window_dims);
1084    if config.update_window_dims.len() != window_dims.len() {
1085        return Err(crate::Error::InvalidConfig {
1086            op: "scatter",
1087            message: format!(
1088                "update_window_dims length {} does not match window dims count {}",
1089                config.update_window_dims.len(),
1090                window_dims.len()
1091            ),
1092        });
1093    }
1094
1095    let update_rank = updates_shape.len();
1096    let expected_batch_rank = update_rank
1097        .checked_sub(config.update_window_dims.len())
1098        .ok_or_else(|| crate::Error::InvalidConfig {
1099            op: "scatter",
1100            message: format!(
1101                "update_window_dims length {} exceeds update rank {}",
1102                config.update_window_dims.len(),
1103                update_rank
1104            ),
1105        })?;
1106    if expected_batch_rank != batch_shape.len() {
1107        return Err(crate::Error::InvalidConfig {
1108            op: "scatter",
1109            message: format!(
1110                "updates batch rank {} does not match index batch shape length {}",
1111                expected_batch_rank,
1112                batch_shape.len()
1113            ),
1114        });
1115    }
1116
1117    for &axis in &config.update_window_dims {
1118        if axis >= update_rank {
1119            return Err(crate::Error::AxisOutOfBounds {
1120                op: "scatter",
1121                axis,
1122                rank: update_rank,
1123            });
1124        }
1125    }
1126    {
1127        let mut seen = vec![false; update_rank];
1128        for &axis in &config.update_window_dims {
1129            if seen[axis] {
1130                return Err(crate::Error::DuplicateAxis {
1131                    op: "scatter",
1132                    axis,
1133                    role: "update_window_dims",
1134                });
1135            }
1136            seen[axis] = true;
1137        }
1138    }
1139
1140    let mut is_update_window_dim = vec![false; update_rank];
1141    for &axis in &config.update_window_dims {
1142        is_update_window_dim[axis] = true;
1143    }
1144
1145    {
1146        let mut batch_axis = 0usize;
1147        for axis in 0..update_rank {
1148            if !is_update_window_dim[axis] {
1149                if updates_shape[axis] != batch_shape[batch_axis] {
1150                    return Err(crate::Error::InvalidConfig {
1151                        op: "scatter",
1152                        message: format!(
1153                            "updates batch dim {} extent {} does not match index batch dim {} extent {}",
1154                            axis,
1155                            updates_shape[axis],
1156                            batch_axis,
1157                            batch_shape[batch_axis]
1158                        ),
1159                    });
1160                }
1161                batch_axis += 1;
1162            }
1163        }
1164    }
1165
1166    let mut window_shape = vec![1usize; op_rank];
1167    let mut window_shape_updates = vec![0usize; config.update_window_dims.len()];
1168    for (pos, &update_axis) in config.update_window_dims.iter().enumerate() {
1169        let dim = updates_shape[update_axis];
1170        window_shape_updates[pos] = dim;
1171        window_shape[window_dims[pos]] = dim;
1172    }
1173    for axis in 0..op_rank {
1174        let _ = clamp_window_start("scatter", 0, operand_shape[axis], window_shape[axis])?;
1175    }
1176
1177    let batch_elems = checked_product("scatter", "batch shape", &batch_shape)?;
1178    let window_elems = checked_product("scatter", "window update shape", &window_shape_updates)?;
1179    let mut out = clone_host_tensor_from_pool(buffers, "scatter", operand)?;
1180
1181    let mut batch_idx = vec![0usize; batch_shape.len()];
1182    let mut window_idx = vec![0usize; window_shape_updates.len()];
1183    let mut update_idx = vec![0usize; update_rank];
1184    let mut operand_base = vec![0usize; op_rank];
1185    let mut operand_idx = vec![0usize; op_rank];
1186    let mut index_scratch = vec![0usize; scatter_indices.shape.len()];
1187
1188    for _ in 0..batch_elems {
1189        operand_base.fill(0);
1190        for (component, &operand_dim) in config.scatter_dims_to_operand_dims.iter().enumerate() {
1191            let start = index_component(
1192                "scatter",
1193                scatter_indices,
1194                &batch_idx,
1195                config.index_vector_dim,
1196                component,
1197                &mut index_scratch,
1198            )?;
1199            operand_base[operand_dim] = clamp_window_start(
1200                "scatter",
1201                start,
1202                operand_shape[operand_dim],
1203                window_shape[operand_dim],
1204            )?;
1205        }
1206
1207        window_idx.fill(0);
1208        for _ in 0..window_elems {
1209            let mut batch_axis = 0usize;
1210            let mut window_axis = 0usize;
1211            for axis in 0..update_rank {
1212                if is_update_window_dim[axis] {
1213                    update_idx[axis] = window_idx[window_axis];
1214                    window_axis += 1;
1215                } else {
1216                    update_idx[axis] = batch_idx[batch_axis];
1217                    batch_axis += 1;
1218                }
1219            }
1220
1221            operand_idx.copy_from_slice(&operand_base);
1222            for (window_axis, &operand_axis) in window_dims.iter().enumerate() {
1223                operand_idx[operand_axis] += window_idx[window_axis];
1224            }
1225
1226            let value = *updates.get(&update_idx)?;
1227            let slot = out.get_mut(&operand_idx)?;
1228            *slot = *slot + value;
1229            advance_col_major_index(&mut window_idx, &window_shape_updates);
1230        }
1231        advance_col_major_index(&mut batch_idx, &batch_shape);
1232    }
1233
1234    Ok(out)
1235}
1236
1237fn typed_dynamic_slice<T: Copy + Clone + PoolScalar>(
1238    buffers: &mut BufferPool,
1239    input: &TypedTensor<T>,
1240    starts: &IndexTensor,
1241    slice_sizes: &[usize],
1242) -> crate::Result<TypedTensor<T>> {
1243    let input_shape = input.shape();
1244    if slice_sizes.len() != input_shape.len() {
1245        return Err(crate::Error::RankMismatch {
1246            op: "dynamic_slice",
1247            expected: input_shape.len(),
1248            actual: slice_sizes.len(),
1249        });
1250    }
1251    if starts.shape.len() != 1 {
1252        return Err(crate::Error::InvalidConfig {
1253            op: "dynamic_slice",
1254            message: "starts must be a rank-1 tensor".into(),
1255        });
1256    }
1257    if starts.values.len() != input_shape.len() {
1258        return Err(crate::Error::InvalidConfig {
1259            op: "dynamic_slice",
1260            message: format!(
1261                "starts length {} must match input rank {}",
1262                starts.values.len(),
1263                input_shape.len()
1264            ),
1265        });
1266    }
1267
1268    let mut clamped_starts = vec![0usize; input_shape.len()];
1269    for axis in 0..input_shape.len() {
1270        clamped_starts[axis] = clamp_window_start(
1271            "dynamic_slice",
1272            starts.values[axis],
1273            input_shape[axis],
1274            slice_sizes[axis],
1275        )?;
1276    }
1277
1278    let out_shape = slice_sizes.to_vec();
1279    // SAFETY: the dynamic-slice loop below assigns every output coordinate exactly once.
1280    let mut out = pooled_uninit_tensor(buffers, out_shape.clone())?;
1281    let mut out_idx = vec![0usize; out_shape.len()];
1282    let mut input_idx = vec![0usize; out_shape.len()];
1283
1284    for out_value in out.host_data_mut()?.iter_mut() {
1285        for axis in 0..out_shape.len() {
1286            input_idx[axis] = clamped_starts[axis] + out_idx[axis];
1287        }
1288        *out_value = *input.get(&input_idx)?;
1289        advance_col_major_index(&mut out_idx, &out_shape);
1290    }
1291
1292    Ok(out)
1293}
1294
1295fn typed_dynamic_update_slice<T: Copy + Clone + PoolScalar>(
1296    buffers: &mut BufferPool,
1297    operand: &TypedTensor<T>,
1298    update: &TypedTensor<T>,
1299    starts: &IndexTensor,
1300) -> crate::Result<TypedTensor<T>> {
1301    let operand_shape = operand.shape();
1302    let update_shape = update.shape();
1303    if update_shape.len() != operand_shape.len() {
1304        return Err(crate::Error::RankMismatch {
1305            op: "dynamic_update_slice",
1306            expected: operand_shape.len(),
1307            actual: update_shape.len(),
1308        });
1309    }
1310    if starts.shape.len() != 1 {
1311        return Err(crate::Error::InvalidConfig {
1312            op: "dynamic_update_slice",
1313            message: "starts must be a rank-1 tensor".into(),
1314        });
1315    }
1316    if starts.values.len() != operand_shape.len() {
1317        return Err(crate::Error::InvalidConfig {
1318            op: "dynamic_update_slice",
1319            message: format!(
1320                "starts length {} must match operand rank {}",
1321                starts.values.len(),
1322                operand_shape.len()
1323            ),
1324        });
1325    }
1326
1327    let mut clamped_starts = vec![0usize; operand_shape.len()];
1328    for axis in 0..operand_shape.len() {
1329        clamped_starts[axis] = clamp_window_start(
1330            "dynamic_update_slice",
1331            starts.values[axis],
1332            operand_shape[axis],
1333            update_shape[axis],
1334        )?;
1335    }
1336
1337    let mut out = clone_host_tensor_from_pool(buffers, "dynamic_update_slice", operand)?;
1338    let mut update_idx = vec![0usize; update_shape.len()];
1339    let mut operand_idx = vec![0usize; operand_shape.len()];
1340
1341    for update_value in update.as_slice()? {
1342        for axis in 0..update_shape.len() {
1343            operand_idx[axis] = clamped_starts[axis] + update_idx[axis];
1344        }
1345        *out.get_mut(&operand_idx)? = *update_value;
1346        advance_col_major_index(&mut update_idx, update_shape);
1347    }
1348
1349    Ok(out)
1350}
1351
1352fn typed_pad<T: Copy + Clone + Zero + PoolScalar>(
1353    buffers: &mut BufferPool,
1354    input: &TypedTensor<T>,
1355    config: &PadConfig,
1356) -> crate::Result<TypedTensor<T>> {
1357    typed_pad_with_fill(buffers, input, config, T::zero())
1358}
1359
1360fn typed_pad_with_fill<T: Copy + Clone + PoolScalar>(
1361    buffers: &mut BufferPool,
1362    input: &TypedTensor<T>,
1363    config: &PadConfig,
1364    fill: T,
1365) -> crate::Result<TypedTensor<T>> {
1366    let input_shape = input.shape();
1367    let rank = input_shape.len();
1368    if config.edge_padding_low.len() != rank {
1369        return Err(crate::Error::RankMismatch {
1370            op: "pad",
1371            expected: rank,
1372            actual: config.edge_padding_low.len(),
1373        });
1374    }
1375    if config.edge_padding_high.len() != rank {
1376        return Err(crate::Error::RankMismatch {
1377            op: "pad",
1378            expected: rank,
1379            actual: config.edge_padding_high.len(),
1380        });
1381    }
1382    if config.interior_padding.len() != rank {
1383        return Err(crate::Error::RankMismatch {
1384            op: "pad",
1385            expected: rank,
1386            actual: config.interior_padding.len(),
1387        });
1388    }
1389
1390    let mut out_shape = Vec::with_capacity(input_shape.len());
1391    for (axis, &input_extent) in input_shape.iter().enumerate() {
1392        if config.interior_padding[axis] < 0 {
1393            return Err(crate::Error::InvalidConfig {
1394                op: "pad",
1395                message: format!("interior padding must be non-negative on axis {axis}"),
1396            });
1397        }
1398        let input_extent_i64 =
1399            i64::try_from(input_extent).map_err(|_| crate::Error::InvalidConfig {
1400                op: "pad",
1401                message: format!("input extent on axis {axis} does not fit in i64"),
1402            })?;
1403        let spacing = config.interior_padding[axis]
1404            .checked_add(1)
1405            .ok_or_else(|| crate::Error::InvalidConfig {
1406                op: "pad",
1407                message: format!("interior padding overflow on axis {axis}"),
1408            })?;
1409        let base = if input_extent == 0 {
1410            0
1411        } else {
1412            input_extent_i64
1413                .checked_sub(1)
1414                .and_then(|extent| extent.checked_mul(spacing))
1415                .and_then(|extent| extent.checked_add(1))
1416                .ok_or_else(|| crate::Error::InvalidConfig {
1417                    op: "pad",
1418                    message: format!("padded input extent overflow on axis {axis}"),
1419                })?
1420        };
1421        let dim = config.edge_padding_low[axis]
1422            .checked_add(config.edge_padding_high[axis])
1423            .and_then(|edge| edge.checked_add(base))
1424            .ok_or_else(|| crate::Error::InvalidConfig {
1425                op: "pad",
1426                message: format!("output dimension overflow on axis {axis}"),
1427            })?;
1428        out_shape.push(
1429            usize::try_from(dim).map_err(|_| crate::Error::InvalidConfig {
1430                op: "pad",
1431                message: format!("negative output dimension on axis {axis}"),
1432            })?,
1433        );
1434    }
1435
1436    let mut out = pooled_filled_tensor(buffers, out_shape.clone(), fill)?;
1437    let mut input_idx = vec![0usize; input_shape.len()];
1438    let mut out_idx = vec![0usize; input_shape.len()];
1439
1440    for input_value in input.as_slice()? {
1441        let mut in_bounds = true;
1442        for axis in 0..input_shape.len() {
1443            let out_pos = i128::from(config.edge_padding_low[axis])
1444                + input_idx[axis] as i128 * i128::from(config.interior_padding[axis] + 1);
1445            if !(0..out_shape[axis] as i128).contains(&out_pos) {
1446                in_bounds = false;
1447                break;
1448            }
1449            out_idx[axis] = out_pos as usize;
1450        }
1451        if in_bounds {
1452            *out.get_mut(&out_idx)? = *input_value;
1453        }
1454        advance_col_major_index(&mut input_idx, input_shape);
1455    }
1456
1457    Ok(out)
1458}
1459
1460#[cfg(test)]
1461mod tests;