Skip to main content

nuvec_core/
index.rs

1//! Indexing (`x.i(..)`, MLX's `x[...]`) and functional updates (`x.at(..).set(v)`, JAX's
2//! `x.at[...].set(v)`).
3//!
4//! An index is an integer, a range (over `i32` or `usize`, optionally `.step(n)`), `..`,
5//! [`Ellipsis`], [`NewAxis`], an integer array, or a tuple of up to eight of these. Basic
6//! indices (no arrays) give views; integer arrays gather, following MLX's (NumPy's) rules for
7//! broadcasting the index arrays and placing their dimensions.
8
9use std::ops::{Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive};
10use std::panic::Location;
11
12use smallvec::SmallVec;
13
14use crate::ops::imp;
15use crate::scalar::ScalarOrArray;
16use crate::shape::Shape;
17use crate::view::SliceArg;
18use crate::{Array, Category, DType, Error, Result};
19
20mod sealed {
21    pub trait Sealed {}
22}
23
24/// `...` in an index: as many full slices as the other entries leave dimensions.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26pub struct Ellipsis;
27
28/// `None` (`np.newaxis`) in an index: a new dimension of size 1.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
30pub struct NewAxis;
31
32/// A range with a step, made by [`Step::step`]: the range selects positions and the step
33/// walks them, backwards when negative (ndarray's rule). `(0..6).step(2)` is Python's
34/// `0:6:2`, `(..).step(-1)` is `::-1`, `(1..4).step(-1)` is `3:0:-1` (positions 3, 2, 1).
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct Stepped<R> {
37    range: R,
38    step: i32,
39}
40
41/// One entry of an index after lowering.
42#[doc(hidden)]
43#[derive(Debug, Clone)]
44pub enum IndexItem {
45    /// A single position (removes the dimension).
46    Int(i64),
47    /// A range `start..stop` (negative bounds count from the end; clamped) with a step: a
48    /// negative step walks the range backwards from its end (ndarray's rule).
49    Slice {
50        start: Option<i64>,
51        stop: Option<i64>,
52        step: i64,
53    },
54    /// A new size-1 dimension.
55    NewAxis,
56    /// The remaining dimensions.
57    Ellipsis,
58    /// An integer index array (gather).
59    Array(Array),
60}
61
62/// Bounds of a range, for [`Step`].
63#[doc(hidden)]
64pub trait Bounds: sealed::Sealed {
65    #[doc(hidden)]
66    fn bounds(&self) -> (Option<i64>, Option<i64>);
67}
68
69/// Adds a step to a range used as an index: `x.i((0..6).step(2))`, `x.i((..).step(-1))`
70/// (see [`Stepped`] for negative steps).
71#[diagnostic::on_unimplemented(
72    message = "`{Self}` cannot take a step",
73    label = "`.step(n)` applies to ranges such as `0..6`, `1..`, `..=4` or `..`"
74)]
75pub trait Step: Bounds + Sized {
76    /// This range with every `n`-th position (negative: backwards, as Python's `::-1`).
77    fn step(self, n: i32) -> Stepped<Self> {
78        Stepped {
79            range: self,
80            step: n,
81        }
82    }
83}
84
85/// The exclusive end of an inclusive range ending at `end` (`..=-1` runs to the end).
86fn inclusive_end(end: i64) -> Option<i64> {
87    if end == -1 {
88        None
89    } else {
90        Some(end.saturating_add(1))
91    }
92}
93
94use crate::axes::wide;
95
96macro_rules! bounds {
97    ($($t:ty => |$r:ident| $e:expr),* $(,)?) => {$(
98        impl sealed::Sealed for $t {}
99        impl Bounds for $t {
100            fn bounds(&self) -> (Option<i64>, Option<i64>) {
101                let $r = self;
102                $e
103            }
104        }
105        impl Step for $t {}
106    )*};
107}
108
109bounds! {
110    Range<i32> => |r| (Some(r.start.into()), Some(r.end.into())),
111    Range<usize> => |r| (Some(wide(r.start)), Some(wide(r.end))),
112    RangeFrom<i32> => |r| (Some(r.start.into()), None),
113    RangeFrom<usize> => |r| (Some(wide(r.start)), None),
114    RangeTo<i32> => |r| (None, Some(r.end.into())),
115    RangeTo<usize> => |r| (None, Some(wide(r.end))),
116    RangeInclusive<i32> => |r| (Some((*r.start()).into()), inclusive_end((*r.end()).into())),
117    RangeInclusive<usize> => |r| (Some(wide(*r.start())), inclusive_end(wide(*r.end()))),
118    RangeToInclusive<i32> => |r| (None, inclusive_end(r.end.into())),
119    RangeToInclusive<usize> => |r| (None, inclusive_end(wide(r.end))),
120    RangeFull => |_r| (None, None),
121}
122
123/// An index of [`Array::i`] and [`Array::at`]: an `i32` or `usize`, a range over either
124/// (`1..3`, `1..`, `..3`, `1..=2`, `..=2`, with an optional `.step(n)`), `..`, [`Ellipsis`],
125/// [`NewAxis`], an integer index array (`&Array` or `Array`), or a tuple of up to eight of
126/// these.
127#[diagnostic::on_unimplemented(
128    message = "`{Self}` is not an index",
129    label = "expected an integer, a range (`1..3`, `..=2`, `(0..6).step(2)`), `..`, `Ellipsis`, `NewAxis`, an index array (`&idx`) or a tuple of these"
130)]
131pub trait ArrayIndex: sealed::Sealed {
132    #[doc(hidden)]
133    fn index_items(self, out: &mut SmallVec<[IndexItem; 4]>);
134}
135
136fn slice_item(start: Option<i64>, stop: Option<i64>, step: i64) -> IndexItem {
137    IndexItem::Slice { start, stop, step }
138}
139
140/// A range item as a Python slice of a dimension of size `n`.
141fn python_slice(start: Option<i64>, stop: Option<i64>, step: i64, n: usize) -> SliceArg {
142    let n = n as i64;
143    let norm = |b: i64| (if b < 0 { b + n } else { b }).clamp(0, n);
144    let s = start.map_or(0, norm);
145    let e = stop.map_or(n, norm).max(s);
146    let step = step.clamp(isize::MIN as i64, isize::MAX as i64) as isize;
147    if step > 0 {
148        SliceArg::new(Some(s as isize), Some(e as isize), step)
149    } else if e == s {
150        SliceArg::new(Some(0), Some(0), 1)
151    } else {
152        let stop = if s == 0 { None } else { Some(s as isize - 1) };
153        SliceArg::new(Some(e as isize - 1), stop, step)
154    }
155}
156
157macro_rules! range_index {
158    ($($t:ty),*) => {$(
159        impl ArrayIndex for $t {
160            fn index_items(self, out: &mut SmallVec<[IndexItem; 4]>) {
161                let (start, stop) = self.bounds();
162                out.push(slice_item(start, stop, 1));
163            }
164        }
165        impl sealed::Sealed for Stepped<$t> {}
166        impl ArrayIndex for Stepped<$t> {
167            fn index_items(self, out: &mut SmallVec<[IndexItem; 4]>) {
168                let (start, stop) = self.range.bounds();
169                out.push(slice_item(start, stop, self.step.into()));
170            }
171        }
172    )*};
173}
174
175range_index!(
176    Range<i32>,
177    Range<usize>,
178    RangeFrom<i32>,
179    RangeFrom<usize>,
180    RangeTo<i32>,
181    RangeTo<usize>,
182    RangeInclusive<i32>,
183    RangeInclusive<usize>,
184    RangeToInclusive<i32>,
185    RangeToInclusive<usize>,
186    RangeFull
187);
188
189impl sealed::Sealed for i32 {}
190impl ArrayIndex for i32 {
191    fn index_items(self, out: &mut SmallVec<[IndexItem; 4]>) {
192        out.push(IndexItem::Int(self.into()));
193    }
194}
195
196impl sealed::Sealed for usize {}
197impl ArrayIndex for usize {
198    fn index_items(self, out: &mut SmallVec<[IndexItem; 4]>) {
199        out.push(IndexItem::Int(wide(self)));
200    }
201}
202
203impl sealed::Sealed for Ellipsis {}
204impl ArrayIndex for Ellipsis {
205    fn index_items(self, out: &mut SmallVec<[IndexItem; 4]>) {
206        out.push(IndexItem::Ellipsis);
207    }
208}
209
210impl sealed::Sealed for NewAxis {}
211impl ArrayIndex for NewAxis {
212    fn index_items(self, out: &mut SmallVec<[IndexItem; 4]>) {
213        out.push(IndexItem::NewAxis);
214    }
215}
216
217impl sealed::Sealed for &Array {}
218impl ArrayIndex for &Array {
219    fn index_items(self, out: &mut SmallVec<[IndexItem; 4]>) {
220        out.push(IndexItem::Array(self.clone()));
221    }
222}
223
224impl sealed::Sealed for Array {}
225impl ArrayIndex for Array {
226    fn index_items(self, out: &mut SmallVec<[IndexItem; 4]>) {
227        out.push(IndexItem::Array(self));
228    }
229}
230
231macro_rules! tuple_index {
232    ($($name:ident)+) => {
233        impl<$($name: ArrayIndex),+> sealed::Sealed for ($($name,)+) {}
234        #[diagnostic::do_not_recommend]
235        impl<$($name: ArrayIndex),+> ArrayIndex for ($($name,)+) {
236            #[allow(non_snake_case)]
237            fn index_items(self, out: &mut SmallVec<[IndexItem; 4]>) {
238                let ($($name,)+) = self;
239                $($name.index_items(out);)+
240            }
241        }
242    };
243}
244
245tuple_index!(A);
246tuple_index!(A B);
247tuple_index!(A B C);
248tuple_index!(A B C D);
249tuple_index!(A B C D E);
250tuple_index!(A B C D E F);
251tuple_index!(A B C D E F G);
252tuple_index!(A B C D E F G H);
253
254fn items_of(index: impl ArrayIndex) -> SmallVec<[IndexItem; 4]> {
255    let mut items = SmallVec::new();
256    index.index_items(&mut items);
257    items
258}
259
260/// The arrays of an index, for failure reporting.
261fn index_arrays(items: &[IndexItem]) -> impl Iterator<Item = &Array> {
262    items.iter().filter_map(|i| match i {
263        IndexItem::Array(a) => Some(a),
264        _ => None,
265    })
266}
267
268/// Where an output dimension of an index comes from.
269#[derive(Debug, Clone)]
270enum Slot {
271    /// A new size-1 dimension.
272    New,
273    /// Input dimension `d`, sliced.
274    Dim(usize),
275    /// Input dimension `d`, indexed by an integer (`None`) or an index array.
276    Adv(usize, Option<Array>, i64),
277}
278
279/// An index resolved against an array of `shape`: one slice per input dimension and the
280/// output slots.
281struct Resolved {
282    args: Vec<SliceArg>,
283    slots: Vec<Slot>,
284}
285
286impl Resolved {
287    fn has_arrays(&self) -> bool {
288        self.slots
289            .iter()
290            .any(|s| matches!(s, Slot::Adv(_, Some(_), _)))
291    }
292}
293
294fn resolve(items: &[IndexItem], shape: &[usize]) -> Result<Resolved> {
295    let ndim = shape.len();
296    let ellipses = items
297        .iter()
298        .filter(|i| matches!(i, IndexItem::Ellipsis))
299        .count();
300    if ellipses > 1 {
301        return Err(Error::invalid_index(
302            "index",
303            "an index can only have a single ellipsis",
304        ));
305    }
306    let consuming = items
307        .iter()
308        .filter(|i| !matches!(i, IndexItem::Ellipsis | IndexItem::NewAxis))
309        .count();
310    if consuming > ndim {
311        return Err(Error::invalid_index(
312            "index",
313            format!("too many indices ({consuming}) for an array of {ndim} dimensions"),
314        ));
315    }
316    let mut args = Vec::with_capacity(ndim);
317    let mut slots = Vec::with_capacity(items.len() + ndim);
318    let mut dim = 0usize;
319    let full = |dim: usize, args: &mut Vec<SliceArg>, slots: &mut Vec<Slot>| {
320        args.push(SliceArg::full());
321        slots.push(Slot::Dim(dim));
322    };
323    for item in items {
324        match item {
325            IndexItem::Ellipsis => {
326                for _ in 0..ndim - consuming {
327                    full(dim, &mut args, &mut slots);
328                    dim += 1;
329                }
330            }
331            IndexItem::NewAxis => slots.push(Slot::New),
332            IndexItem::Slice { start, stop, step } => {
333                if *step == 0 {
334                    return Err(Error::invalid_index("index", "slice step must not be zero"));
335                }
336                args.push(python_slice(*start, *stop, *step, shape[dim]));
337                slots.push(Slot::Dim(dim));
338                dim += 1;
339            }
340            IndexItem::Int(i) => {
341                let n = shape[dim] as i64;
342                let k = if *i < 0 { i + n } else { *i };
343                if !(0..n).contains(&k) {
344                    return Err(Error::invalid_index(
345                        "index",
346                        format!("index {i} is out of bounds for axis {dim} with size {n}"),
347                    ));
348                }
349                args.push(SliceArg::new(Some(k as isize), Some(k as isize + 1), 1));
350                slots.push(Slot::Adv(dim, None, k));
351                dim += 1;
352            }
353            IndexItem::Array(a) => {
354                if !matches!(a.dtype().category(), Category::Signed | Category::Unsigned) {
355                    return Err(Error::dtype_mismatch("index", a.dtype(), DType::I32));
356                }
357                args.push(SliceArg::full());
358                slots.push(Slot::Adv(dim, Some(a.clone()), 0));
359                dim += 1;
360            }
361        }
362    }
363    while dim < ndim {
364        full(dim, &mut args, &mut slots);
365        dim += 1;
366    }
367    Ok(Resolved { args, slots })
368}
369
370/// The shape of the basic index `r` applied to `sliced` (the array after slicing).
371fn basic_shape(r: &Resolved, sliced: &[usize]) -> Shape {
372    r.slots
373        .iter()
374        .filter_map(|s| match s {
375            Slot::New => Some(1),
376            Slot::Dim(d) => Some(sliced[*d]),
377            Slot::Adv(..) => None,
378        })
379        .collect()
380}
381
382/// `a[index]` for a basic index: one slice view, reshaped to drop integer positions and
383/// insert new axes (still a view).
384fn basic(a: &Array, r: &Resolved) -> Result<Array> {
385    let view = imp::slice(a, &r.args)?;
386    let shape = basic_shape(r, view.dims());
387    imp::reshape_to(&view, shape)
388}
389
390/// The index arrays of `r`'s advanced slots (integers as 0-D arrays), with their input
391/// dimensions, wrapped (negative counts from the end) and clamped into range.
392fn advanced_indices(a: &Array, r: &Resolved) -> Result<Vec<(usize, Array)>> {
393    r.slots
394        .iter()
395        .filter_map(|s| match s {
396            Slot::Adv(d, idx, k) => Some((*d, idx.clone(), *k)),
397            _ => None,
398        })
399        .map(|(d, idx, k)| {
400            let n = a.dims()[d] as i64;
401            let idx = match idx {
402                Some(idx) => {
403                    let idx = imp::astype(&idx, DType::I64)?;
404                    let wrapped = imp::where_(imp::less(&idx, 0)?, imp::add(&idx, n)?, &idx)?;
405                    imp::minimum(&imp::maximum(&wrapped, 0)?, (n - 1).max(0))?
406                }
407                None => crate::creation::from_data(&[k], &[])?,
408            };
409            Ok((d, idx))
410        })
411        .collect()
412}
413
414/// One linear index over the indexed axes `adv` of `v` (row-major over them), broadcast to
415/// the index arrays' common shape: `(index, that shape, the indexed axes' element count)`.
416fn linear_index(v: &Array, adv: &[(usize, Array)]) -> Result<(Array, Shape, usize)> {
417    let mut bshape: Shape = Shape::new();
418    for (_, idx) in adv {
419        bshape = crate::view::broadcast_shapes("index", &bshape, idx.dims())?;
420    }
421    let sizes: Vec<usize> = adv.iter().map(|&(d, _)| v.dims()[d]).collect();
422    let total: usize = sizes.iter().product();
423    let mut linear: Option<Array> = None;
424    let mut stride = 1usize;
425    for ((_, idx), &n) in adv.iter().zip(&sizes).rev() {
426        let term = imp::multiply(idx, stride as i64)?;
427        linear = Some(match linear {
428            None => term,
429            Some(l) => imp::add(&l, &term)?,
430        });
431        stride *= n;
432    }
433    let linear = imp::broadcast_to(
434        &linear.unwrap_or_else(|| unreachable!("an advanced index has an index array")),
435        &bshape,
436    )?;
437    let index_dtype = if total <= i32::MAX as usize {
438        DType::I32
439    } else {
440        DType::I64
441    };
442    Ok((imp::astype(&linear, index_dtype)?, bshape, total))
443}
444
445/// `a[index]` with index arrays (MLX's advanced indexing): the index arrays (and integers)
446/// broadcast together; their dimensions replace the indexed axes where those are adjacent,
447/// else they come first. Lowered to one `take` over the indexed axes flattened.
448fn advanced(a: &Array, r: &Resolved) -> Result<Array> {
449    // Slices first (a view): every input dimension stays; integers index like 0-D arrays,
450    // so their dimensions are not sliced.
451    let mut args = r.args.clone();
452    for slot in &r.slots {
453        if let Slot::Adv(d, None, _) = slot {
454            args[*d] = SliceArg::full();
455        }
456    }
457    let v = imp::slice(a, &args)?;
458    let adv = advanced_indices(&v, r)?;
459    let adv_dims: Vec<usize> = adv.iter().map(|(d, _)| *d).collect();
460    let rest: Vec<usize> = (0..v.ndim()).filter(|d| !adv_dims.contains(d)).collect();
461    let (linear, bshape, total) = linear_index(&v, &adv)?;
462    // Indexed axes first, flattened into one, gathered.
463    let perm: Vec<usize> = adv_dims.iter().chain(&rest).copied().collect();
464    let vt = imp::transpose(&v, &perm)?;
465    let mut flat: Shape = Shape::from_elem(total, 1);
466    flat.extend(rest.iter().map(|&d| v.dims()[d]));
467    let vf = imp::reshape_to(&vt, flat)?;
468    let out = crate::indexing::take(&vf, &linear, Some(0))?;
469    // `out` is [bshape..., rest...]; arrange it as MLX places the dimensions.
470    let nb = bshape.len();
471    let adv_positions: Vec<usize> = r
472        .slots
473        .iter()
474        .enumerate()
475        .filter(|(_, s)| matches!(s, Slot::Adv(..)))
476        .map(|(i, _)| i)
477        .collect();
478    let adjacent = adv_positions.windows(2).all(|w| w[1] == w[0] + 1);
479    let mut order: Vec<usize> = Vec::with_capacity(out.ndim());
480    let mut shape: Shape = Shape::new();
481    let mut next_rest = nb;
482    let mut placed = !adjacent;
483    if !adjacent {
484        order.extend(0..nb);
485        shape.extend_from_slice(&bshape);
486    }
487    for slot in &r.slots {
488        match slot {
489            Slot::New => shape.push(1),
490            Slot::Dim(_) => {
491                order.push(next_rest);
492                shape.push(out.dims()[next_rest]);
493                next_rest += 1;
494            }
495            Slot::Adv(..) => {
496                if !placed {
497                    order.extend(0..nb);
498                    shape.extend_from_slice(&bshape);
499                    placed = true;
500                }
501            }
502        }
503    }
504    let arranged = imp::transpose(&out, &order)?;
505    imp::reshape_to(&arranged, shape)
506}
507
508fn index_with(a: &Array, items: &[IndexItem]) -> Result<Array> {
509    let r = resolve(items, a.dims())?;
510    if r.has_arrays() {
511        advanced(a, &r)
512    } else {
513        basic(a, &r)
514    }
515}
516
517impl Array {
518    /// The array indexed as MLX's `x[...]`: integers, ranges (`1..3`, `..=2`,
519    /// `(0..6).step(2)`, `(..).step(-1)`), `..`, [`Ellipsis`], [`NewAxis`], integer index
520    /// arrays, and tuples of these (one entry per dimension, leading dimensions first).
521    ///
522    /// Basic indices give views (no copy); integer arrays gather. Integers out of bounds,
523    /// two ellipses, too many entries or a zero step make the result a failed array
524    /// ([`crate::OpErrorKind::InvalidIndex`]); slices clamp as in Python.
525    ///
526    /// ```
527    /// use nuvec_core::{Array, Ellipsis, NewAxis, Step};
528    ///
529    /// let x = Array::from_slice(&(0..24).collect::<Vec<i32>>(), [2, 3, 4]);
530    /// assert_eq!(x.i(0).shape(), [3, 4]);                          // x[0]
531    /// assert_eq!(x.i((.., 1..3)).shape(), [2, 2, 4]);              // x[:, 1:3]
532    /// assert_eq!(x.i((Ellipsis, -1)).to_vec::<i32>().unwrap()[0], 3); // x[..., -1]
533    /// assert_eq!(x.i((.., NewAxis)).shape(), [2, 1, 3, 4]);        // x[:, None]
534    /// assert_eq!(x.i((.., .., (0..4).step(2))).shape(), [2, 3, 2]); // x[:, :, 0:4:2]
535    /// let rows = Array::from_slice(&[1i32, 0], [2]);
536    /// assert_eq!(x.i((0, &rows)).shape(), [2, 4]);                 // x[0, rows]
537    /// assert!(x.i(5).is_failed());
538    /// ```
539    #[track_caller]
540    pub fn i(&self, index: impl ArrayIndex) -> Array {
541        let items = items_of(index);
542        let mut ins: SmallVec<[&Array; 4]> = SmallVec::new();
543        ins.push(self);
544        ins.extend(index_arrays(&items));
545        Array::sticky("index", Location::caller(), &ins, || {
546            index_with(self, &items)
547        })
548    }
549
550    /// A functional update of this array at `index` (JAX's `x.at[...]`): finish it with
551    /// [`At::set`] or [`At::add`]. It takes the array by value, so that
552    /// `let x = x.at(..).set(&v);` leaves the update the only owner of `x`'s buffer and the
553    /// write happens in place; `x.clone().at(..)` keeps `x`.
554    ///
555    /// ```
556    /// use nuvec_core::{Array, Ellipsis};
557    ///
558    /// let x = Array::from_slice(&[0.0f32; 6], [2, 3]);
559    /// let x = x.at((.., 1..)).set(1.0);
560    /// let x = x.at((Ellipsis, 0)).add(&Array::from_slice(&[5.0f32, 6.0], [2]));
561    /// assert_eq!(x.to_vec::<f32>().unwrap(), [5.0, 1.0, 1.0, 6.0, 1.0, 1.0]);
562    /// ```
563    pub fn at(self, index: impl ArrayIndex) -> At {
564        At {
565            base: self,
566            items: items_of(index),
567        }
568    }
569
570    /// A copy of this array with the region selected by `index` replaced by `update` (MLX's
571    /// `slice_update`, `a[index] = update`): the borrowing form of [`Array::at`] with
572    /// [`At::set`], which writes in place only when the base is not shared.
573    #[track_caller]
574    pub fn slice_update(&self, update: &Array, index: impl ArrayIndex) -> Array {
575        let items = items_of(index);
576        let mut ins: SmallVec<[&Array; 4]> = SmallVec::from_slice(&[self, update]);
577        ins.extend(index_arrays(&items));
578        Array::sticky("slice_update", Location::caller(), &ins, || {
579            update_at(self, &items, ScalarOrArray::Array(update.clone()), false)
580        })
581    }
582}
583
584/// A pending functional update made by [`Array::at`].
585#[must_use = "an update does nothing until `set` or `add` is called"]
586#[derive(Debug, Clone)]
587pub struct At {
588    base: Array,
589    items: SmallVec<[IndexItem; 4]>,
590}
591
592#[allow(clippy::should_implement_trait)] // `add` is MLX's name; `At` is not a number
593impl At {
594    /// The array with the indexed region replaced by `value` (broadcast to the region and
595    /// cast to the array's dtype; a weak Rust number is allowed), as MLX's `x[...] = v`.
596    /// With repeated index-array positions, one of the values wins.
597    #[track_caller]
598    pub fn set(self, value: impl Into<ScalarOrArray>) -> Array {
599        self.apply("at.set", Location::caller(), value.into(), false)
600    }
601
602    /// The array with `value` added to the indexed region; repeated index-array positions
603    /// accumulate (MLX's `x.at[...].add(v)`).
604    #[track_caller]
605    pub fn add(self, value: impl Into<ScalarOrArray>) -> Array {
606        self.apply("at.add", Location::caller(), value.into(), true)
607    }
608
609    fn apply(
610        self,
611        op: &'static str,
612        at: &'static Location<'static>,
613        value: ScalarOrArray,
614        add: bool,
615    ) -> Array {
616        let At { base, items } = self;
617        let failed = std::iter::once(&base)
618            .chain(index_arrays(&items))
619            .chain(match &value {
620                ScalarOrArray::Array(v) => Some(v),
621                ScalarOrArray::Scalar(_) => None,
622            })
623            .find_map(Array::slot_error);
624        if let Some(error) = failed {
625            return Array::failed(error, base.dtype(), base.dims());
626        }
627        match update_at(&base, &items, value, add) {
628            Ok(out) => out,
629            Err(e) => Array::failed(e.locate(op, at), base.dtype(), base.dims()),
630        }
631    }
632}
633
634/// The common update of index arrays on adjacent axes `k..k + m` (an embedding gradient's
635/// `h.at(&ids).add(&g)`), every other axis whole: the indexed axes merge into one (a view of
636/// a contiguous base) and the update is one scatter along it, with the indices broadcast to
637/// the update. `None` when the index has another form.
638fn update_along_axis(
639    base: &Array,
640    r: &Resolved,
641    value: &Array,
642    add: bool,
643) -> Result<Option<Array>> {
644    let mut adv_dims = Vec::new();
645    for (slot, arg) in r.slots.iter().zip(&r.args) {
646        match slot {
647            Slot::New => return Ok(None),
648            Slot::Dim(d) => {
649                let n = base.dims()[*d] as isize;
650                let whole = arg.step == 1
651                    && arg.start.is_none_or(|s| s == 0 || s <= -n)
652                    && arg.stop.is_none_or(|e| e >= n);
653                if !whole {
654                    return Ok(None);
655                }
656            }
657            Slot::Adv(d, ..) => adv_dims.push(*d),
658        }
659    }
660    let k = adv_dims[0];
661    if adv_dims.iter().enumerate().any(|(i, &d)| d != k + i) {
662        return Ok(None);
663    }
664    let adv = advanced_indices(base, r)?;
665    let (linear, bshape, total) = linear_index(base, &adv)?;
666    let dims = base.dims();
667    let (pre, post) = (&dims[..k], &dims[k + adv_dims.len()..]);
668    // The base with the indexed axes merged, and the update in the indexed shape.
669    let mut merged: Shape = pre.into();
670    merged.push(total);
671    merged.extend_from_slice(post);
672    let flat_base = imp::reshape_to(base, merged)?;
673    let mut region: Shape = pre.into();
674    region.extend_from_slice(&bshape);
675    region.extend_from_slice(post);
676    let m: usize = bshape.iter().product();
677    let mut along: Shape = pre.into();
678    along.push(m);
679    along.extend_from_slice(post);
680    let update = imp::broadcast_to(&imp::astype(value, base.dtype())?, &region)?;
681    let update = imp::reshape_to(&imp::contiguous(&update)?, along.clone())?;
682    let mut index_shape: Shape = Shape::from_elem(1, pre.len());
683    index_shape.push(m);
684    index_shape.extend(std::iter::repeat_n(1, post.len()));
685    let indices = imp::broadcast_to(&imp::reshape_to(&linear, index_shape)?, &along)?;
686    let out = if add {
687        crate::indexing::scatter_add_along_axis(&flat_base, &indices, &update, k as isize)?
688    } else {
689        crate::indexing::put_along_axis(&flat_base, &indices, &update, k as isize)?
690    };
691    Ok(Some(imp::reshape_to(&out, dims.into())?))
692}
693
694/// `base` with the region at `items` replaced by (or, with `add`, increased by) `value`.
695fn update_at(base: &Array, items: &[IndexItem], value: ScalarOrArray, add: bool) -> Result<Array> {
696    let r = resolve(items, base.dims())?;
697    let value = value.into_array(Some(base.dtype()))?;
698    if !r.has_arrays() {
699        // A basic region: `slice_update` (in place when `base` is not shared).
700        let region = imp::slice(base, &r.args)?;
701        let indexed = basic_shape(&r, region.dims());
702        let update = imp::broadcast_to(&imp::astype(&value, base.dtype())?, &indexed)?;
703        let update = imp::reshape_to(&update, region.dims().into())?;
704        let update = if add {
705            imp::add(&region, &update)?
706        } else {
707            update
708        };
709        return imp::slice_update(base, &update, &r.args);
710    }
711    if let Some(out) = update_along_axis(base, &r, &value, add)? {
712        return Ok(out);
713    }
714    // Index arrays on axes that are not adjacent, mixed with partial slices or new axes:
715    // scatter into the flattened array at each element's position, found by indexing the
716    // positions of `base` the same way.
717    let positions_dtype = if base.size() <= i32::MAX as usize {
718        DType::I32
719    } else {
720        DType::I64
721    };
722    let positions = imp::reshape_to(
723        &imp::arange(0.0, base.size() as f64, 1.0, positions_dtype)?,
724        base.dims().into(),
725    )?;
726    let selected = index_with(&positions, items)?;
727    let update = imp::broadcast_to(&imp::astype(&value, base.dtype())?, selected.dims())?;
728    let flat = imp::reshape_to(base, Shape::from_elem(base.size(), 1))?;
729    let n = selected.size();
730    let positions = imp::reshape_to(&selected, Shape::from_elem(n, 1))?;
731    let update = imp::reshape_to(&imp::contiguous(&update)?, Shape::from_elem(n, 1))?;
732    let out = if add {
733        crate::indexing::scatter_add_along_axis(&flat, &positions, &update, 0)?
734    } else {
735        crate::indexing::put_along_axis(&flat, &positions, &update, 0)?
736    };
737    imp::reshape_to(&out, base.dims().into())
738}
739
740#[cfg(test)]
741mod tests {
742    use super::*;
743
744    fn arange(shape: &[usize]) -> Array {
745        let n: usize = shape.iter().product();
746        let data: Vec<i32> = (0..n as i32).collect();
747        Array::from_slice(&data, shape)
748    }
749
750    #[test]
751    fn basic_forms() {
752        let x = arange(&[4, 5, 6]);
753        assert_eq!(x.i(0).shape(), [5, 6]);
754        assert_eq!(x.i(-1).to_vec::<i32>().unwrap()[0], 90);
755        assert_eq!(x.i((.., 1..3)).shape(), [4, 2, 6]);
756        assert_eq!(x.i((Ellipsis, -1)).shape(), [4, 5]);
757        assert_eq!(x.i((.., NewAxis, ..)).shape(), [4, 1, 5, 6]);
758        assert_eq!(x.i((..).step(-1)).to_vec::<i32>().unwrap()[0], 90);
759        assert_eq!(x.i((1.., ..=2, (0..6).step(2))).shape(), [3, 3, 3]);
760        assert_eq!(x.i(1usize..3usize).shape(), [2, 5, 6]);
761        // Views: no gather, no copy.
762        assert!(matches!(
763            x.i((.., 1..3)).primitive_t(),
764            Some(crate::graph::Primitive::Slice { .. })
765        ));
766    }
767
768    #[test]
769    fn invalid_indices() {
770        let x = arange(&[4, 5]);
771        for bad in [
772            x.i(4),
773            x.i(-5),
774            x.i((Ellipsis, 0, Ellipsis)),
775            x.i((0, 0, 0)),
776            x.i((0..2).step(0)),
777        ] {
778            let e = bad.check().unwrap_err();
779            assert!(
780                matches!(e.kind(), Some(crate::OpErrorKind::InvalidIndex(_))),
781                "{e}"
782            );
783        }
784    }
785
786    #[test]
787    fn advanced_placement() {
788        let x = arange(&[2, 3, 4]);
789        let i2 = Array::from_slice(&[0i32, 1], [2]);
790        let j2 = Array::from_slice(&[1i32, 2], [2]);
791        assert_eq!(x.i((&i2, .., &j2)).shape(), [2, 3]);
792        assert_eq!(x.i((.., &i2, &j2)).shape(), [2, 2]);
793        assert_eq!(x.i((&i2, 1)).shape(), [2, 4]);
794        assert_eq!(x.i((1, .., &i2)).shape(), [2, 3]);
795        assert_eq!(x.i((NewAxis, &i2)).shape(), [1, 2, 3, 4]);
796        assert_eq!(x.i((&i2, NewAxis)).shape(), [2, 1, 3, 4]);
797        assert_eq!(x.i((Ellipsis, &i2)).shape(), [2, 3, 2]);
798        assert_eq!(x.i((.., &i2, &j2)).to_vec::<i32>().unwrap(), [1, 6, 13, 18]);
799    }
800
801    #[test]
802    fn updates() {
803        let x = Array::from_slice(&[0i32; 6], [2, 3]);
804        let y = x.clone().at((.., 1..)).set(7);
805        assert_eq!(y.to_vec::<i32>().unwrap(), [0, 7, 7, 0, 7, 7]);
806        let ids = Array::from_slice(&[0i32, 0, 1], [3]);
807        let z = x.at(&ids).add(1);
808        assert_eq!(z.to_vec::<i32>().unwrap(), [2, 2, 2, 1, 1, 1]);
809    }
810}