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