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