Skip to main content

nuvec_core/
primitive.rs

1//! What a graph node computes.
2//!
3//! Primitives form a closed enum: equality and hashing — the basis for
4//! common-subexpression elimination — are derived, and every per-primitive rule (output
5//! inference here; derivative and batching rules in their modules) is an exhaustive `match`.
6//!
7//! Primitives require operands of equal shape and dtype. Promotion and broadcasting happen
8//! in the ops layer, which inserts `AsType` and `Broadcast` nodes.
9
10use smallvec::{SmallVec, smallvec};
11
12use crate::custom::CustomCall;
13use crate::shape::{Shape, Strides};
14use crate::view::{self, DimSlice};
15use crate::{Array, Category, DType, Error, Result};
16
17/// An element-wise function of one operand.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19#[non_exhaustive]
20pub enum UnaryOp {
21    /// `-x`.
22    Negative,
23    /// `|x|`; the magnitude for complex numbers.
24    Abs,
25    /// `e^x`.
26    Exp,
27    /// Natural logarithm.
28    Log,
29    /// Square root.
30    Sqrt,
31    /// Sine.
32    Sin,
33    /// Cosine.
34    Cos,
35    /// Hyperbolic tangent.
36    Tanh,
37    /// `1 / (1 + e^-x)`.
38    Sigmoid,
39    /// The complex conjugate (identity for real numbers).
40    Conjugate,
41    /// The inverse error function.
42    ErfInv,
43}
44
45impl UnaryOp {
46    /// Whether the function computes in floating point (integer inputs are promoted).
47    pub(crate) fn is_transcendental(self) -> bool {
48        !matches!(self, Self::Negative | Self::Abs | Self::Conjugate)
49    }
50
51    pub(crate) fn name(self) -> &'static str {
52        match self {
53            Self::Negative => "negative",
54            Self::Abs => "abs",
55            Self::Exp => "exp",
56            Self::Log => "log",
57            Self::Sqrt => "sqrt",
58            Self::Sin => "sin",
59            Self::Cos => "cos",
60            Self::Tanh => "tanh",
61            Self::Sigmoid => "sigmoid",
62            Self::Conjugate => "conjugate",
63            Self::ErfInv => "erfinv",
64        }
65    }
66}
67
68/// An element-wise function of two operands.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
70#[non_exhaustive]
71pub enum BinaryOp {
72    /// `a + b`.
73    Add,
74    /// `a - b`.
75    Subtract,
76    /// `a * b`.
77    Multiply,
78    /// `a / b` (true division).
79    Divide,
80    /// The larger operand; NaN propagates.
81    Maximum,
82    /// The smaller operand; NaN propagates.
83    Minimum,
84    /// `log(exp(a) + exp(b))`, computed stably (MLX's `logaddexp`); NaN propagates.
85    LogAddExp,
86    /// `floor(a / b)` (MLX's `floor_divide`): for floats the floor of the rounded quotient,
87    /// for integers the quotient rounded toward negative infinity (0 for a zero divisor,
88    /// which MLX leaves undefined).
89    FloorDivide,
90    /// `a` to the power `b` (MLX's `power`): `pow` for floats, repeated squaring for
91    /// integers (0 for a negative exponent, which MLX leaves undefined).
92    Power,
93    /// `a == b`.
94    Equal,
95    /// `a != b`.
96    NotEqual,
97    /// `a < b`.
98    Less,
99    /// `a <= b`.
100    LessEqual,
101    /// `a > b`.
102    Greater,
103    /// `a >= b`.
104    GreaterEqual,
105}
106
107impl BinaryOp {
108    /// Whether the result is boolean.
109    pub(crate) fn is_comparison(self) -> bool {
110        matches!(
111            self,
112            Self::Equal
113                | Self::NotEqual
114                | Self::Less
115                | Self::LessEqual
116                | Self::Greater
117                | Self::GreaterEqual
118        )
119    }
120
121    pub(crate) fn name(self) -> &'static str {
122        match self {
123            Self::Add => "add",
124            Self::Subtract => "subtract",
125            Self::Multiply => "multiply",
126            Self::Divide => "divide",
127            Self::Maximum => "maximum",
128            Self::Minimum => "minimum",
129            Self::LogAddExp => "logaddexp",
130            Self::FloorDivide => "floor_divide",
131            Self::Power => "power",
132            Self::Equal => "equal",
133            Self::NotEqual => "not_equal",
134            Self::Less => "less",
135            Self::LessEqual => "less_equal",
136            Self::Greater => "greater",
137            Self::GreaterEqual => "greater_equal",
138        }
139    }
140}
141
142/// A reduction over a set of axes.
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
144#[non_exhaustive]
145pub enum ReduceOp {
146    /// Sum (identity 0).
147    Sum,
148    /// Product (identity 1).
149    Prod,
150    /// Maximum; NaN propagates. No identity.
151    Max,
152    /// Minimum; NaN propagates. No identity.
153    Min,
154    /// Logical AND of bools (identity true).
155    And,
156    /// Logical OR of bools (identity false).
157    Or,
158}
159
160impl ReduceOp {
161    pub(crate) fn name(self) -> &'static str {
162        match self {
163            Self::Sum => "sum",
164            Self::Prod => "prod",
165            Self::Max => "max",
166            Self::Min => "min",
167            Self::And => "all",
168            Self::Or => "any",
169        }
170    }
171}
172
173/// The position of an extreme value along one axis.
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
175#[non_exhaustive]
176pub enum ArgReduceOp {
177    /// Index of the maximum (first occurrence; first NaN wins).
178    Max,
179    /// Index of the minimum (first occurrence; first NaN wins).
180    Min,
181}
182
183impl ArgReduceOp {
184    pub(crate) fn name(self) -> &'static str {
185        match self {
186            Self::Max => "argmax",
187            Self::Min => "argmin",
188        }
189    }
190}
191
192/// The combination a cumulative scan applies.
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
194#[non_exhaustive]
195pub enum ScanOp {
196    /// Running sum (`cumsum`).
197    Sum,
198    /// Running product (`cumprod`).
199    Prod,
200    /// Running maximum (`cummax`); NaN propagates.
201    Max,
202    /// Running minimum (`cummin`); NaN propagates.
203    Min,
204    /// Running `log(Σ exp)` (`logcumsumexp`).
205    LogAddExp,
206}
207
208impl ScanOp {
209    pub(crate) fn name(self) -> &'static str {
210        match self {
211            Self::Sum => "cumsum",
212            Self::Prod => "cumprod",
213            Self::Max => "cummax",
214            Self::Min => "cummin",
215            Self::LogAddExp => "logcumsumexp",
216        }
217    }
218}
219
220/// How a scatter combines an update with the value it lands on.
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
222#[non_exhaustive]
223pub enum ScatterReduce {
224    /// The update replaces the value (with repeated indices, one of the updates wins).
225    None,
226    /// The update is added to the value (repeated indices accumulate).
227    Sum,
228}
229
230/// An operation recorded in the graph, with its parameters.
231#[derive(Debug, Clone, PartialEq, Eq, Hash)]
232#[non_exhaustive]
233pub enum Primitive {
234    /// Element-wise function of one operand.
235    Unary(UnaryOp),
236    /// Element-wise function of two operands of equal shape and dtype.
237    Binary(BinaryOp),
238    /// `where(condition, a, b)`: a boolean condition selects from `a` or `b`.
239    Select,
240    /// Conversion to another element type.
241    AsType(DType),
242    /// The same elements in another shape. A view when the layout permits, else a copy.
243    Reshape(Vec<usize>),
244    /// The input without its size-1 dimensions at `axes` (sorted, unique). A view. Unlike
245    /// `Reshape`, the output shape follows the input's (MLX's `Squeeze`, which shapeless
246    /// `compile` replays on any shape).
247    Squeeze(Vec<usize>),
248    /// The input with size-1 dimensions inserted at the output's `axes` (sorted, unique). A
249    /// view (MLX's `ExpandDims`).
250    ExpandDims(Vec<usize>),
251    /// Axes reordered: output axis `i` is input axis `axes[i]`. A view.
252    Transpose(Vec<usize>),
253    /// A normalised slice per dimension. A view.
254    Slice {
255        /// First index per dimension.
256        start: Vec<usize>,
257        /// Step per dimension (non-zero, may be negative).
258        step: Vec<isize>,
259        /// Number of selected indices per dimension (the output shape).
260        len: Vec<usize>,
261    },
262    /// Presented as a larger shape with zero strides. A view.
263    Broadcast(Vec<usize>),
264    /// The input unchanged, but no gradient flows through it. A view.
265    StopGradient,
266    /// The input's bytes reinterpreted as `dtype` (MLX's `view`): the same shape when the
267    /// element sizes agree, else the last dimension scaled by their ratio. A view of a
268    /// row-contiguous input (or of equal element sizes); otherwise a copy, reinterpreted.
269    View(DType),
270    /// Floats as E4M3 bytes (uint8), rounding to nearest even and saturating at ±448 (MLX's
271    /// `to_fp8`).
272    ToFp8,
273    /// E4M3 bytes (uint8) as `dtype` floats (MLX's `from_fp8`).
274    FromFp8(DType),
275    /// Sorted `uint32` indices `[..., M]` to int32 `[..., groups]`: where each group's rows
276    /// start along the last axis (the lower bound of `g`; MLX's `gather_mm_offsets`), which
277    /// the grouped gather kernels walk.
278    SortedOffsets {
279        /// The number of groups (weight batches).
280        groups: usize,
281    },
282    /// `(base, update)`: a copy of `base` whose region selected as by `Slice` is replaced by
283    /// `update` (of shape `len`).
284    SliceUpdate {
285        /// First index per dimension.
286        start: Vec<usize>,
287        /// Step per dimension (non-zero, may be negative).
288        step: Vec<isize>,
289        /// Number of selected indices per dimension (the update's shape).
290        len: Vec<usize>,
291    },
292    /// Reduction over sorted, unique `axes`; reduced dimensions become size 1.
293    Reduce {
294        /// The reduction.
295        op: ReduceOp,
296        /// Reduced axes, sorted and unique.
297        axes: Vec<usize>,
298    },
299    /// Index of an extreme value along `axis` (uint32); the axis becomes size 1.
300    ArgReduce {
301        /// Maximum or minimum.
302        op: ArgReduceOp,
303        /// The reduced axis.
304        axis: usize,
305    },
306    /// `(src, indices)`: the elements of `src` at `indices` along `axis` (MLX's
307    /// `GatherAxis`, `take_along_axis`). `indices` (integers) has `src`'s shape except
308    /// along `axis` and gives the output's shape. Negative indices count from the end;
309    /// out-of-range indices are clamped.
310    GatherAxis {
311        /// The indexed axis.
312        axis: usize,
313    },
314    /// `(dst, indices, updates)`: a copy of `dst` with `updates` placed at `indices` along
315    /// `axis` (MLX's `ScatterAxis`, `put_along_axis`). `indices` (integers) and `updates`
316    /// share a shape, which is `dst`'s except along `axis`. Indices as in `GatherAxis`.
317    ScatterAxis {
318        /// The indexed axis.
319        axis: usize,
320        /// How updates combine with `dst`.
321        reduce: ScatterReduce,
322    },
323    /// The input sorted along `axis` (ascending, stable, NaN last).
324    Sort {
325        /// The sorted axis.
326        axis: usize,
327    },
328    /// The indices (uint32) that sort the input along `axis`, as [`Primitive::Sort`].
329    ArgSort {
330        /// The sorted axis.
331        axis: usize,
332    },
333    /// A cumulative scan along `axis`.
334    Scan {
335        /// The combination.
336        op: ScanOp,
337        /// The scanned axis.
338        axis: usize,
339        /// Scan from the end of the axis.
340        reverse: bool,
341        /// Whether each output includes its own element (else it holds the elements
342        /// before it, starting from the combination's identity).
343        inclusive: bool,
344    },
345    /// The inputs (one dtype, equal shapes except along `axis`) joined along `axis`.
346    Concatenate {
347        /// The joined axis.
348        axis: usize,
349    },
350    /// Batched matrix product of `[B..., M, K]` and `[B..., K, N]` with equal batch
351    /// dimensions.
352    Matmul,
353    /// `alpha · (a @ b) + beta · c` for inputs `(a, b, c)`: a `Matmul` whose `c` has the
354    /// output's shape. The factors are stored as `f32` bits (so the enum stays `Eq + Hash`).
355    AddMM {
356        /// `f32::to_bits` of alpha.
357        alpha: u32,
358        /// `f32::to_bits` of beta.
359        beta: u32,
360    },
361    /// Quantization of the last axis in groups (MLX's `quantize`). Outputs the packed codes
362    /// (uint32, `bits` per value) and the scales: for affine, scales and biases in the
363    /// input's dtype; for the floating-point modes, uint8 scale codes.
364    Quantize {
365        /// The mode, group size and bits.
366        quant: crate::Quant,
367    },
368    /// `(w, scales[, biases])`: the packed values restored in `dtype` (affine: `code · scale +
369    /// bias` in the scales' dtype, which `dtype` equals; the floating-point modes have no
370    /// biases).
371    Dequantize {
372        /// The mode, group size and bits.
373        quant: crate::Quant,
374        /// The output dtype.
375        dtype: DType,
376    },
377    /// `(x, w, scales[, biases])`: `x · dequantize(w)ᵀ` (`transpose`) or `x · dequantize(w)`.
378    /// `w` is two-dimensional, or has `x`'s batch dimensions. Biases exist for affine only.
379    QuantizedMatmul {
380        /// The mode, group size and bits.
381        quant: crate::Quant,
382        /// Whether the dequantized matrix is used transposed (MLX's default).
383        transpose: bool,
384    },
385    /// A fused operation with a dedicated GPU kernel ([`crate::fast`]).
386    Fast(crate::fast::imp::FastOp),
387    /// Element-wise operations fused into one generated kernel ([`crate::graph::Fused`]), as
388    /// `compile` builds them. The inputs share the output's shape.
389    Compiled(crate::fused::Fused),
390    /// Random bits from threefry2x32 keys of shape `[..., 2]` (uint32): the output has shape
391    /// `keys.shape[..-1] + shape` and `width` bytes per element (MLX's layout).
392    RandomBits {
393        /// The shape drawn per key.
394        shape: Vec<usize>,
395        /// Bytes per element: 1, 2 or 4.
396        width: u8,
397    },
398    /// A call of a [`crate::custom_function`]: the inputs are the primals followed by the
399    /// function's outputs, which this node presents unchanged (views); transforms use the
400    /// function's custom rules.
401    Custom(CustomCall),
402    /// A custom Metal kernel ([`crate::fast::MetalKernel`]).
403    MetalKernel(crate::kernel::KernelCall),
404    /// A row-contiguous copy of the input into a new buffer. Never simplified away (as a
405    /// same-type conversion or identity view would be).
406    Copy,
407    /// `(x, deps...)`: `x` unchanged (sharing its data), evaluated only after the
408    /// `n_deps` other inputs. Gradients flow to `x` only. It orders work, as MLX's
409    /// `depends`: a recomputation that depends on cotangents cannot be merged with the
410    /// forward pass.
411    Depends {
412        /// The number of dependencies after `x`.
413        n_deps: usize,
414    },
415    /// A collective over a distributed group (by id; [`crate::distributed`]).
416    Collective {
417        /// The operation and its parameters.
418        op: crate::distributed::CollectiveOp,
419        /// The group's id.
420        group: usize,
421    },
422    /// Test-only two-output primitive: `(a + b, a * b)`.
423    #[cfg(test)]
424    AddMultiply,
425}
426
427/// The dtype, shape and (for views) strides of one output of a primitive.
428#[derive(Debug, Clone, PartialEq, Eq)]
429pub(crate) struct OutputSpec {
430    pub(crate) dtype: DType,
431    pub(crate) shape: Shape,
432    /// `None` means row-contiguous.
433    pub(crate) strides: Option<Strides>,
434}
435
436impl Primitive {
437    /// `(alpha, beta)` of an `AddMM`.
438    pub(crate) fn addmm_factors(&self) -> (f32, f32) {
439        match self {
440            Self::AddMM { alpha, beta } => (f32::from_bits(*alpha), f32::from_bits(*beta)),
441            _ => (1.0, 0.0),
442        }
443    }
444
445    /// The operation's name, as used in error messages.
446    pub fn name(&self) -> &'static str {
447        match self {
448            Self::Unary(op) => op.name(),
449            Self::Binary(op) => op.name(),
450            Self::Select => "where",
451            Self::AsType(_) => "astype",
452            Self::Reshape(_) => "reshape",
453            Self::Squeeze(_) => "squeeze",
454            Self::ExpandDims(_) => "expand_dims",
455            Self::Transpose(_) => "transpose",
456            Self::Slice { .. } => "slice",
457            Self::Broadcast(_) => "broadcast_to",
458            Self::StopGradient => "stop_gradient",
459            Self::View(_) => "view",
460            Self::ToFp8 => "to_fp8",
461            Self::SortedOffsets { .. } => "sorted_offsets",
462            Self::FromFp8(_) => "from_fp8",
463            Self::SliceUpdate { .. } => "slice_update",
464            Self::Reduce { op, .. } => op.name(),
465            Self::ArgReduce { op, .. } => op.name(),
466            Self::GatherAxis { .. } => "take_along_axis",
467            Self::ScatterAxis {
468                reduce: ScatterReduce::None,
469                ..
470            } => "put_along_axis",
471            Self::ScatterAxis { .. } => "scatter_add_along_axis",
472            Self::Concatenate { .. } => "concatenate",
473            Self::Sort { .. } => "sort",
474            Self::ArgSort { .. } => "argsort",
475            Self::Scan { op, .. } => op.name(),
476            Self::Matmul => "matmul",
477            Self::AddMM { .. } => "addmm",
478            Self::RandomBits { .. } => "random_bits",
479            Self::Fast(op) => op.name(),
480            Self::Compiled(_) => "compiled",
481            Self::Quantize { .. } => "quantize",
482            Self::Dequantize { .. } => "dequantize",
483            Self::QuantizedMatmul { .. } => "quantized_matmul",
484            Self::Custom(_) => "custom_function",
485            Self::MetalKernel(_) => "metal_kernel",
486            Self::Copy => "copy",
487            Self::Depends { .. } => "depends",
488            Self::Collective { op, .. } => op.name(),
489            #[cfg(test)]
490            Self::AddMultiply => "add_multiply",
491        }
492    }
493
494    /// Whether the parameters hold sizes taken from the traced shapes, so a replay on other
495    /// shapes would be wrong (MLX's primitives without `output_shapes`): shapeless `compile`
496    /// rejects these. `Broadcast` is listed; shapeless `compile` re-derives its shape from
497    /// the operands it was broadcast against. `Attention` keeps kernel choices made for the
498    /// traced lengths.
499    pub fn fixes_shape(&self) -> bool {
500        use crate::fast::imp::FastOp;
501        match self {
502            Self::Reshape(_)
503            | Self::Slice { .. }
504            | Self::SliceUpdate { .. }
505            | Self::Broadcast(_)
506            | Self::RandomBits { .. }
507            | Self::MetalKernel(_)
508            | Self::Fast(FastOp::Attention { .. } | FastOp::CausalSoftmax { .. })
509            | Self::Collective {
510                op: crate::distributed::CollectiveOp::Recv { .. },
511                ..
512            } => true,
513            Self::Unary(_)
514            | Self::Binary(_)
515            | Self::Select
516            | Self::AsType(_)
517            | Self::Transpose(_)
518            | Self::Squeeze(_)
519            | Self::ExpandDims(_)
520            | Self::StopGradient
521            | Self::View(_)
522            | Self::ToFp8
523            | Self::FromFp8(_)
524            | Self::SortedOffsets { .. }
525            | Self::Reduce { .. }
526            | Self::ArgReduce { .. }
527            | Self::GatherAxis { .. }
528            | Self::ScatterAxis { .. }
529            | Self::Sort { .. }
530            | Self::ArgSort { .. }
531            | Self::Scan { .. }
532            | Self::Concatenate { .. }
533            | Self::Matmul
534            | Self::AddMM { .. }
535            | Self::Quantize { .. }
536            | Self::Dequantize { .. }
537            | Self::QuantizedMatmul { .. }
538            | Self::Fast(_)
539            | Self::Compiled(_)
540            | Self::Custom(_)
541            | Self::Copy
542            | Self::Collective { .. }
543            | Self::Depends { .. } => false,
544            #[cfg(test)]
545            Self::AddMultiply => false,
546        }
547    }
548
549    /// The output shape of a `Squeeze` or `ExpandDims` of an input of shape `dims`.
550    pub(crate) fn unit_axes_shape(&self, dims: &[usize]) -> Result<Vec<usize>> {
551        let sorted = |axes: &[usize], n: usize| {
552            axes.windows(2).all(|w| w[0] < w[1]) && axes.last().is_none_or(|&a| a < n)
553        };
554        match self {
555            Self::Squeeze(axes) => {
556                if !sorted(axes, dims.len()) || axes.iter().any(|&a| dims[a] != 1) {
557                    return Err(Error::InvalidArgument(format!(
558                        "squeeze: cannot remove axes {axes:?} of shape {dims:?}"
559                    )));
560                }
561                Ok(dims
562                    .iter()
563                    .enumerate()
564                    .filter(|(i, _)| !axes.contains(i))
565                    .map(|(_, &d)| d)
566                    .collect())
567            }
568            Self::ExpandDims(axes) => {
569                let n = dims.len() + axes.len();
570                if !sorted(axes, n) {
571                    return Err(Error::InvalidArgument(format!(
572                        "expand_dims: invalid axes {axes:?} for {n} dimensions"
573                    )));
574                }
575                let mut rest = dims.iter();
576                Ok((0..n)
577                    .map(|i| {
578                        if axes.contains(&i) {
579                            1
580                        } else {
581                            *rest.next().expect("n")
582                        }
583                    })
584                    .collect())
585            }
586            _ => unreachable!("not a unit-axes view"),
587        }
588    }
589
590    /// Validates the inputs and infers the outputs, without computing anything.
591    pub(crate) fn output_specs(&self, inputs: &[Array]) -> Result<SmallVec<[OutputSpec; 1]>> {
592        let computed = |dtype: DType, shape: &[usize]| OutputSpec {
593            dtype,
594            shape: shape.into(),
595            strides: None,
596        };
597        let spec = match self {
598            Self::Unary(op) => {
599                let [a] = self.arity::<1>(inputs)?;
600                // As the ops layer promotes and rejects (the kernels rely on it).
601                let supported = match op {
602                    UnaryOp::Negative => a.dtype() != DType::Bool,
603                    UnaryOp::Abs | UnaryOp::Conjugate => true,
604                    _ => a.dtype().category() == Category::Float,
605                };
606                if !supported {
607                    return Err(Error::unsupported_dtype(op.name(), a.dtype()));
608                }
609                let dtype = match (op, a.dtype()) {
610                    (UnaryOp::Abs, DType::Complex64) => DType::F32,
611                    (_, d) => d,
612                };
613                computed(dtype, a.dims())
614            }
615            Self::Binary(op) => {
616                let [a, b] = self.arity::<2>(inputs)?;
617                self.same_shape_and_dtype(a, b)?;
618                let category = a.dtype().category();
619                let unsupported = match op {
620                    BinaryOp::LogAddExp => category != Category::Float,
621                    BinaryOp::FloorDivide | BinaryOp::Power => {
622                        matches!(category, Category::Bool | Category::Complex)
623                    }
624                    BinaryOp::Divide => !matches!(category, Category::Float | Category::Complex),
625                    BinaryOp::Maximum
626                    | BinaryOp::Minimum
627                    | BinaryOp::Less
628                    | BinaryOp::LessEqual
629                    | BinaryOp::Greater
630                    | BinaryOp::GreaterEqual => category == Category::Complex,
631                    _ => false,
632                };
633                if unsupported {
634                    return Err(Error::unsupported_dtype(op.name(), a.dtype()));
635                }
636                computed(
637                    if op.is_comparison() {
638                        DType::Bool
639                    } else {
640                        a.dtype()
641                    },
642                    a.dims(),
643                )
644            }
645            Self::Select => {
646                let [c, a, b] = self.arity::<3>(inputs)?;
647                self.same_shape_and_dtype(a, b)?;
648                if c.dtype() != DType::Bool || c.dims() != a.dims() {
649                    return Err(Error::InvalidArgument(
650                        "where: condition must be a bool array of the same shape".into(),
651                    ));
652                }
653                computed(a.dtype(), a.dims())
654            }
655            Self::AsType(dtype) => {
656                let [a] = self.arity::<1>(inputs)?;
657                computed(*dtype, a.dims())
658            }
659            Self::Collective { op, group } => {
660                use crate::distributed::CollectiveOp;
661                match op {
662                    CollectiveOp::Recv { shape, dtype, .. } => {
663                        self.arity::<0>(inputs)?;
664                        computed(*dtype, shape)
665                    }
666                    CollectiveOp::Gather => {
667                        let [a] = self.arity::<1>(inputs)?;
668                        let n = crate::distributed::group_size(*group)?;
669                        let mut shape = a.dims().to_vec();
670                        match shape.first_mut() {
671                            Some(d) => *d *= n,
672                            None => shape.push(n),
673                        }
674                        computed(a.dtype(), &shape)
675                    }
676                    CollectiveOp::Reduce(_) | CollectiveOp::Send { .. } => {
677                        let [a] = self.arity::<1>(inputs)?;
678                        computed(a.dtype(), a.dims())
679                    }
680                }
681            }
682            Self::Squeeze(_) | Self::ExpandDims(_) => {
683                let [a] = self.arity::<1>(inputs)?;
684                let shape = self.unit_axes_shape(a.dims())?;
685                OutputSpec {
686                    dtype: a.dtype(),
687                    strides: view::nocopy_reshape(a.dims(), a.strides(), &shape),
688                    shape: shape.as_slice().into(),
689                }
690            }
691            Self::Reshape(shape) => {
692                let [a] = self.arity::<1>(inputs)?;
693                if crate::shape::checked_size(shape)? != a.size() {
694                    return Err(Error::InvalidArgument(format!(
695                        "reshape: cannot reshape {} elements into shape {shape:?}",
696                        a.size()
697                    )));
698                }
699                OutputSpec {
700                    dtype: a.dtype(),
701                    shape: shape.as_slice().into(),
702                    strides: view::nocopy_reshape(a.dims(), a.strides(), shape),
703                }
704            }
705            Self::Transpose(axes) => {
706                let [a] = self.arity::<1>(inputs)?;
707                let (shape, strides) = view::permute(a.dims(), a.strides(), axes)?;
708                OutputSpec {
709                    dtype: a.dtype(),
710                    shape,
711                    strides: Some(strides),
712                }
713            }
714            Self::Slice { len, .. } => {
715                let [a] = self.arity::<1>(inputs)?;
716                let dims = self.dim_slices(a.ndim())?;
717                if !region_fits(&dims, a.dims()) {
718                    return Err(Error::InvalidArgument(format!(
719                        "slice: region {dims:?} does not fit shape {:?}",
720                        a.dims()
721                    )));
722                }
723                let (strides, _) = view::slice_geometry(a.strides(), &dims);
724                OutputSpec {
725                    dtype: a.dtype(),
726                    shape: len.as_slice().into(),
727                    strides: Some(strides),
728                }
729            }
730            Self::Broadcast(shape) => {
731                let [a] = self.arity::<1>(inputs)?;
732                let strides = view::broadcast_strides(a.dims(), a.strides(), shape)?;
733                OutputSpec {
734                    dtype: a.dtype(),
735                    shape: shape.as_slice().into(),
736                    strides: Some(strides),
737                }
738            }
739            Self::Copy => {
740                let [a] = self.arity::<1>(inputs)?;
741                computed(a.dtype(), a.dims())
742            }
743            Self::Depends { n_deps } => {
744                if inputs.len() != n_deps + 1 {
745                    return Err(Error::InvalidArgument(format!(
746                        "depends: {} dependencies expected, got {}",
747                        n_deps,
748                        inputs.len().saturating_sub(1)
749                    )));
750                }
751                let a = &inputs[0];
752                OutputSpec {
753                    dtype: a.dtype(),
754                    shape: a.dims().into(),
755                    strides: Some(a.strides().into()),
756                }
757            }
758            Self::StopGradient => {
759                let [a] = self.arity::<1>(inputs)?;
760                OutputSpec {
761                    dtype: a.dtype(),
762                    shape: a.dims().into(),
763                    strides: Some(a.strides().into()),
764                }
765            }
766            Self::SortedOffsets { groups } => {
767                let [a] = self.arity::<1>(inputs)?;
768                if a.dtype() != DType::U32 || a.ndim() == 0 {
769                    return Err(Error::InvalidArgument(format!(
770                        "sorted_offsets: indices must be uint32 with at least one axis, got {} {:?}",
771                        a.dtype(),
772                        a.dims()
773                    )));
774                }
775                let mut shape = a.dims()[..a.ndim() - 1].to_vec();
776                shape.push(*groups);
777                computed(DType::I32, &shape)
778            }
779            Self::ToFp8 => {
780                let [a] = self.arity::<1>(inputs)?;
781                if a.dtype().category() != Category::Float {
782                    return Err(Error::unsupported_dtype("to_fp8", a.dtype()));
783                }
784                computed(DType::U8, a.dims())
785            }
786            Self::FromFp8(dtype) => {
787                let [a] = self.arity::<1>(inputs)?;
788                if a.dtype() != DType::U8 {
789                    return Err(Error::InvalidArgument(format!(
790                        "from_fp8: E4M3 bytes must be uint8, got {}",
791                        a.dtype()
792                    )));
793                }
794                if dtype.category() != Category::Float {
795                    return Err(Error::unsupported_dtype("from_fp8", *dtype));
796                }
797                computed(*dtype, a.dims())
798            }
799            Self::View(dtype) => {
800                let [a] = self.arity::<1>(inputs)?;
801                let (from, to) = (a.dtype().size(), dtype.size());
802                if from == to {
803                    OutputSpec {
804                        dtype: *dtype,
805                        shape: a.dims().into(),
806                        strides: Some(a.strides().into()),
807                    }
808                } else {
809                    let n = a.ndim();
810                    let last = a.dims().last().copied();
811                    let scaled = match last {
812                        Some(d) if (d * from) % to == 0 => d * from / to,
813                        _ => {
814                            return Err(Error::InvalidArgument(match last {
815                                None => format!(
816                                    "view: a scalar of {} cannot be viewed as {dtype} (another size)",
817                                    a.dtype()
818                                ),
819                                Some(d) => format!(
820                                    "view: the last dimension ({d}) times {from} bytes does not                                      divide into {dtype} values of {to} bytes"
821                                ),
822                            }));
823                        }
824                    };
825                    let mut shape: Shape = a.dims().into();
826                    shape[n - 1] = scaled;
827                    // A row-contiguous input stays shared (when its offset divides); others
828                    // are copied first, row-contiguous as well.
829                    let row = crate::shape::flags_for(a.dims(), a.strides()).row_contiguous;
830                    OutputSpec {
831                        dtype: *dtype,
832                        strides: row.then(|| crate::shape::row_contiguous_strides(&shape)),
833                        shape,
834                    }
835                }
836            }
837            Self::SliceUpdate { len, .. } => {
838                let [base, update] = self.arity::<2>(inputs)?;
839                let dims = self.dim_slices(base.ndim())?;
840                if !region_fits(&dims, base.dims()) || update.dims() != len.as_slice() {
841                    return Err(Error::InvalidArgument(format!(
842                        "slice_update: region {dims:?} does not fit shape {:?} with update {:?}",
843                        base.dims(),
844                        update.dims()
845                    )));
846                }
847                if base.dtype() != update.dtype() {
848                    return Err(Error::dtype_mismatch(
849                        "slice_update",
850                        base.dtype(),
851                        update.dtype(),
852                    ));
853                }
854                computed(base.dtype(), base.dims())
855            }
856            Self::Reduce { op, axes } => {
857                let [a] = self.arity::<1>(inputs)?;
858                let shape = self.reduced_shape(a.dims(), axes)?;
859                if !axes.windows(2).all(|w| w[0] < w[1]) {
860                    return Err(Error::InvalidArgument(format!(
861                        "{}: axes {axes:?} must be sorted and unique",
862                        self.name()
863                    )));
864                }
865                let supported = match op {
866                    ReduceOp::And | ReduceOp::Or => a.dtype() == DType::Bool,
867                    ReduceOp::Max | ReduceOp::Min => a.dtype() != DType::Complex64,
868                    ReduceOp::Sum | ReduceOp::Prod => true,
869                };
870                if !supported {
871                    return Err(Error::unsupported_dtype(self.name(), a.dtype()));
872                }
873                let ordered = matches!(op, ReduceOp::Max | ReduceOp::Min);
874                if ordered && axes.iter().any(|&d| a.dims()[d] == 0) {
875                    return Err(Error::InvalidArgument(format!(
876                        "{}: cannot reduce a zero-length axis (no identity)",
877                        self.name()
878                    )));
879                }
880                computed(a.dtype(), &shape)
881            }
882            Self::ArgReduce { axis, .. } => {
883                let [a] = self.arity::<1>(inputs)?;
884                if a.dtype() == DType::Complex64 {
885                    return Err(Error::unsupported_dtype(self.name(), a.dtype()));
886                }
887                computed(DType::U32, &self.reduced_shape(a.dims(), &[*axis])?)
888            }
889            Self::GatherAxis { axis } => {
890                let [src, indices] = self.arity::<2>(inputs)?;
891                self.indexed(src, indices, *axis)?;
892                computed(src.dtype(), indices.dims())
893            }
894            Self::ScatterAxis { axis, .. } => {
895                let [dst, indices, updates] = self.arity::<3>(inputs)?;
896                self.indexed(dst, indices, *axis)?;
897                if updates.dims() != indices.dims() {
898                    return Err(Error::shape_mismatch(
899                        self.name(),
900                        indices.dims(),
901                        updates.dims(),
902                    ));
903                }
904                if updates.dtype() != dst.dtype() {
905                    return Err(Error::dtype_mismatch(
906                        self.name(),
907                        dst.dtype(),
908                        updates.dtype(),
909                    ));
910                }
911                computed(dst.dtype(), dst.dims())
912            }
913            Self::Sort { axis } | Self::ArgSort { axis } | Self::Scan { axis, .. } => {
914                let [a] = self.arity::<1>(inputs)?;
915                if *axis >= a.ndim() {
916                    return Err(Error::InvalidArgument(format!(
917                        "{}: axis {axis} out of range for {} dimensions",
918                        self.name(),
919                        a.ndim()
920                    )));
921                }
922                let ordered = matches!(
923                    self,
924                    Self::Sort { .. }
925                        | Self::ArgSort { .. }
926                        | Self::Scan {
927                            op: ScanOp::Max | ScanOp::Min,
928                            ..
929                        }
930                );
931                let float_only = matches!(
932                    self,
933                    Self::Scan {
934                        op: ScanOp::LogAddExp,
935                        ..
936                    }
937                );
938                let category = a.dtype().category();
939                if (ordered && category == crate::Category::Complex)
940                    || (float_only && category != crate::Category::Float)
941                {
942                    return Err(Error::unsupported_dtype(self.name(), a.dtype()));
943                }
944                let dtype = match self {
945                    Self::ArgSort { .. } => DType::U32,
946                    _ => a.dtype(),
947                };
948                computed(dtype, a.dims())
949            }
950            Self::Concatenate { axis } => {
951                let first = inputs.first().ok_or_else(|| {
952                    Error::InvalidArgument("concatenate: no arrays to join".into())
953                })?;
954                if *axis >= first.ndim() {
955                    return Err(Error::InvalidArgument(format!(
956                        "concatenate: axis {axis} out of range for {} dimensions",
957                        first.ndim()
958                    )));
959                }
960                let mut shape: Shape = first.dims().into();
961                for a in &inputs[1..] {
962                    let fits = a.ndim() == first.ndim()
963                        && a.dims()
964                            .iter()
965                            .zip(first.dims())
966                            .enumerate()
967                            .all(|(d, (x, y))| d == *axis || x == y);
968                    if !fits {
969                        return Err(Error::shape_mismatch("concatenate", first.dims(), a.dims()));
970                    }
971                    if a.dtype() != first.dtype() {
972                        return Err(Error::dtype_mismatch(
973                            "concatenate",
974                            first.dtype(),
975                            a.dtype(),
976                        ));
977                    }
978                    shape[*axis] = shape[*axis].checked_add(a.dims()[*axis]).ok_or_else(|| {
979                        Error::InvalidArgument("concatenate: the joined size overflows".into())
980                    })?;
981                }
982                computed(first.dtype(), &shape)
983            }
984            Self::Matmul | Self::AddMM { .. } => {
985                let (a, b) = match self {
986                    Self::Matmul => {
987                        let [a, b] = self.arity::<2>(inputs)?;
988                        (a, b)
989                    }
990                    _ => {
991                        let [a, b, _] = self.arity::<3>(inputs)?;
992                        (a, b)
993                    }
994                };
995                let (sa, sb) = (a.dims(), b.dims());
996                let n = sa.len();
997                let valid =
998                    n >= 2 && sb.len() == n && sa[..n - 2] == sb[..n - 2] && sa[n - 1] == sb[n - 2];
999                if !valid {
1000                    return Err(Error::shape_mismatch(self.name(), sa, sb));
1001                }
1002                if a.dtype() != b.dtype() {
1003                    return Err(Error::dtype_mismatch(self.name(), a.dtype(), b.dtype()));
1004                }
1005                if !matches!(a.dtype().category(), Category::Float | Category::Complex) {
1006                    return Err(Error::unsupported_dtype(self.name(), a.dtype()));
1007                }
1008                let mut shape: Shape = sa.into();
1009                shape[n - 1] = sb[n - 1];
1010                if let Self::AddMM { .. } = self {
1011                    let c = &inputs[2];
1012                    if c.dims() != shape.as_slice() || c.dtype() != a.dtype() {
1013                        return Err(Error::InvalidArgument(format!(
1014                            "addmm: c must be {} of shape {shape:?}, got {} {:?}",
1015                            a.dtype(),
1016                            c.dtype(),
1017                            c.dims()
1018                        )));
1019                    }
1020                }
1021                computed(a.dtype(), &shape)
1022            }
1023            Self::RandomBits { shape, width } => {
1024                let [keys] = self.arity::<1>(inputs)?;
1025                if keys.dtype() != DType::U32 || keys.dims().last() != Some(&2) {
1026                    return Err(Error::InvalidArgument(format!(
1027                        "random_bits: keys must be uint32 of shape [..., 2], got {} {:?}",
1028                        keys.dtype(),
1029                        keys.dims()
1030                    )));
1031                }
1032                let dtype = match width {
1033                    1 => DType::U8,
1034                    2 => DType::U16,
1035                    4 => DType::U32,
1036                    _ => {
1037                        return Err(Error::InvalidArgument(format!(
1038                            "random_bits: width must be 1, 2 or 4, got {width}"
1039                        )));
1040                    }
1041                };
1042                let out: Vec<usize> = keys.dims()[..keys.ndim() - 1]
1043                    .iter()
1044                    .chain(shape)
1045                    .copied()
1046                    .collect();
1047                computed(dtype, &out)
1048            }
1049            Self::Fast(op) => {
1050                return Ok(op
1051                    .outputs(inputs)?
1052                    .into_iter()
1053                    .map(|(dtype, shape)| computed(dtype, &shape))
1054                    .collect());
1055            }
1056            Self::Compiled(fused) => {
1057                let dtypes: Vec<DType> = inputs.iter().map(Array::dtype).collect();
1058                if dtypes != fused.inputs() {
1059                    return Err(Error::InvalidArgument(format!(
1060                        "compiled: inputs {dtypes:?}, expected {:?}",
1061                        fused.inputs()
1062                    )));
1063                }
1064                let shape = inputs[0].dims();
1065                if let Some(other) = inputs.iter().find(|a| a.dims() != shape) {
1066                    return Err(Error::shape_mismatch("compiled", shape, other.dims()));
1067                }
1068                return Ok(fused
1069                    .dtypes()
1070                    .into_iter()
1071                    .map(|d| computed(d, shape))
1072                    .collect());
1073            }
1074            Self::Quantize { quant } => {
1075                let [w] = self.arity::<1>(inputs)?;
1076                let (group_size, bits) = quant.checked(self.name())?;
1077                let n = w.ndim();
1078                if !matches!(w.dtype(), DType::F16 | DType::BF16 | DType::F32) {
1079                    return Err(Error::unsupported_dtype("quantize", w.dtype()));
1080                }
1081                if n < 2 || w.dims()[n - 1] % group_size != 0 {
1082                    return Err(Error::InvalidArgument(format!(
1083                        "quantize: needs at least 2 dimensions with the last divisible by \
1084                         the group size {group_size}, got shape {:?}",
1085                        w.dims()
1086                    )));
1087                }
1088                let last = w.dims()[n - 1];
1089                let with_last = |d: usize| {
1090                    let mut s: Shape = w.dims().into();
1091                    s[n - 1] = d;
1092                    s
1093                };
1094                let packed = computed(DType::U32, &with_last(last * bits / 32));
1095                if quant.mode != crate::QuantMode::Affine {
1096                    return Ok(smallvec![
1097                        packed,
1098                        computed(DType::U8, &with_last(last / group_size))
1099                    ]);
1100                }
1101                let groups = computed(w.dtype(), &with_last(last / group_size));
1102                return Ok(smallvec![packed, groups.clone(), groups]);
1103            }
1104            Self::Dequantize { quant, dtype } => {
1105                let affine = quant.mode == crate::QuantMode::Affine;
1106                let w = self.quantized_arity(inputs, 2 + usize::from(affine))?;
1107                let mut shape: Shape = w.dims().into();
1108                let n = shape.len();
1109                shape[n - 1] = self.quantized_columns(w, &inputs[1..], *quant)?;
1110                if (affine && *dtype != inputs[1].dtype())
1111                    || !matches!(dtype, DType::F16 | DType::BF16 | DType::F32)
1112                {
1113                    return Err(Error::unsupported_dtype("dequantize", *dtype));
1114                }
1115                computed(*dtype, &shape)
1116            }
1117            Self::QuantizedMatmul { quant, transpose } => {
1118                let affine = quant.mode == crate::QuantMode::Affine;
1119                let x = self.quantized_arity(inputs, 3 + usize::from(affine))?;
1120                let (w, scales) = (&inputs[1], &inputs[2]);
1121                let columns = self.quantized_columns(w, &inputs[2..], *quant)?;
1122                let rows = w.dims()[w.ndim() - 2];
1123                let (inner, outer) = if *transpose {
1124                    (columns, rows)
1125                } else {
1126                    (rows, columns)
1127                };
1128                let (xn, wn) = (x.ndim(), w.ndim());
1129                let batch_ok = wn == 2 || (xn == wn && x.dims()[..xn - 2] == w.dims()[..wn - 2]);
1130                if xn == 0 || x.dims()[xn - 1] != inner || !batch_ok {
1131                    return Err(Error::shape_mismatch(
1132                        "quantized_matmul",
1133                        x.dims(),
1134                        w.dims(),
1135                    ));
1136                }
1137                if affine && x.dtype() != scales.dtype() {
1138                    return Err(Error::dtype_mismatch(
1139                        "quantized_matmul",
1140                        x.dtype(),
1141                        scales.dtype(),
1142                    ));
1143                }
1144                if !matches!(x.dtype(), DType::F16 | DType::BF16 | DType::F32) {
1145                    return Err(Error::unsupported_dtype("quantized_matmul", x.dtype()));
1146                }
1147                let mut shape: Shape = x.dims().into();
1148                shape[xn - 1] = outer;
1149                computed(x.dtype(), &shape)
1150            }
1151            Self::MetalKernel(call) => {
1152                return Ok(call
1153                    .outputs
1154                    .iter()
1155                    .map(|(dtype, shape)| computed(*dtype, shape))
1156                    .collect());
1157            }
1158            Self::Custom(call) => {
1159                let outputs = inputs.get(call.primals..).unwrap_or_default();
1160                if outputs.is_empty() {
1161                    return Err(Error::InvalidArgument(
1162                        "custom_function: the function must return at least one array".into(),
1163                    ));
1164                }
1165                return Ok(outputs
1166                    .iter()
1167                    .map(|a| OutputSpec {
1168                        dtype: a.dtype(),
1169                        shape: a.dims().into(),
1170                        strides: Some(a.strides().into()),
1171                    })
1172                    .collect());
1173            }
1174            #[cfg(test)]
1175            Self::AddMultiply => {
1176                let [a, b] = self.arity::<2>(inputs)?;
1177                self.same_shape_and_dtype(a, b)?;
1178                let spec = computed(a.dtype(), a.dims());
1179                return Ok(smallvec![spec.clone(), spec]);
1180            }
1181        };
1182        Ok(smallvec![spec])
1183    }
1184
1185    /// The per-dimension slices of a `Slice` or `SliceUpdate` primitive over `ndim`
1186    /// dimensions.
1187    pub(crate) fn dim_slices(&self, ndim: usize) -> Result<Vec<DimSlice>> {
1188        match self {
1189            Self::Slice { start, step, len } | Self::SliceUpdate { start, step, len }
1190                if start.len() == ndim && step.len() == ndim && len.len() == ndim =>
1191            {
1192                Ok((0..ndim)
1193                    .map(|i| DimSlice {
1194                        start: start[i],
1195                        step: step[i],
1196                        len: len[i],
1197                    })
1198                    .collect())
1199            }
1200            _ => Err(Error::InvalidArgument(
1201                "slice: one start, step and length per dimension".into(),
1202            )),
1203        }
1204    }
1205
1206    /// The first input of a quantized primitive with `arity` inputs (affine has one more:
1207    /// the biases).
1208    fn quantized_arity<'a>(&self, inputs: &'a [Array], arity: usize) -> Result<&'a Array> {
1209        if inputs.len() != arity {
1210            return Err(Error::InvalidArgument(format!(
1211                "{}: {arity} inputs expected, got {}",
1212                self.name(),
1213                inputs.len()
1214            )));
1215        }
1216        Ok(&inputs[0])
1217    }
1218
1219    /// The number of values per row of packed codes `w` (uint32, at least 2-D), after
1220    /// validating it against `[scales[, biases]]` (affine: one float dtype, equal shapes;
1221    /// floating-point modes: uint8 scales), one value per group, as MLX's
1222    /// `validate_quantized_input`.
1223    pub(crate) fn quantized_columns(
1224        &self,
1225        w: &Array,
1226        groups: &[Array],
1227        quant: crate::Quant,
1228    ) -> Result<usize> {
1229        let (group_size, bits) = quant.checked(self.name())?;
1230        let scales = &groups[0];
1231        let n = w.ndim();
1232        let invalid =
1233            |what: String| Err(Error::InvalidArgument(format!("{}: {what}", self.name())));
1234        if w.dtype() != DType::U32 {
1235            return invalid(format!("packed weights must be uint32, got {}", w.dtype()));
1236        }
1237        if quant.mode == crate::QuantMode::Affine {
1238            let biases = &groups[1];
1239            if !matches!(scales.dtype(), DType::F16 | DType::BF16 | DType::F32)
1240                || biases.dtype() != scales.dtype()
1241                || biases.dims() != scales.dims()
1242            {
1243                return invalid(format!(
1244                    "scales and biases must share a float dtype and shape, got {} {:?} and {} {:?}",
1245                    scales.dtype(),
1246                    scales.dims(),
1247                    biases.dtype(),
1248                    biases.dims()
1249                ));
1250            }
1251        } else if scales.dtype() != DType::U8 {
1252            return invalid(format!(
1253                "scale type must be uint8 but received type {}",
1254                scales.dtype()
1255            ));
1256        }
1257        let packed = w.dims().last().copied().unwrap_or(0) * 32;
1258        let groups = scales.dims().last().copied().unwrap_or(0) * group_size;
1259        if n < 2
1260            || scales.ndim() != n
1261            || w.dims()[..n - 1] != scales.dims()[..n - 1]
1262            || packed != groups * bits
1263        {
1264            return invalid(format!(
1265                "packed weights {:?} and scales {:?} do not match group size {group_size} \
1266                 and {bits} bits",
1267                w.dims(),
1268                scales.dims()
1269            ));
1270        }
1271        Ok(groups)
1272    }
1273
1274    /// Validates an indexing operation's array and indices: integer indices with the
1275    /// array's shape except along `axis`.
1276    fn indexed(&self, array: &Array, indices: &Array, axis: usize) -> Result<()> {
1277        if !matches!(
1278            indices.dtype().category(),
1279            crate::Category::Signed | crate::Category::Unsigned
1280        ) {
1281            return Err(Error::InvalidArgument(format!(
1282                "{}: indices must be integers, got {}",
1283                self.name(),
1284                indices.dtype()
1285            )));
1286        }
1287        let fits = indices.ndim() == array.ndim()
1288            && axis < array.ndim()
1289            && indices
1290                .dims()
1291                .iter()
1292                .zip(array.dims())
1293                .enumerate()
1294                .all(|(d, (i, a))| d == axis || i == a);
1295        if !fits {
1296            return Err(Error::InvalidArgument(format!(
1297                "{}: indices of shape {:?} do not fit an array of shape {:?} along axis {axis}",
1298                self.name(),
1299                indices.dims(),
1300                array.dims()
1301            )));
1302        }
1303        if array.dims()[axis] == 0 && indices.size() > 0 {
1304            return Err(Error::InvalidArgument(format!(
1305                "{}: cannot index an empty axis",
1306                self.name()
1307            )));
1308        }
1309        Ok(())
1310    }
1311
1312    /// `shape` with `axes` set to 1, after validating the axes.
1313    fn reduced_shape(&self, shape: &[usize], axes: &[usize]) -> Result<Shape> {
1314        let mut out: Shape = shape.into();
1315        for &axis in axes {
1316            *out.get_mut(axis).ok_or_else(|| {
1317                Error::InvalidArgument(format!(
1318                    "{}: axis {axis} out of range for shape {shape:?}",
1319                    self.name()
1320                ))
1321            })? = 1;
1322        }
1323        Ok(out)
1324    }
1325
1326    fn arity<'a, const N: usize>(&self, inputs: &'a [Array]) -> Result<&'a [Array; N]> {
1327        inputs.try_into().map_err(|_| {
1328            Error::InvalidArgument(format!(
1329                "{} takes {N} inputs, got {}",
1330                self.name(),
1331                inputs.len()
1332            ))
1333        })
1334    }
1335
1336    fn same_shape_and_dtype(&self, a: &Array, b: &Array) -> Result<()> {
1337        if a.dims() != b.dims() {
1338            return Err(Error::shape_mismatch(self.name(), a.dims(), b.dims()));
1339        }
1340        if a.dtype() != b.dtype() {
1341            return Err(Error::dtype_mismatch(self.name(), a.dtype(), b.dtype()));
1342        }
1343        Ok(())
1344    }
1345}
1346
1347/// A row-contiguous copy of `x` ([`Primitive::Copy`]).
1348pub(crate) fn copy(x: &Array) -> Result<Array> {
1349    Ok(Array::from_op(Primitive::Copy, vec![x.clone()])?.swap_remove(0))
1350}
1351
1352/// `x`, evaluated after `deps` ([`Primitive::Depends`]).
1353pub(crate) fn depends(x: &Array, deps: &[Array]) -> Result<Array> {
1354    let mut inputs = Vec::with_capacity(deps.len() + 1);
1355    inputs.push(x.clone());
1356    inputs.extend_from_slice(deps);
1357    let primitive = Primitive::Depends { n_deps: deps.len() };
1358    Ok(Array::from_op(primitive, inputs)?.swap_remove(0))
1359}
1360
1361/// Whether each dimension's slice selects indices within `shape` with a non-zero step (a
1362/// zero-length slice starts at most at the end), so that no offset computed from it
1363/// overflows. The step of a slice of at most one index is never applied (any value).
1364pub(crate) fn region_fits(dims: &[DimSlice], shape: &[usize]) -> bool {
1365    dims.len() == shape.len()
1366        && dims.iter().zip(shape).all(|(d, &n)| {
1367            if d.step == 0 {
1368                return false;
1369            }
1370            if d.len == 0 {
1371                return d.start <= n;
1372            }
1373            let last = isize::try_from(d.len - 1)
1374                .ok()
1375                .and_then(|steps| steps.checked_mul(d.step))
1376                .and_then(|span| isize::try_from(d.start).ok()?.checked_add(span));
1377            d.start < n && last.is_some_and(|last| (0..n as isize).contains(&last))
1378        })
1379}
1380
1381#[cfg(test)]
1382mod tests {
1383    use std::collections::hash_map::DefaultHasher;
1384    use std::hash::{Hash, Hasher};
1385
1386    use super::*;
1387
1388    fn hash(p: &Primitive) -> u64 {
1389        let mut h = DefaultHasher::new();
1390        p.hash(&mut h);
1391        h.finish()
1392    }
1393
1394    #[test]
1395    fn equal_primitives_compare_and_hash_equal() {
1396        let add = Primitive::Binary(BinaryOp::Add);
1397        assert_eq!(add, Primitive::Binary(BinaryOp::Add));
1398        assert_eq!(hash(&add), hash(&Primitive::Binary(BinaryOp::Add)));
1399        assert_ne!(add, Primitive::Binary(BinaryOp::Multiply));
1400        let s = |start: Vec<usize>| Primitive::Slice {
1401            start,
1402            step: vec![1],
1403            len: vec![2],
1404        };
1405        assert_eq!(s(vec![1]), s(vec![1]));
1406        assert_ne!(s(vec![1]), s(vec![2]));
1407    }
1408}