Skip to main content

nuvec_core/ops/
imp.rs

1//! Array operations.
2//!
3//! Following MLX, operations promote operand types and broadcast shapes here, by inserting
4//! `AsType` and `Broadcast` nodes, so primitives always see operands of equal shape and
5//! dtype. Views (`reshape`, `transpose`, `slice`, `broadcast_to`, `expand_dims`,
6//! `squeeze`) share their input's data whenever the layout permits.
7
8use std::ops::{Add, Div, Mul, Neg, Sub};
9use std::panic::Location;
10
11use crate::buffer::Buffer;
12use crate::cast::{Canon, Scalar, with_type};
13use crate::primitive::{ArgReduceOp, BinaryOp, Primitive, ReduceOp, UnaryOp};
14use crate::scalar::{ScalarOperand, ScalarOrArray, resolve};
15use crate::shape::{self, Shape};
16use crate::view::{self, SliceArg};
17use crate::{
18    Array, Category, Complex64, DType, Error, Result, at_least_float, bf16, f16, promote_types,
19};
20
21fn single(primitive: Primitive, inputs: Vec<Array>) -> Result<Array> {
22    let mut outputs = Array::from_op(primitive, inputs)?;
23    Ok(outputs.swap_remove(0))
24}
25
26// ── Element-wise binary ─────────────────────────────────────────────────────
27
28pub(crate) fn binary(op: BinaryOp, a: &Array, b: &Array) -> Result<Array> {
29    let mut dtype = promote_types(a.dtype(), b.dtype());
30    if matches!(op, BinaryOp::Divide | BinaryOp::LogAddExp) {
31        dtype = at_least_float(dtype);
32    }
33    let ordered = matches!(
34        op,
35        BinaryOp::Maximum
36            | BinaryOp::Minimum
37            | BinaryOp::LogAddExp
38            | BinaryOp::Less
39            | BinaryOp::LessEqual
40            | BinaryOp::Greater
41            | BinaryOp::GreaterEqual
42    );
43    if ordered && dtype == DType::Complex64 {
44        return Err(Error::unsupported_dtype(op.name(), dtype));
45    }
46    let shape = view::broadcast_shapes(op.name(), a.dims(), b.dims())?;
47    let a = broadcast_to(&astype(a, dtype)?, &shape)?;
48    let b = broadcast_to(&astype(b, dtype)?, &shape)?;
49    single(Primitive::Binary(op), vec![a, b])
50}
51
52macro_rules! binary_fn {
53    ($($(#[$doc:meta])* $name:ident => $op:ident;)*) => {$(
54        $(#[$doc])*
55        pub(crate) fn $name(a: impl Into<ScalarOrArray>, b: impl Into<ScalarOrArray>) -> Result<Array> {
56            let (a, b) = resolve(a.into(), b.into())?;
57            binary(BinaryOp::$op, &a, &b)
58        }
59    )*};
60}
61
62// Every binary function accepts arrays or Rust numbers for either operand; numbers are weak
63// and adapt to the array operand (see [`ScalarOrArray`]).
64binary_fn! {
65    /// Element-wise `a + b`, with promotion and broadcasting. Integers wrap; for booleans
66    /// this is logical OR.
67    add => Add;
68    /// Element-wise `a - b`, with promotion and broadcasting. Integers wrap; for booleans
69    /// this is logical XOR.
70    subtract => Subtract;
71    /// Element-wise `a * b`, with promotion and broadcasting. Integers wrap; for booleans
72    /// this is logical AND.
73    multiply => Multiply;
74    /// Element-wise true division `a / b`. Integer operands are promoted to floating point.
75    divide => Divide;
76    /// Element-wise maximum; NaN propagates. Not defined for complex numbers.
77    maximum => Maximum;
78    /// Element-wise minimum; NaN propagates. Not defined for complex numbers.
79    minimum => Minimum;
80    /// Element-wise `a == b` as a bool array.
81    equal => Equal;
82    /// Element-wise `a != b` as a bool array.
83    not_equal => NotEqual;
84    /// Element-wise `a < b` as a bool array. Not defined for complex numbers.
85    less => Less;
86    /// Element-wise `a <= b` as a bool array. Not defined for complex numbers.
87    less_equal => LessEqual;
88    /// Element-wise `a > b` as a bool array. Not defined for complex numbers.
89    greater => Greater;
90    /// Element-wise `a >= b` as a bool array. Not defined for complex numbers.
91    greater_equal => GreaterEqual;
92}
93
94// ── Element-wise unary ──────────────────────────────────────────────────────
95
96pub(crate) fn unary(op: UnaryOp, a: &Array) -> Result<Array> {
97    let dtype = if op.is_transcendental() {
98        at_least_float(a.dtype())
99    } else {
100        a.dtype()
101    };
102    let unsupported = match op {
103        UnaryOp::Negative => dtype == DType::Bool,
104        UnaryOp::Abs | UnaryOp::Conjugate => false,
105        _ => dtype == DType::Complex64,
106    };
107    if unsupported {
108        return Err(Error::unsupported_dtype(op.name(), dtype));
109    }
110    single(Primitive::Unary(op), vec![astype(a, dtype)?])
111}
112
113/// Element-wise complex conjugate. Real arrays are returned unchanged.
114pub(crate) fn conjugate(a: &Array) -> Result<Array> {
115    if a.dtype() != DType::Complex64 {
116        return Ok(a.clone());
117    }
118    unary(UnaryOp::Conjugate, a)
119}
120
121/// Element-wise sign: `-1`, `0` or `1` in `a`'s dtype, as MLX's `sign` (`(a > 0) - (a < 0)`,
122/// so NaN gives 0). Unsigned integers and booleans give `a != 0`. Complex numbers are not
123/// supported.
124pub(crate) fn sign(a: &Array) -> Result<Array> {
125    let dtype = a.dtype();
126    match dtype.category() {
127        Category::Complex => Err(Error::unsupported_dtype("sign", dtype)),
128        Category::Bool | Category::Unsigned => astype(&greater(a, 0)?, dtype),
129        _ => subtract(
130            astype(&greater(a, 0)?, dtype)?,
131            astype(&less(a, 0)?, dtype)?,
132        ),
133    }
134}
135
136macro_rules! unary_fn {
137    ($($(#[$doc:meta])* $name:ident => $op:ident;)*) => {$(
138        $(#[$doc])*
139        pub(crate) fn $name(a: &Array) -> Result<Array> {
140            unary(UnaryOp::$op, a)
141        }
142    )*};
143}
144
145unary_fn! {
146    /// Element-wise `-a`. Integers wrap. Not defined for booleans.
147    negative => Negative;
148    /// Element-wise absolute value; the float32 magnitude for complex numbers.
149    abs => Abs;
150    /// Element-wise `e^a`, in at least float32.
151    exp => Exp;
152    /// Element-wise natural logarithm, in at least float32.
153    log => Log;
154    /// Element-wise square root, in at least float32.
155    sqrt => Sqrt;
156    /// Element-wise sine, in at least float32.
157    sin => Sin;
158    /// Element-wise cosine, in at least float32.
159    cos => Cos;
160    /// Element-wise hyperbolic tangent, in at least float32.
161    tanh => Tanh;
162    /// Element-wise logistic function `1 / (1 + e^-a)`, in at least float32.
163    sigmoid => Sigmoid;
164    /// Element-wise inverse error function, in at least float32.
165    erfinv => ErfInv;
166}
167
168/// `log(sum(exp(a)))` over `axes` (all axes if empty), computed stably as MLX does: the
169/// maximum is subtracted first (without gradient), and rows whose maximum is infinite
170/// return that maximum.
171pub(crate) fn logsumexp(a: &Array, axes: &[isize], keepdims: bool) -> Result<Array> {
172    let a = astype(a, at_least_float(a.dtype()))?;
173    if last_axis_only(&a, axes) && crate::fast::imp::on_gpu(a.dtype(), a.size()) {
174        let out = Array::from_op(
175            Primitive::Fast(crate::fast::imp::FastOp::LogSumExp),
176            vec![a],
177        )?;
178        let out = out.into_iter().next().expect("one output");
179        return if keepdims {
180            Ok(out)
181        } else {
182            squeeze(&out, axes)
183        };
184    }
185    let maxval = stop_gradient(&max(&a, axes, true)?)?;
186    let out = log(&sum(&exp(&subtract(&a, &maxval)?)?, axes, true)?)?;
187    let out = add(&out, &maxval)?;
188    let out = where_(equal(&abs(&maxval)?, f64::INFINITY)?, &maxval, &out)?;
189    if keepdims {
190        Ok(out)
191    } else {
192        squeeze(&out, axes)
193    }
194}
195
196/// `exp(a) / sum(exp(a))` over `axes` (all axes if empty), with the maximum subtracted
197/// first (without gradient), as MLX does.
198pub(crate) fn softmax(a: &Array, axes: &[isize]) -> Result<Array> {
199    let a = astype(a, at_least_float(a.dtype()))?;
200    if last_axis_only(&a, axes) && crate::fast::imp::on_gpu(a.dtype(), a.size()) {
201        return single(Primitive::Fast(crate::fast::imp::FastOp::Softmax), vec![a]);
202    }
203    let maxval = stop_gradient(&max(&a, axes, true)?)?;
204    let ex = exp(&subtract(&a, &maxval)?)?;
205    divide(&ex, sum(&ex, axes, true)?)
206}
207
208/// Whether `axes` (all if empty) reduce the last axis and otherwise only axes of size 1:
209/// MLX's condition for its last-axis softmax and log-sum-exp kernels.
210fn last_axis_only(a: &Array, axes: &[isize]) -> bool {
211    let n = a.ndim() as isize;
212    let all: Vec<isize> = if axes.is_empty() {
213        (0..n).collect()
214    } else {
215        axes.to_vec()
216    };
217    n > 0
218        && a.dtype() != DType::Complex64
219        && all.iter().all(|&ax| (-n..n).contains(&ax))
220        && all.iter().any(|&ax| ax.rem_euclid(n) == n - 1)
221        && all
222            .iter()
223            .all(|&ax| ax.rem_euclid(n) == n - 1 || a.dims()[ax.rem_euclid(n) as usize] == 1)
224}
225
226/// Element-wise selection: `a` where `condition` is true, otherwise `b`.
227///
228/// `condition` is converted to bool (non-zero is true); `a` and `b` are promoted to a
229/// common type; all three are broadcast together. Numbers are accepted for any operand
230/// (weak, as in [`ScalarOrArray`]).
231pub(crate) fn where_(
232    condition: impl Into<ScalarOrArray>,
233    a: impl Into<ScalarOrArray>,
234    b: impl Into<ScalarOrArray>,
235) -> Result<Array> {
236    let (a, b) = resolve(a.into(), b.into())?;
237    let condition = condition.into().into_array(None)?;
238    let (condition, a, b) = (&condition, &a, &b);
239    let dtype = promote_types(a.dtype(), b.dtype());
240    let shape = view::broadcast_shapes("where", condition.dims(), a.dims())?;
241    let shape = view::broadcast_shapes("where", &shape, b.dims())?;
242    let condition = broadcast_to(&astype(condition, DType::Bool)?, &shape)?;
243    let a = broadcast_to(&astype(a, dtype)?, &shape)?;
244    let b = broadcast_to(&astype(b, dtype)?, &shape)?;
245    single(Primitive::Select, vec![condition, a, b])
246}
247
248/// Converts the elements to `dtype`. Float-to-integer conversion saturates (NaN becomes
249/// 0); conversion to bool tests for non-zero; complex-to-real keeps the real part.
250pub(crate) fn astype(a: &Array, dtype: DType) -> Result<Array> {
251    if a.dtype() == dtype {
252        return Ok(a.clone());
253    }
254    single(Primitive::AsType(dtype), vec![a.clone()])
255}
256
257// ── Views ───────────────────────────────────────────────────────────────────
258
259/// The same elements in another shape; one dimension may be `-1` (inferred). Shares the
260/// data when the layout permits, otherwise copies.
261pub(crate) fn reshape(a: &Array, shape: &[isize]) -> Result<Array> {
262    let shape = view::infer_shape(a.size(), shape)?;
263    reshape_to(a, shape)
264}
265
266pub(crate) fn reshape_to(a: &Array, shape: Shape) -> Result<Array> {
267    if a.dims() == shape.as_slice() {
268        return Ok(a.clone());
269    }
270    single(Primitive::Reshape(shape.to_vec()), vec![a.clone()])
271}
272
273/// Reorders the axes: output axis `i` is input axis `axes[i]`. Shares the data.
274pub(crate) fn transpose(a: &Array, axes: &[usize]) -> Result<Array> {
275    view::permute(a.dims(), a.strides(), axes)?;
276    if axes.iter().enumerate().all(|(i, &axis)| i == axis) {
277        return Ok(a.clone());
278    }
279    single(Primitive::Transpose(axes.to_vec()), vec![a.clone()])
280}
281
282/// Selects `start:stop:step` in every dimension, with Python semantics (negative indices
283/// count from the end, bounds are clamped, steps may be negative). Shares the data.
284pub(crate) fn slice(a: &Array, args: &[SliceArg]) -> Result<Array> {
285    let dims = view::normalize_slice(a.dims(), args)?;
286    let full = dims
287        .iter()
288        .zip(a.dims())
289        .all(|(d, &n)| d.start == 0 && d.step == 1 && d.len == n);
290    if full {
291        return Ok(a.clone());
292    }
293    let primitive = Primitive::Slice {
294        start: dims.iter().map(|d| d.start).collect(),
295        step: dims.iter().map(|d| d.step).collect(),
296        len: dims.iter().map(|d| d.len).collect(),
297    };
298    single(primitive, vec![a.clone()])
299}
300
301/// `a` unchanged, but no gradient flows through it in [`crate::vjp`], [`crate::jvp`] and
302/// A copy of `a` with the region selected as by [`slice()`] replaced by `update` (MLX's
303/// `slice_update`, `a[...] = update`): `update` broadcasts to the region's shape and takes
304/// `a`'s dtype.
305pub(crate) fn slice_update(a: &Array, update: &Array, args: &[SliceArg]) -> Result<Array> {
306    let dims = view::normalize_slice(a.dims(), args)?;
307    let len: Vec<usize> = dims.iter().map(|d| d.len).collect();
308    let update = broadcast_to(&astype(update, a.dtype())?, &len)?;
309    single(
310        Primitive::SliceUpdate {
311            start: dims.iter().map(|d| d.start).collect(),
312            step: dims.iter().map(|d| d.step).collect(),
313            len,
314        },
315        vec![a.clone(), update],
316    )
317}
318
319/// [`crate::grad`]. Shares the data.
320pub(crate) fn stop_gradient(a: &Array) -> Result<Array> {
321    single(Primitive::StopGradient, vec![a.clone()])
322}
323
324/// Presents `a` as the larger `shape` (NumPy broadcasting rules). Shares the data.
325pub(crate) fn broadcast_to(a: &Array, shape: &[usize]) -> Result<Array> {
326    if a.dims() == shape {
327        return Ok(a.clone());
328    }
329    single(Primitive::Broadcast(shape.to_vec()), vec![a.clone()])
330}
331
332/// Inserts a dimension of size 1 at `axis` (negative counts from the end of the result).
333/// Shares the data.
334pub(crate) fn expand_dims(a: &Array, axis: isize) -> Result<Array> {
335    let axis = view::normalize_axis(axis, a.ndim() + 1)?;
336    let mut shape: Shape = a.dims().into();
337    shape.insert(axis, 1);
338    reshape_to(a, shape)
339}
340
341/// Removes dimensions of size 1: the given `axes`, or all of them if `axes` is empty.
342/// Shares the data.
343pub(crate) fn squeeze(a: &Array, axes: &[isize]) -> Result<Array> {
344    let mut remove = vec![axes.is_empty(); a.ndim()];
345    for &axis in axes {
346        let i = view::normalize_axis(axis, a.ndim())?;
347        if a.dims()[i] != 1 {
348            return Err(Error::InvalidArgument(format!(
349                "cannot squeeze axis {axis} of size {} in shape {:?}",
350                a.dims()[i],
351                a.dims()
352            )));
353        }
354        remove[i] = true;
355    }
356    let shape = a
357        .dims()
358        .iter()
359        .zip(&remove)
360        .filter(|&(&d, &r)| !(r && d == 1))
361        .map(|(&d, _)| d)
362        .collect();
363    reshape_to(a, shape)
364}
365
366// ── Reductions ──────────────────────────────────────────────────────────────
367
368/// Sorted, unique axes; an empty list means every axis.
369fn normalize_axes(a: &Array, axes: &[isize]) -> Result<Vec<usize>> {
370    if axes.is_empty() {
371        return Ok((0..a.ndim()).collect());
372    }
373    let mut out = axes
374        .iter()
375        .map(|&axis| view::normalize_axis(axis, a.ndim()))
376        .collect::<Result<Vec<_>>>()?;
377    out.sort_unstable();
378    if let Some(w) = out.windows(2).find(|w| w[0] == w[1]) {
379        return Err(Error::invalid_axis("axes", w[0] as i64, a.ndim()));
380    }
381    Ok(out)
382}
383
384/// `out` with the (size-1) `axes` removed, unless `keepdims`.
385fn finish_reduction(out: Array, axes: &[usize], keepdims: bool) -> Result<Array> {
386    if keepdims {
387        return Ok(out);
388    }
389    let shape = out
390        .dims()
391        .iter()
392        .enumerate()
393        .filter(|(d, _)| !axes.contains(d))
394        .map(|(_, &n)| n)
395        .collect();
396    reshape_to(&out, shape)
397}
398
399fn reduction(
400    op: ReduceOp,
401    a: &Array,
402    axes: &[isize],
403    keepdims: bool,
404    dtype: DType,
405) -> Result<Array> {
406    let axes = normalize_axes(a, axes)?;
407    if matches!(op, ReduceOp::Max | ReduceOp::Min) {
408        if a.dtype() == DType::Complex64 {
409            return Err(Error::unsupported_dtype(op.name(), a.dtype()));
410        }
411        if axes.iter().any(|&d| a.dims()[d] == 0) {
412            return Err(Error::InvalidArgument(format!(
413                "{}: cannot reduce a zero-length axis (no identity)",
414                op.name()
415            )));
416        }
417    }
418    let a = astype(a, dtype)?;
419    if axes.is_empty() {
420        return Ok(a); // a 0-D array: nothing to reduce
421    }
422    let out = single(
423        Primitive::Reduce {
424            op,
425            axes: axes.clone(),
426        },
427        vec![a],
428    )?;
429    finish_reduction(out, &axes, keepdims)
430}
431
432/// The type sums and products accumulate into, as in MLX: small integers widen.
433pub(crate) fn widened(dtype: DType) -> DType {
434    match (dtype.category(), dtype.size()) {
435        (Category::Bool, _) => DType::I32,
436        (Category::Signed, 1..=4) => DType::I32,
437        (Category::Signed, _) => DType::I64,
438        (Category::Unsigned, 1..=4) => DType::U32,
439        (Category::Unsigned, _) => DType::U64,
440        _ => dtype,
441    }
442}
443
444/// Sum over `axes` (empty = all). Integers and bools widen to 32 or 64 bits (as in MLX);
445/// half and single precision accumulate in double precision.
446pub(crate) fn sum(a: &Array, axes: &[isize], keepdims: bool) -> Result<Array> {
447    reduction(ReduceOp::Sum, a, axes, keepdims, widened(a.dtype()))
448}
449
450/// Product over `axes` (empty = all), with the same widening as [`sum`].
451pub(crate) fn prod(a: &Array, axes: &[isize], keepdims: bool) -> Result<Array> {
452    reduction(ReduceOp::Prod, a, axes, keepdims, widened(a.dtype()))
453}
454
455/// Maximum over `axes` (empty = all); NaN propagates. Zero-length axes are an error.
456pub(crate) fn max(a: &Array, axes: &[isize], keepdims: bool) -> Result<Array> {
457    reduction(ReduceOp::Max, a, axes, keepdims, a.dtype())
458}
459
460/// Minimum over `axes` (empty = all); NaN propagates. Zero-length axes are an error.
461pub(crate) fn min(a: &Array, axes: &[isize], keepdims: bool) -> Result<Array> {
462    reduction(ReduceOp::Min, a, axes, keepdims, a.dtype())
463}
464
465/// Whether every element over `axes` (empty = all) is non-zero.
466pub(crate) fn all(a: &Array, axes: &[isize], keepdims: bool) -> Result<Array> {
467    reduction(ReduceOp::And, a, axes, keepdims, DType::Bool)
468}
469
470/// Whether any element over `axes` (empty = all) is non-zero.
471pub(crate) fn any(a: &Array, axes: &[isize], keepdims: bool) -> Result<Array> {
472    reduction(ReduceOp::Or, a, axes, keepdims, DType::Bool)
473}
474
475/// Mean over `axes` (empty = all), in at least float32. Empty reductions are NaN.
476pub(crate) fn mean(a: &Array, axes: &[isize], keepdims: bool) -> Result<Array> {
477    let dtype = at_least_float(a.dtype());
478    let count: usize = normalize_axes(a, axes)?
479        .iter()
480        .map(|&d| a.dims()[d])
481        .product();
482    let scale = astype(&Array::from(1.0 / count as f64), dtype)?;
483    multiply(&sum(a, axes, keepdims)?, &scale)
484}
485
486fn arg_reduction(op: ArgReduceOp, a: &Array, axis: Option<isize>, keepdims: bool) -> Result<Array> {
487    if a.dtype() == DType::Complex64 {
488        return Err(Error::unsupported_dtype(op.name(), a.dtype()));
489    }
490    let Some(axis) = axis else {
491        // Over the flattened array; with keepdims the result has one size-1 dimension per
492        // input dimension.
493        let flat = arg_reduction(op, &reshape(a, &[-1])?, Some(0), false)?;
494        return if keepdims {
495            reshape_to(&flat, smallvec::smallvec![1; a.ndim()])
496        } else {
497            Ok(flat)
498        };
499    };
500    let axis = view::normalize_axis(axis, a.ndim())?;
501    if a.dims()[axis] == 0 {
502        return Err(Error::InvalidArgument(format!(
503            "{}: cannot reduce a zero-length axis",
504            op.name()
505        )));
506    }
507    let out = single(Primitive::ArgReduce { op, axis }, vec![a.clone()])?;
508    finish_reduction(out, &[axis], keepdims)
509}
510
511/// Index (uint32) of the maximum along `axis`, or over the flattened array if `None`.
512/// The first occurrence wins; the first NaN wins over any number.
513pub(crate) fn argmax(a: &Array, axis: Option<isize>, keepdims: bool) -> Result<Array> {
514    arg_reduction(ArgReduceOp::Max, a, axis, keepdims)
515}
516
517/// Index (uint32) of the minimum along `axis`, or over the flattened array if `None`.
518/// The first occurrence wins; the first NaN wins over any number.
519pub(crate) fn argmin(a: &Array, axis: Option<isize>, keepdims: bool) -> Result<Array> {
520    arg_reduction(ArgReduceOp::Min, a, axis, keepdims)
521}
522
523// ── Matrix multiplication ───────────────────────────────────────────────────
524
525/// Matrix product of `[..., M, K]` and `[..., K, N]`, broadcasting batch dimensions.
526///
527/// A 1-D `a` is a row vector and a 1-D `b` a column vector; the inserted dimension is
528/// removed from the result. The promoted type must be floating point or complex.
529pub(crate) fn matmul(a: &Array, b: &Array) -> Result<Array> {
530    product(a, b, None)
531}
532
533/// `alpha · (a @ b) + beta · c`, with `c` broadcast to the product's shape, as MLX's
534/// `addmm`. On the GPU it is one kernel (the GEMM's epilogue), so `Linear` layers use it for
535/// their bias.
536pub(crate) fn addmm(c: &Array, a: &Array, b: &Array, alpha: f32, beta: f32) -> Result<Array> {
537    product(a, b, Some((c, alpha, beta)))
538}
539
540/// Matrix product with NumPy's rules for 1-D operands and batch broadcasting, optionally
541/// fused with `alpha·(·) + beta·c`.
542fn product(a: &Array, b: &Array, c: Option<(&Array, f32, f32)>) -> Result<Array> {
543    let op = if c.is_some() { "addmm" } else { "matmul" };
544    if a.ndim() == 0 || b.ndim() == 0 {
545        return Err(Error::InvalidArgument(format!(
546            "{op}: operands need at least one dimension"
547        )));
548    }
549    let mut dtype = promote_types(a.dtype(), b.dtype());
550    if let Some((c, ..)) = c {
551        dtype = promote_types(dtype, c.dtype());
552    }
553    if !matches!(dtype.category(), Category::Float | Category::Complex) {
554        return Err(Error::unsupported_dtype(op, dtype));
555    }
556    let a2 = if a.ndim() == 1 {
557        expand_dims(a, 0)?
558    } else {
559        a.clone()
560    };
561    let b2 = if b.ndim() == 1 {
562        expand_dims(b, -1)?
563    } else {
564        b.clone()
565    };
566    let (sa, sb) = (a2.dims(), b2.dims());
567    if sa[sa.len() - 1] != sb[sb.len() - 2] {
568        return Err(Error::shape_mismatch(op, a.dims(), b.dims()));
569    }
570    let (a2, b2) = (astype(&a2, dtype)?, astype(&b2, dtype)?);
571    let (na, nb) = (a2.ndim(), b2.ndim());
572    let batch = view::broadcast_shapes(op, &a2.dims()[..na - 2], &b2.dims()[..nb - 2])?;
573    let (m, n) = (a2.dims()[na - 2], b2.dims()[nb - 1]);
574    let mut expanded: Shape = batch.clone();
575    expanded.extend_from_slice(&[m, n]);
576    // Dimensions inserted for 1-D operands, removed from the result.
577    let mut drop = Vec::new();
578    if a.ndim() == 1 {
579        drop.push(expanded.len() as isize - 2);
580    }
581    if b.ndim() == 1 {
582        drop.push(expanded.len() as isize - 1);
583    }
584    // `c`, broadcast to the result and then to the expanded product shape.
585    let c_full = match c {
586        Some((c, alpha, beta)) => {
587            let final_shape: Vec<usize> = expanded
588                .iter()
589                .enumerate()
590                .filter(|(i, _)| !drop.contains(&(*i as isize)))
591                .map(|(_, &d)| d)
592                .collect();
593            let mut cc = broadcast_to(&astype(c, dtype)?, &final_shape)?;
594            for &d in &drop {
595                cc = expand_dims(&cc, d)?;
596            }
597            Some((broadcast_to(&cc, &expanded)?, alpha, beta))
598        }
599        None => None,
600    };
601    let record = |x: Array, y: Array, z: Option<(Array, f32, f32)>| match z {
602        None => single(Primitive::Matmul, vec![x, y]),
603        Some((z, alpha, beta)) => single(
604            Primitive::AddMM {
605                alpha: alpha.to_bits(),
606                beta: beta.to_bits(),
607            },
608            vec![x, y, z],
609        ),
610    };
611    let out = if na > 2 && nb == 2 {
612        // One large product instead of a batch of small ones (as in MLX).
613        let k = a2.dims()[na - 1];
614        let rows = a2
615            .size()
616            .checked_div(k)
617            .unwrap_or_else(|| a2.dims()[..na - 1].iter().product());
618        let flat_c = match c_full {
619            Some((z, alpha, beta)) => {
620                Some((reshape_to(&z, smallvec::smallvec![rows, n])?, alpha, beta))
621            }
622            None => None,
623        };
624        let flat = record(
625            reshape_to(&a2, smallvec::smallvec![rows, k])?,
626            b2.clone(),
627            flat_c,
628        )?;
629        reshape_to(&flat, expanded)?
630    } else {
631        let full = |x: &Array, nd: usize| -> Result<Array> {
632            let mut shape = batch.clone();
633            shape.extend_from_slice(&x.dims()[nd - 2..]);
634            broadcast_to(x, &shape)
635        };
636        record(full(&a2, na)?, full(&b2, nb)?, c_full)?
637    };
638    if drop.is_empty() {
639        Ok(out)
640    } else {
641        squeeze(&out, &drop)
642    }
643}
644
645// ── Creation ────────────────────────────────────────────────────────────────
646
647/// An array of zeros.
648pub(crate) fn zeros(shape: &[usize], dtype: DType) -> Result<Array> {
649    let buffer = Buffer::try_zeroed(shape::checked_nbytes(shape, dtype.size())?)?;
650    Array::from_buffer(buffer, dtype, shape)
651}
652
653/// An array of ones.
654pub(crate) fn ones(shape: &[usize], dtype: DType) -> Result<Array> {
655    filled(shape, dtype, |_| Canon::Int(1))
656}
657
658/// Evenly spaced values `start, start + step, …` below `stop` (above it for negative
659/// steps), converted to `dtype`. The length is `ceil((stop - start) / step)`, as in MLX.
660pub(crate) fn arange(start: f64, stop: f64, step: f64, dtype: DType) -> Result<Array> {
661    let invalid = |msg: &str| Err(Error::InvalidArgument(format!("arange: {msg}")));
662    if dtype == DType::Bool {
663        return Err(Error::unsupported_dtype("arange", dtype));
664    }
665    if start.is_nan() || stop.is_nan() || step.is_nan() || start.is_infinite() || stop.is_infinite()
666    {
667        return invalid("cannot compute the length from NaN or infinite bounds");
668    }
669    if step == 0.0 {
670        return invalid("step must be non-zero");
671    }
672    let len = if step.is_infinite() {
673        usize::from((step > 0.0 && start < stop) || (step < 0.0 && start > stop))
674    } else {
675        let real = ((stop - start) / step).ceil();
676        if real > i32::MAX as f64 {
677            return invalid("maximum size exceeded");
678        }
679        real.max(0.0) as usize
680    };
681    // `i * step` would be NaN for i = 0 and an infinite step.
682    filled(&[len], dtype, |i| {
683        Canon::Float(if i == 0 {
684            start
685        } else {
686            start + i as f64 * step
687        })
688    })
689}
690
691/// An array whose element `i` (row-major) is `value(i)` converted to `dtype`.
692pub(crate) fn filled(
693    shape: &[usize],
694    dtype: DType,
695    value: impl Fn(usize) -> Canon,
696) -> Result<Array> {
697    let mut buffer = Buffer::try_alloc(shape::checked_nbytes(shape, dtype.size())?)?;
698    with_type!(dtype, T => {
699        for (i, x) in buffer.typed_mut::<T>().iter_mut().enumerate() {
700            *x = T::from_canon(value(i));
701        }
702    });
703    Array::from_buffer(buffer, dtype, shape)
704}
705
706// ── Methods ─────────────────────────────────────────────────────────────────
707
708impl Array {
709    /// Converts the elements to `dtype`; see [`astype`].
710    pub(crate) fn try_astype(&self, dtype: DType) -> Result<Array> {
711        astype(self, dtype)
712    }
713
714    /// The same elements in another shape; see [`reshape`].
715    pub(crate) fn try_reshape(&self, shape: &[isize]) -> Result<Array> {
716        reshape(self, shape)
717    }
718}
719
720// ── Operators ───────────────────────────────────────────────────────────────
721
722// Operators cannot return a `Result`. An invalid expression yields a failed array that
723// carries the error (located at the operator) instead of panicking: the same sticky model
724// as every operation.
725
726macro_rules! operator {
727    ($trait:ident, $method:ident, $op:ident) => {
728        impl $trait<&Array> for &Array {
729            type Output = Array;
730            #[track_caller]
731            fn $method(self, rhs: &Array) -> Array {
732                Array::sticky(stringify!($op), Location::caller(), &[self, rhs], || {
733                    $op(self, rhs)
734                })
735            }
736        }
737        impl $trait<Array> for &Array {
738            type Output = Array;
739            #[track_caller]
740            fn $method(self, rhs: Array) -> Array {
741                Array::sticky(stringify!($op), Location::caller(), &[self, &rhs], || {
742                    $op(self, &rhs)
743                })
744            }
745        }
746        impl $trait<&Array> for Array {
747            type Output = Array;
748            #[track_caller]
749            fn $method(self, rhs: &Array) -> Array {
750                Array::sticky(stringify!($op), Location::caller(), &[&self, rhs], || {
751                    $op(&self, rhs)
752                })
753            }
754        }
755        impl $trait<Array> for Array {
756            type Output = Array;
757            #[track_caller]
758            fn $method(self, rhs: Array) -> Array {
759                Array::sticky(stringify!($op), Location::caller(), &[&self, &rhs], || {
760                    $op(&self, &rhs)
761                })
762            }
763        }
764    };
765}
766
767operator!(Add, add, add);
768operator!(Sub, sub, subtract);
769operator!(Mul, mul, multiply);
770operator!(Div, div, divide);
771
772/// Operators with a weak Rust number on the right: `&x * 2.0`.
773macro_rules! scalar_rhs_operator {
774    ($trait:ident, $method:ident, $op:ident) => {
775        impl<S: ScalarOperand> $trait<S> for &Array {
776            type Output = Array;
777            #[track_caller]
778            fn $method(self, rhs: S) -> Array {
779                Array::sticky(stringify!($op), Location::caller(), &[self], || {
780                    $op(self, rhs.into())
781                })
782            }
783        }
784        impl<S: ScalarOperand> $trait<S> for Array {
785            type Output = Array;
786            #[track_caller]
787            fn $method(self, rhs: S) -> Array {
788                Array::sticky(stringify!($op), Location::caller(), &[&self], || {
789                    $op(&self, rhs.into())
790                })
791            }
792        }
793    };
794}
795
796scalar_rhs_operator!(Add, add, add);
797scalar_rhs_operator!(Sub, sub, subtract);
798scalar_rhs_operator!(Mul, mul, multiply);
799scalar_rhs_operator!(Div, div, divide);
800
801/// Operators with a weak Rust number on the left: `1.0 - &x`.
802///
803/// These impls cannot be generic (orphan rule), so there is one per literal kind: integer
804/// literals resolve to `i32` and float literals to `f64` only while no other integer or
805/// float impl competes. Other number types go on the right or through the functions
806/// (`subtract(x_f32, &a)`); since numbers are weak, the result is the same.
807macro_rules! scalar_lhs_operator {
808    ($trait:ident, $method:ident, $op:ident; $($t:ty),*) => {$(
809        impl $trait<&Array> for $t {
810            type Output = Array;
811            #[track_caller]
812            fn $method(self, rhs: &Array) -> Array {
813                Array::sticky(stringify!($op), Location::caller(), &[rhs], || $op(self, rhs))
814            }
815        }
816        impl $trait<Array> for $t {
817            type Output = Array;
818            #[track_caller]
819            fn $method(self, rhs: Array) -> Array {
820                Array::sticky(stringify!($op), Location::caller(), &[&rhs], || $op(self, &rhs))
821            }
822        }
823    )*};
824}
825
826macro_rules! scalar_lhs_operators {
827    ($($t:ty),*) => {
828        scalar_lhs_operator!(Add, add, add; $($t),*);
829        scalar_lhs_operator!(Sub, sub, subtract; $($t),*);
830        scalar_lhs_operator!(Mul, mul, multiply; $($t),*);
831        scalar_lhs_operator!(Div, div, divide; $($t),*);
832    };
833}
834
835scalar_lhs_operators!(bool, i32, f64, f16, bf16, Complex64);
836
837impl Neg for &Array {
838    type Output = Array;
839    #[track_caller]
840    fn neg(self) -> Array {
841        Array::sticky("negative", Location::caller(), &[self], || negative(self))
842    }
843}
844
845impl Neg for Array {
846    type Output = Array;
847    #[track_caller]
848    fn neg(self) -> Array {
849        Array::sticky("negative", Location::caller(), &[&self], || negative(&self))
850    }
851}
852
853/// `a` with each element repeated `repeats` times along `axis` (of the flattened array
854/// without one), as MLX's `repeat`.
855pub(crate) fn repeat(a: &Array, repeats: usize, axis: Option<isize>) -> Result<Array> {
856    let (a, axis) = match axis {
857        Some(axis) => (a.clone(), view::normalize_axis(axis, a.ndim())?),
858        None => (reshape(a, &[-1])?, 0),
859    };
860    let mut wide: Vec<usize> = a.dims().to_vec();
861    wide.insert(axis + 1, repeats);
862    let mut out: Vec<isize> = a.dims().iter().map(|&d| d as isize).collect();
863    out[axis] *= repeats as isize;
864    reshape(
865        &broadcast_to(&expand_dims(&a, axis as isize + 1)?, &wide)?,
866        &out,
867    )
868}
869
870/// `a` in row-major memory: itself if it already is, else a copy (MLX's `contiguous`).
871/// The body of [`crate::ops::copy`] (the `Copy` primitive).
872pub(crate) fn copy(a: &Array) -> Result<Array> {
873    crate::primitive::copy(a)
874}
875
876pub(crate) fn contiguous(a: &Array) -> Result<Array> {
877    if a.flags().row_contiguous {
878        return Ok(a.clone());
879    }
880    let shape: Vec<isize> = a.dims().iter().map(|&d| d as isize).collect();
881    reshape(&reshape(a, &[-1])?, &shape)
882}
883
884#[cfg(test)]
885mod tests {
886    use super::*;
887    use crate::buffer::live;
888
889    /// Live buffers created while running `f` (after evaluating its result).
890    fn allocations(f: impl FnOnce() -> Array) -> isize {
891        let before = live::count();
892        let out = f();
893        out.eval().unwrap();
894        live::count() - before
895    }
896
897    #[test]
898    fn views_share_data() {
899        let base = arange(0.0, 24.0, 1.0, DType::F32)
900            .unwrap()
901            .try_reshape(&[2, 3, 4])
902            .unwrap();
903        base.eval().unwrap();
904        assert_eq!(allocations(|| base.try_reshape(&[6, 4]).unwrap()), 0);
905        assert_eq!(allocations(|| base.t()), 0);
906        assert_eq!(
907            allocations(|| slice(
908                &base,
909                &[(..).into(), SliceArg::new(None, None, -2), (1..3).into()]
910            )
911            .unwrap()),
912            0
913        );
914        assert_eq!(
915            allocations(|| broadcast_to(&base, &[5, 2, 3, 4]).unwrap()),
916            0
917        );
918        assert_eq!(allocations(|| expand_dims(&base, 1).unwrap()), 0);
919        assert_eq!(
920            allocations(|| squeeze(&expand_dims(&base, -1).unwrap(), &[]).unwrap()),
921            0
922        );
923        // A transposed layout cannot be flattened without copying.
924        assert_eq!(allocations(|| base.t().try_reshape(&[-1]).unwrap()), 1);
925    }
926
927    #[test]
928    fn noop_views_record_nothing() {
929        let a = Array::from_slice(&[1u8, 2, 3, 4], [2, 2]);
930        assert!(a.try_reshape(&[2, 2]).unwrap().primitive_t().is_none());
931        assert!(transpose(&a, &[0, 1]).unwrap().primitive_t().is_none());
932        assert!(
933            slice(&a, &[SliceArg::full(), SliceArg::full()])
934                .unwrap()
935                .primitive_t()
936                .is_none()
937        );
938        assert!(astype(&a, DType::U8).unwrap().primitive_t().is_none());
939        assert!(broadcast_to(&a, &[2, 2]).unwrap().primitive_t().is_none());
940    }
941
942    #[test]
943    fn view_flags() {
944        let a = zeros(&[2, 3], DType::F32).unwrap();
945        let t = a.t();
946        assert!(t.flags().col_contiguous && !t.flags().row_contiguous);
947        assert_eq!(t.strides(), &[1, 3]);
948        let b = broadcast_to(&a, &[4, 2, 3]).unwrap();
949        assert_eq!(b.strides(), &[0, 3, 1]);
950        assert!(!b.flags().contiguous);
951    }
952}