Skip to main content

nuvec_core/
ops.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 (D-019). 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};
9
10use crate::buffer::Buffer;
11use crate::cast::{Canon, Scalar, with_type};
12use crate::primitive::{ArgReduceOp, BinaryOp, Primitive, ReduceOp, UnaryOp};
13use crate::shape::{self, Shape};
14use crate::view::{self, SliceArg};
15use crate::{Array, Category, DType, Error, Result, at_least_float, promote_types};
16
17fn single(primitive: Primitive, inputs: Vec<Array>) -> Result<Array> {
18    let mut outputs = Array::from_op(primitive, inputs)?;
19    Ok(outputs.swap_remove(0))
20}
21
22/// Returns the error of the first input that has already failed, so that chained
23/// operations report the original cause.
24fn check_inputs(inputs: &[&Array]) -> Result<()> {
25    match inputs.iter().find_map(|a| a.error()) {
26        Some(err) => Err(err),
27        None => Ok(()),
28    }
29}
30
31// ── Element-wise binary ─────────────────────────────────────────────────────
32
33fn binary(op: BinaryOp, a: &Array, b: &Array) -> Result<Array> {
34    check_inputs(&[a, b])?;
35    let mut dtype = promote_types(a.dtype(), b.dtype());
36    if op == BinaryOp::Divide {
37        dtype = at_least_float(dtype);
38    }
39    let ordered = matches!(
40        op,
41        BinaryOp::Maximum
42            | BinaryOp::Minimum
43            | BinaryOp::Less
44            | BinaryOp::LessEqual
45            | BinaryOp::Greater
46            | BinaryOp::GreaterEqual
47    );
48    if ordered && dtype == DType::Complex64 {
49        return Err(Error::UnsupportedDType {
50            op: op.name(),
51            dtype,
52        });
53    }
54    let shape = view::broadcast_shapes(op.name(), a.shape(), b.shape())?;
55    let a = broadcast_to(&astype(a, dtype)?, &shape)?;
56    let b = broadcast_to(&astype(b, dtype)?, &shape)?;
57    single(Primitive::Binary(op), vec![a, b])
58}
59
60macro_rules! binary_fn {
61    ($($(#[$doc:meta])* $name:ident => $op:ident;)*) => {$(
62        $(#[$doc])*
63        pub fn $name(a: &Array, b: &Array) -> Result<Array> {
64            binary(BinaryOp::$op, a, b)
65        }
66    )*};
67}
68
69binary_fn! {
70    /// Element-wise `a + b`, with promotion and broadcasting. Integers wrap; for booleans
71    /// this is logical OR.
72    add => Add;
73    /// Element-wise `a - b`, with promotion and broadcasting. Integers wrap; for booleans
74    /// this is logical XOR.
75    subtract => Subtract;
76    /// Element-wise `a * b`, with promotion and broadcasting. Integers wrap; for booleans
77    /// this is logical AND.
78    multiply => Multiply;
79    /// Element-wise true division `a / b`. Integer operands are promoted to floating point.
80    divide => Divide;
81    /// Element-wise maximum; NaN propagates. Not defined for complex numbers.
82    maximum => Maximum;
83    /// Element-wise minimum; NaN propagates. Not defined for complex numbers.
84    minimum => Minimum;
85    /// Element-wise `a == b` as a bool array.
86    equal => Equal;
87    /// Element-wise `a != b` as a bool array.
88    not_equal => NotEqual;
89    /// Element-wise `a < b` as a bool array. Not defined for complex numbers.
90    less => Less;
91    /// Element-wise `a <= b` as a bool array. Not defined for complex numbers.
92    less_equal => LessEqual;
93    /// Element-wise `a > b` as a bool array. Not defined for complex numbers.
94    greater => Greater;
95    /// Element-wise `a >= b` as a bool array. Not defined for complex numbers.
96    greater_equal => GreaterEqual;
97}
98
99// ── Element-wise unary ──────────────────────────────────────────────────────
100
101fn unary(op: UnaryOp, a: &Array) -> Result<Array> {
102    check_inputs(&[a])?;
103    let dtype = if op.is_transcendental() {
104        at_least_float(a.dtype())
105    } else {
106        a.dtype()
107    };
108    let unsupported = match op {
109        UnaryOp::Negative => dtype == DType::Bool,
110        UnaryOp::Abs => false,
111        _ => dtype == DType::Complex64,
112    };
113    if unsupported {
114        return Err(Error::UnsupportedDType {
115            op: op.name(),
116            dtype,
117        });
118    }
119    single(Primitive::Unary(op), vec![astype(a, dtype)?])
120}
121
122macro_rules! unary_fn {
123    ($($(#[$doc:meta])* $name:ident => $op:ident;)*) => {$(
124        $(#[$doc])*
125        pub fn $name(a: &Array) -> Result<Array> {
126            unary(UnaryOp::$op, a)
127        }
128    )*};
129}
130
131unary_fn! {
132    /// Element-wise `-a`. Integers wrap. Not defined for booleans.
133    negative => Negative;
134    /// Element-wise absolute value; the float32 magnitude for complex numbers.
135    abs => Abs;
136    /// Element-wise `e^a`, in at least float32.
137    exp => Exp;
138    /// Element-wise natural logarithm, in at least float32.
139    log => Log;
140    /// Element-wise square root, in at least float32.
141    sqrt => Sqrt;
142    /// Element-wise sine, in at least float32.
143    sin => Sin;
144    /// Element-wise cosine, in at least float32.
145    cos => Cos;
146    /// Element-wise hyperbolic tangent, in at least float32.
147    tanh => Tanh;
148    /// Element-wise logistic function `1 / (1 + e^-a)`, in at least float32.
149    sigmoid => Sigmoid;
150}
151
152/// Element-wise selection: `a` where `condition` is true, otherwise `b`.
153///
154/// `condition` is converted to bool (non-zero is true); `a` and `b` are promoted to a
155/// common type; all three are broadcast together.
156pub fn where_(condition: &Array, a: &Array, b: &Array) -> Result<Array> {
157    check_inputs(&[condition, a, b])?;
158    let dtype = promote_types(a.dtype(), b.dtype());
159    let shape = view::broadcast_shapes("where", condition.shape(), a.shape())?;
160    let shape = view::broadcast_shapes("where", &shape, b.shape())?;
161    let condition = broadcast_to(&astype(condition, DType::Bool)?, &shape)?;
162    let a = broadcast_to(&astype(a, dtype)?, &shape)?;
163    let b = broadcast_to(&astype(b, dtype)?, &shape)?;
164    single(Primitive::Select, vec![condition, a, b])
165}
166
167/// Converts the elements to `dtype`. Float-to-integer conversion saturates (NaN becomes
168/// 0); conversion to bool tests for non-zero; complex-to-real keeps the real part.
169pub fn astype(a: &Array, dtype: DType) -> Result<Array> {
170    if a.dtype() == dtype {
171        return Ok(a.clone());
172    }
173    single(Primitive::AsType(dtype), vec![a.clone()])
174}
175
176// ── Views ───────────────────────────────────────────────────────────────────
177
178/// The same elements in another shape; one dimension may be `-1` (inferred). Shares the
179/// data when the layout permits, otherwise copies.
180pub fn reshape(a: &Array, shape: &[isize]) -> Result<Array> {
181    let shape = view::infer_shape(a.size(), shape)?;
182    reshape_to(a, shape)
183}
184
185fn reshape_to(a: &Array, shape: Shape) -> Result<Array> {
186    if a.shape() == shape.as_slice() {
187        return Ok(a.clone());
188    }
189    single(Primitive::Reshape(shape.to_vec()), vec![a.clone()])
190}
191
192/// Reorders the axes: output axis `i` is input axis `axes[i]`. Shares the data.
193pub fn transpose(a: &Array, axes: &[usize]) -> Result<Array> {
194    view::permute(a.shape(), a.strides(), axes)?;
195    if axes.iter().enumerate().all(|(i, &axis)| i == axis) {
196        return Ok(a.clone());
197    }
198    single(Primitive::Transpose(axes.to_vec()), vec![a.clone()])
199}
200
201/// Selects `start:stop:step` in every dimension, with Python semantics (negative indices
202/// count from the end, bounds are clamped, steps may be negative). Shares the data.
203pub fn slice(a: &Array, args: &[SliceArg]) -> Result<Array> {
204    let dims = view::normalize_slice(a.shape(), args)?;
205    let full = dims
206        .iter()
207        .zip(a.shape())
208        .all(|(d, &n)| d.start == 0 && d.step == 1 && d.len == n);
209    if full {
210        return Ok(a.clone());
211    }
212    let primitive = Primitive::Slice {
213        start: dims.iter().map(|d| d.start).collect(),
214        step: dims.iter().map(|d| d.step).collect(),
215        len: dims.iter().map(|d| d.len).collect(),
216    };
217    single(primitive, vec![a.clone()])
218}
219
220/// Presents `a` as the larger `shape` (NumPy broadcasting rules). Shares the data.
221pub fn broadcast_to(a: &Array, shape: &[usize]) -> Result<Array> {
222    if a.shape() == shape {
223        return Ok(a.clone());
224    }
225    single(Primitive::Broadcast(shape.to_vec()), vec![a.clone()])
226}
227
228/// Inserts a dimension of size 1 at `axis` (negative counts from the end of the result).
229/// Shares the data.
230pub fn expand_dims(a: &Array, axis: isize) -> Result<Array> {
231    let axis = view::normalize_axis(axis, a.ndim() + 1)?;
232    let mut shape: Shape = a.shape().into();
233    shape.insert(axis, 1);
234    reshape_to(a, shape)
235}
236
237/// Removes dimensions of size 1: the given `axes`, or all of them if `axes` is empty.
238/// Shares the data.
239pub fn squeeze(a: &Array, axes: &[isize]) -> Result<Array> {
240    let mut remove = vec![axes.is_empty(); a.ndim()];
241    for &axis in axes {
242        let i = view::normalize_axis(axis, a.ndim())?;
243        if a.shape()[i] != 1 {
244            return Err(Error::InvalidArgument(format!(
245                "cannot squeeze axis {axis} of size {} in shape {:?}",
246                a.shape()[i],
247                a.shape()
248            )));
249        }
250        remove[i] = true;
251    }
252    let shape = a
253        .shape()
254        .iter()
255        .zip(&remove)
256        .filter(|&(&d, &r)| !(r && d == 1))
257        .map(|(&d, _)| d)
258        .collect();
259    reshape_to(a, shape)
260}
261
262// ── Reductions ──────────────────────────────────────────────────────────────
263
264/// Sorted, unique axes; an empty list means every axis.
265fn normalize_axes(a: &Array, axes: &[isize]) -> Result<Vec<usize>> {
266    if axes.is_empty() {
267        return Ok((0..a.ndim()).collect());
268    }
269    let mut out = axes
270        .iter()
271        .map(|&axis| view::normalize_axis(axis, a.ndim()))
272        .collect::<Result<Vec<_>>>()?;
273    out.sort_unstable();
274    if out.windows(2).any(|w| w[0] == w[1]) {
275        return Err(Error::InvalidArgument(format!(
276            "duplicate axes in {axes:?}"
277        )));
278    }
279    Ok(out)
280}
281
282/// `out` with the (size-1) `axes` removed, unless `keepdims`.
283fn finish_reduction(out: Array, axes: &[usize], keepdims: bool) -> Result<Array> {
284    if keepdims {
285        return Ok(out);
286    }
287    let shape = out
288        .shape()
289        .iter()
290        .enumerate()
291        .filter(|(d, _)| !axes.contains(d))
292        .map(|(_, &n)| n)
293        .collect();
294    reshape_to(&out, shape)
295}
296
297fn reduction(
298    op: ReduceOp,
299    a: &Array,
300    axes: &[isize],
301    keepdims: bool,
302    dtype: DType,
303) -> Result<Array> {
304    check_inputs(&[a])?;
305    let axes = normalize_axes(a, axes)?;
306    if matches!(op, ReduceOp::Max | ReduceOp::Min) {
307        if a.dtype() == DType::Complex64 {
308            return Err(Error::UnsupportedDType {
309                op: op.name(),
310                dtype: a.dtype(),
311            });
312        }
313        if axes.iter().any(|&d| a.shape()[d] == 0) {
314            return Err(Error::InvalidArgument(format!(
315                "{}: cannot reduce a zero-length axis (no identity)",
316                op.name()
317            )));
318        }
319    }
320    let a = astype(a, dtype)?;
321    if axes.is_empty() {
322        return Ok(a); // a 0-D array: nothing to reduce
323    }
324    let out = single(
325        Primitive::Reduce {
326            op,
327            axes: axes.clone(),
328        },
329        vec![a],
330    )?;
331    finish_reduction(out, &axes, keepdims)
332}
333
334/// The type sums and products accumulate into, as in MLX: small integers widen.
335fn widened(dtype: DType) -> DType {
336    match (dtype.category(), dtype.size()) {
337        (Category::Bool, _) => DType::I32,
338        (Category::Signed, 1..=4) => DType::I32,
339        (Category::Signed, _) => DType::I64,
340        (Category::Unsigned, 1..=4) => DType::U32,
341        (Category::Unsigned, _) => DType::U64,
342        _ => dtype,
343    }
344}
345
346/// Sum over `axes` (empty = all). Integers and bools widen to 32 or 64 bits (as in MLX);
347/// half and single precision accumulate in double precision.
348pub fn sum(a: &Array, axes: &[isize], keepdims: bool) -> Result<Array> {
349    reduction(ReduceOp::Sum, a, axes, keepdims, widened(a.dtype()))
350}
351
352/// Product over `axes` (empty = all), with the same widening as [`sum`].
353pub fn prod(a: &Array, axes: &[isize], keepdims: bool) -> Result<Array> {
354    reduction(ReduceOp::Prod, a, axes, keepdims, widened(a.dtype()))
355}
356
357/// Maximum over `axes` (empty = all); NaN propagates. Zero-length axes are an error.
358pub fn max(a: &Array, axes: &[isize], keepdims: bool) -> Result<Array> {
359    reduction(ReduceOp::Max, a, axes, keepdims, a.dtype())
360}
361
362/// Minimum over `axes` (empty = all); NaN propagates. Zero-length axes are an error.
363pub fn min(a: &Array, axes: &[isize], keepdims: bool) -> Result<Array> {
364    reduction(ReduceOp::Min, a, axes, keepdims, a.dtype())
365}
366
367/// Whether every element over `axes` (empty = all) is non-zero.
368pub fn all(a: &Array, axes: &[isize], keepdims: bool) -> Result<Array> {
369    reduction(ReduceOp::And, a, axes, keepdims, DType::Bool)
370}
371
372/// Whether any element over `axes` (empty = all) is non-zero.
373pub fn any(a: &Array, axes: &[isize], keepdims: bool) -> Result<Array> {
374    reduction(ReduceOp::Or, a, axes, keepdims, DType::Bool)
375}
376
377/// Mean over `axes` (empty = all), in at least float32. Empty reductions are NaN.
378pub fn mean(a: &Array, axes: &[isize], keepdims: bool) -> Result<Array> {
379    let dtype = at_least_float(a.dtype());
380    let count: usize = normalize_axes(a, axes)?
381        .iter()
382        .map(|&d| a.shape()[d])
383        .product();
384    let scale = astype(&Array::scalar(1.0 / count as f64), dtype)?;
385    multiply(&sum(a, axes, keepdims)?, &scale)
386}
387
388fn arg_reduction(op: ArgReduceOp, a: &Array, axis: Option<isize>, keepdims: bool) -> Result<Array> {
389    check_inputs(&[a])?;
390    if a.dtype() == DType::Complex64 {
391        return Err(Error::UnsupportedDType {
392            op: op.name(),
393            dtype: a.dtype(),
394        });
395    }
396    let Some(axis) = axis else {
397        // Over the flattened array; with keepdims the result has one size-1 dimension per
398        // input dimension.
399        let flat = arg_reduction(op, &reshape(a, &[-1])?, Some(0), false)?;
400        return if keepdims {
401            reshape_to(&flat, smallvec::smallvec![1; a.ndim()])
402        } else {
403            Ok(flat)
404        };
405    };
406    let axis = view::normalize_axis(axis, a.ndim())?;
407    if a.shape()[axis] == 0 {
408        return Err(Error::InvalidArgument(format!(
409            "{}: cannot reduce a zero-length axis",
410            op.name()
411        )));
412    }
413    let out = single(Primitive::ArgReduce { op, axis }, vec![a.clone()])?;
414    finish_reduction(out, &[axis], keepdims)
415}
416
417/// Index (uint32) of the maximum along `axis`, or over the flattened array if `None`.
418/// The first occurrence wins; the first NaN wins over any number.
419pub fn argmax(a: &Array, axis: Option<isize>, keepdims: bool) -> Result<Array> {
420    arg_reduction(ArgReduceOp::Max, a, axis, keepdims)
421}
422
423/// Index (uint32) of the minimum along `axis`, or over the flattened array if `None`.
424/// The first occurrence wins; the first NaN wins over any number.
425pub fn argmin(a: &Array, axis: Option<isize>, keepdims: bool) -> Result<Array> {
426    arg_reduction(ArgReduceOp::Min, a, axis, keepdims)
427}
428
429// ── Matrix multiplication ───────────────────────────────────────────────────
430
431/// Matrix product of `[..., M, K]` and `[..., K, N]`, broadcasting batch dimensions.
432///
433/// A 1-D `a` is a row vector and a 1-D `b` a column vector; the inserted dimension is
434/// removed from the result. The promoted type must be floating point or complex.
435pub fn matmul(a: &Array, b: &Array) -> Result<Array> {
436    check_inputs(&[a, b])?;
437    if a.ndim() == 0 || b.ndim() == 0 {
438        return Err(Error::InvalidArgument(
439            "matmul: operands need at least one dimension".into(),
440        ));
441    }
442    let dtype = promote_types(a.dtype(), b.dtype());
443    if !matches!(dtype.category(), Category::Float | Category::Complex) {
444        return Err(Error::UnsupportedDType {
445            op: "matmul",
446            dtype,
447        });
448    }
449    let a2 = if a.ndim() == 1 {
450        expand_dims(a, 0)?
451    } else {
452        a.clone()
453    };
454    let b2 = if b.ndim() == 1 {
455        expand_dims(b, -1)?
456    } else {
457        b.clone()
458    };
459    let (sa, sb) = (a2.shape(), b2.shape());
460    if sa[sa.len() - 1] != sb[sb.len() - 2] {
461        return Err(Error::ShapeMismatch {
462            op: "matmul",
463            lhs: a.shape().to_vec(),
464            rhs: b.shape().to_vec(),
465        });
466    }
467    let (a2, b2) = (astype(&a2, dtype)?, astype(&b2, dtype)?);
468    let out = if a2.ndim() > 2 && b2.ndim() == 2 {
469        // One large product instead of a batch of small ones (as in MLX).
470        let k = a2.shape()[a2.ndim() - 1];
471        let rows = a2
472            .size()
473            .checked_div(k)
474            .unwrap_or_else(|| a2.shape()[..a2.ndim() - 1].iter().product());
475        let flat = single(
476            Primitive::Matmul,
477            vec![reshape_to(&a2, smallvec::smallvec![rows, k])?, b2.clone()],
478        )?;
479        let mut shape: Shape = a2.shape().into();
480        *shape.last_mut().expect("ndim > 2") = b2.shape()[1];
481        reshape_to(&flat, shape)?
482    } else {
483        let (na, nb) = (a2.ndim(), b2.ndim());
484        let batch = view::broadcast_shapes("matmul", &a2.shape()[..na - 2], &b2.shape()[..nb - 2])?;
485        let full = |x: &Array, nd: usize| -> Result<Array> {
486            let mut shape = batch.clone();
487            shape.extend_from_slice(&x.shape()[nd - 2..]);
488            broadcast_to(x, &shape)
489        };
490        single(Primitive::Matmul, vec![full(&a2, na)?, full(&b2, nb)?])?
491    };
492    // Remove the dimensions inserted for 1-D operands.
493    let mut drop = Vec::new();
494    if a.ndim() == 1 {
495        drop.push(out.ndim() as isize - 2);
496    }
497    if b.ndim() == 1 {
498        drop.push(-1);
499    }
500    if drop.is_empty() {
501        Ok(out)
502    } else {
503        squeeze(&out, &drop)
504    }
505}
506
507// ── Creation ────────────────────────────────────────────────────────────────
508
509/// An array of zeros.
510pub fn zeros(shape: &[usize], dtype: DType) -> Result<Array> {
511    let buffer = Buffer::try_zeroed(shape::checked_nbytes(shape, dtype.size())?)?;
512    Array::from_buffer(buffer, dtype, shape)
513}
514
515/// An array of ones.
516pub fn ones(shape: &[usize], dtype: DType) -> Result<Array> {
517    filled(shape, dtype, |_| Canon::Int(1))
518}
519
520/// Evenly spaced values `start, start + step, …` below `stop` (above it for negative
521/// steps), converted to `dtype`. The length is `ceil((stop - start) / step)`, as in MLX.
522pub fn arange(start: f64, stop: f64, step: f64, dtype: DType) -> Result<Array> {
523    let invalid = |msg: &str| Err(Error::InvalidArgument(format!("arange: {msg}")));
524    if dtype == DType::Bool {
525        return Err(Error::UnsupportedDType {
526            op: "arange",
527            dtype,
528        });
529    }
530    if start.is_nan() || stop.is_nan() || step.is_nan() || start.is_infinite() || stop.is_infinite()
531    {
532        return invalid("cannot compute the length from NaN or infinite bounds");
533    }
534    if step == 0.0 {
535        return invalid("step must be non-zero");
536    }
537    let len = if step.is_infinite() {
538        usize::from((step > 0.0 && start < stop) || (step < 0.0 && start > stop))
539    } else {
540        let real = ((stop - start) / step).ceil();
541        if real > i32::MAX as f64 {
542            return invalid("maximum size exceeded");
543        }
544        real.max(0.0) as usize
545    };
546    // `i * step` would be NaN for i = 0 and an infinite step.
547    filled(&[len], dtype, |i| {
548        Canon::Float(if i == 0 {
549            start
550        } else {
551            start + i as f64 * step
552        })
553    })
554}
555
556/// An array whose element `i` (row-major) is `value(i)` converted to `dtype`.
557fn filled(shape: &[usize], dtype: DType, value: impl Fn(usize) -> Canon) -> Result<Array> {
558    let mut buffer = Buffer::try_alloc(shape::checked_nbytes(shape, dtype.size())?)?;
559    with_type!(dtype, T => {
560        for (i, x) in buffer.typed_mut::<T>().iter_mut().enumerate() {
561            *x = T::from_canon(value(i));
562        }
563    });
564    Array::from_buffer(buffer, dtype, shape)
565}
566
567// ── Methods ─────────────────────────────────────────────────────────────────
568
569impl Array {
570    /// Converts the elements to `dtype`; see [`astype`].
571    pub fn astype(&self, dtype: DType) -> Result<Array> {
572        astype(self, dtype)
573    }
574
575    /// The same elements in another shape; see [`reshape`].
576    pub fn reshape(&self, shape: &[isize]) -> Result<Array> {
577        reshape(self, shape)
578    }
579
580    /// The array with all axes reversed (the matrix transpose for 2-D arrays).
581    pub fn t(&self) -> Result<Array> {
582        let axes: Vec<usize> = (0..self.ndim()).rev().collect();
583        transpose(self, &axes)
584    }
585}
586
587// ── Operators ───────────────────────────────────────────────────────────────
588
589/// Operators cannot return a `Result`. An invalid expression yields an array whose
590/// evaluation (or readback) returns the error, instead of panicking.
591fn or_failed(result: Result<Array>, lhs: &Array) -> Array {
592    result.unwrap_or_else(|err| Array::failed(err, lhs.dtype(), lhs.shape()))
593}
594
595macro_rules! operator {
596    ($trait:ident, $method:ident, $op:ident) => {
597        impl $trait<&Array> for &Array {
598            type Output = Array;
599            fn $method(self, rhs: &Array) -> Array {
600                or_failed($op(self, rhs), self)
601            }
602        }
603        impl $trait<Array> for &Array {
604            type Output = Array;
605            fn $method(self, rhs: Array) -> Array {
606                or_failed($op(self, &rhs), self)
607            }
608        }
609        impl $trait<&Array> for Array {
610            type Output = Array;
611            fn $method(self, rhs: &Array) -> Array {
612                or_failed($op(&self, rhs), &self)
613            }
614        }
615        impl $trait<Array> for Array {
616            type Output = Array;
617            fn $method(self, rhs: Array) -> Array {
618                or_failed($op(&self, &rhs), &self)
619            }
620        }
621    };
622}
623
624operator!(Add, add, add);
625operator!(Sub, sub, subtract);
626operator!(Mul, mul, multiply);
627operator!(Div, div, divide);
628
629impl Neg for &Array {
630    type Output = Array;
631    fn neg(self) -> Array {
632        or_failed(negative(self), self)
633    }
634}
635
636impl Neg for Array {
637    type Output = Array;
638    fn neg(self) -> Array {
639        or_failed(negative(&self), &self)
640    }
641}
642
643#[cfg(test)]
644mod tests {
645    use super::*;
646    use crate::buffer::live;
647
648    /// Live buffers created while running `f` (after evaluating its result).
649    fn allocations(f: impl FnOnce() -> Array) -> isize {
650        let before = live::count();
651        let out = f();
652        out.eval().unwrap();
653        live::count() - before
654    }
655
656    #[test]
657    fn views_share_data() {
658        let base = arange(0.0, 24.0, 1.0, DType::F32)
659            .unwrap()
660            .reshape(&[2, 3, 4])
661            .unwrap();
662        base.eval().unwrap();
663        assert_eq!(allocations(|| base.reshape(&[6, 4]).unwrap()), 0);
664        assert_eq!(allocations(|| base.t().unwrap()), 0);
665        assert_eq!(
666            allocations(|| slice(
667                &base,
668                &[(..).into(), SliceArg::new(None, None, -2), (1..3).into()]
669            )
670            .unwrap()),
671            0
672        );
673        assert_eq!(
674            allocations(|| broadcast_to(&base, &[5, 2, 3, 4]).unwrap()),
675            0
676        );
677        assert_eq!(allocations(|| expand_dims(&base, 1).unwrap()), 0);
678        assert_eq!(
679            allocations(|| squeeze(&expand_dims(&base, -1).unwrap(), &[]).unwrap()),
680            0
681        );
682        // A transposed layout cannot be flattened without copying.
683        assert_eq!(allocations(|| base.t().unwrap().reshape(&[-1]).unwrap()), 1);
684    }
685
686    #[test]
687    fn noop_views_record_nothing() {
688        let a = Array::from_slice(&[1u8, 2, 3, 4], &[2, 2]).unwrap();
689        assert!(a.reshape(&[2, 2]).unwrap().primitive().is_none());
690        assert!(transpose(&a, &[0, 1]).unwrap().primitive().is_none());
691        assert!(
692            slice(&a, &[SliceArg::full(), SliceArg::full()])
693                .unwrap()
694                .primitive()
695                .is_none()
696        );
697        assert!(astype(&a, DType::U8).unwrap().primitive().is_none());
698        assert!(broadcast_to(&a, &[2, 2]).unwrap().primitive().is_none());
699    }
700
701    #[test]
702    fn view_flags() {
703        let a = zeros(&[2, 3], DType::F32).unwrap();
704        let t = a.t().unwrap();
705        assert!(t.flags().col_contiguous && !t.flags().row_contiguous);
706        assert_eq!(t.strides(), &[1, 3]);
707        let b = broadcast_to(&a, &[4, 2, 3]).unwrap();
708        assert_eq!(b.strides(), &[0, 3, 1]);
709        assert!(!b.flags().contiguous);
710    }
711}