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::SumScatter => {
677                        let [a] = self.arity::<1>(inputs)?;
678                        let n = crate::distributed::group_size(*group)?;
679                        let mut shape = a.dims().to_vec();
680                        shape[0] /= n;
681                        computed(a.dtype(), &shape)
682                    }
683                    CollectiveOp::Reduce(_) | CollectiveOp::Send { .. } => {
684                        let [a] = self.arity::<1>(inputs)?;
685                        computed(a.dtype(), a.dims())
686                    }
687                }
688            }
689            Self::Squeeze(_) | Self::ExpandDims(_) => {
690                let [a] = self.arity::<1>(inputs)?;
691                let shape = self.unit_axes_shape(a.dims())?;
692                OutputSpec {
693                    dtype: a.dtype(),
694                    strides: view::nocopy_reshape(a.dims(), a.strides(), &shape),
695                    shape: shape.as_slice().into(),
696                }
697            }
698            Self::Reshape(shape) => {
699                let [a] = self.arity::<1>(inputs)?;
700                if crate::shape::checked_size(shape)? != a.size() {
701                    return Err(Error::InvalidArgument(format!(
702                        "reshape: cannot reshape {} elements into shape {shape:?}",
703                        a.size()
704                    )));
705                }
706                OutputSpec {
707                    dtype: a.dtype(),
708                    shape: shape.as_slice().into(),
709                    strides: view::nocopy_reshape(a.dims(), a.strides(), shape),
710                }
711            }
712            Self::Transpose(axes) => {
713                let [a] = self.arity::<1>(inputs)?;
714                let (shape, strides) = view::permute(a.dims(), a.strides(), axes)?;
715                OutputSpec {
716                    dtype: a.dtype(),
717                    shape,
718                    strides: Some(strides),
719                }
720            }
721            Self::Slice { len, .. } => {
722                let [a] = self.arity::<1>(inputs)?;
723                let dims = self.dim_slices(a.ndim())?;
724                if !region_fits(&dims, a.dims()) {
725                    return Err(Error::InvalidArgument(format!(
726                        "slice: region {dims:?} does not fit shape {:?}",
727                        a.dims()
728                    )));
729                }
730                let (strides, _) = view::slice_geometry(a.strides(), &dims);
731                OutputSpec {
732                    dtype: a.dtype(),
733                    shape: len.as_slice().into(),
734                    strides: Some(strides),
735                }
736            }
737            Self::Broadcast(shape) => {
738                let [a] = self.arity::<1>(inputs)?;
739                let strides = view::broadcast_strides(a.dims(), a.strides(), shape)?;
740                OutputSpec {
741                    dtype: a.dtype(),
742                    shape: shape.as_slice().into(),
743                    strides: Some(strides),
744                }
745            }
746            Self::Copy => {
747                let [a] = self.arity::<1>(inputs)?;
748                computed(a.dtype(), a.dims())
749            }
750            Self::Depends { n_deps } => {
751                if inputs.len() != n_deps + 1 {
752                    return Err(Error::InvalidArgument(format!(
753                        "depends: {} dependencies expected, got {}",
754                        n_deps,
755                        inputs.len().saturating_sub(1)
756                    )));
757                }
758                let a = &inputs[0];
759                OutputSpec {
760                    dtype: a.dtype(),
761                    shape: a.dims().into(),
762                    strides: Some(a.strides().into()),
763                }
764            }
765            Self::StopGradient => {
766                let [a] = self.arity::<1>(inputs)?;
767                OutputSpec {
768                    dtype: a.dtype(),
769                    shape: a.dims().into(),
770                    strides: Some(a.strides().into()),
771                }
772            }
773            Self::SortedOffsets { groups } => {
774                let [a] = self.arity::<1>(inputs)?;
775                if a.dtype() != DType::U32 || a.ndim() == 0 {
776                    return Err(Error::InvalidArgument(format!(
777                        "sorted_offsets: indices must be uint32 with at least one axis, got {} {:?}",
778                        a.dtype(),
779                        a.dims()
780                    )));
781                }
782                let mut shape = a.dims()[..a.ndim() - 1].to_vec();
783                shape.push(*groups);
784                computed(DType::I32, &shape)
785            }
786            Self::ToFp8 => {
787                let [a] = self.arity::<1>(inputs)?;
788                if a.dtype().category() != Category::Float {
789                    return Err(Error::unsupported_dtype("to_fp8", a.dtype()));
790                }
791                computed(DType::U8, a.dims())
792            }
793            Self::FromFp8(dtype) => {
794                let [a] = self.arity::<1>(inputs)?;
795                if a.dtype() != DType::U8 {
796                    return Err(Error::InvalidArgument(format!(
797                        "from_fp8: E4M3 bytes must be uint8, got {}",
798                        a.dtype()
799                    )));
800                }
801                if dtype.category() != Category::Float {
802                    return Err(Error::unsupported_dtype("from_fp8", *dtype));
803                }
804                computed(*dtype, a.dims())
805            }
806            Self::View(dtype) => {
807                let [a] = self.arity::<1>(inputs)?;
808                let (from, to) = (a.dtype().size(), dtype.size());
809                if from == to {
810                    OutputSpec {
811                        dtype: *dtype,
812                        shape: a.dims().into(),
813                        strides: Some(a.strides().into()),
814                    }
815                } else {
816                    let n = a.ndim();
817                    let last = a.dims().last().copied();
818                    let scaled = match last {
819                        Some(d) if (d * from) % to == 0 => d * from / to,
820                        _ => {
821                            return Err(Error::InvalidArgument(match last {
822                                None => format!(
823                                    "view: a scalar of {} cannot be viewed as {dtype} (another size)",
824                                    a.dtype()
825                                ),
826                                Some(d) => format!(
827                                    "view: the last dimension ({d}) times {from} bytes does not                                      divide into {dtype} values of {to} bytes"
828                                ),
829                            }));
830                        }
831                    };
832                    let mut shape: Shape = a.dims().into();
833                    shape[n - 1] = scaled;
834                    // A row-contiguous input stays shared (when its offset divides); others
835                    // are copied first, row-contiguous as well.
836                    let row = crate::shape::flags_for(a.dims(), a.strides()).row_contiguous;
837                    OutputSpec {
838                        dtype: *dtype,
839                        strides: row.then(|| crate::shape::row_contiguous_strides(&shape)),
840                        shape,
841                    }
842                }
843            }
844            Self::SliceUpdate { len, .. } => {
845                let [base, update] = self.arity::<2>(inputs)?;
846                let dims = self.dim_slices(base.ndim())?;
847                if !region_fits(&dims, base.dims()) || update.dims() != len.as_slice() {
848                    return Err(Error::InvalidArgument(format!(
849                        "slice_update: region {dims:?} does not fit shape {:?} with update {:?}",
850                        base.dims(),
851                        update.dims()
852                    )));
853                }
854                if base.dtype() != update.dtype() {
855                    return Err(Error::dtype_mismatch(
856                        "slice_update",
857                        base.dtype(),
858                        update.dtype(),
859                    ));
860                }
861                computed(base.dtype(), base.dims())
862            }
863            Self::Reduce { op, axes } => {
864                let [a] = self.arity::<1>(inputs)?;
865                let shape = self.reduced_shape(a.dims(), axes)?;
866                if !axes.windows(2).all(|w| w[0] < w[1]) {
867                    return Err(Error::InvalidArgument(format!(
868                        "{}: axes {axes:?} must be sorted and unique",
869                        self.name()
870                    )));
871                }
872                let supported = match op {
873                    ReduceOp::And | ReduceOp::Or => a.dtype() == DType::Bool,
874                    ReduceOp::Max | ReduceOp::Min => a.dtype() != DType::Complex64,
875                    ReduceOp::Sum | ReduceOp::Prod => true,
876                };
877                if !supported {
878                    return Err(Error::unsupported_dtype(self.name(), a.dtype()));
879                }
880                let ordered = matches!(op, ReduceOp::Max | ReduceOp::Min);
881                if ordered && axes.iter().any(|&d| a.dims()[d] == 0) {
882                    return Err(Error::InvalidArgument(format!(
883                        "{}: cannot reduce a zero-length axis (no identity)",
884                        self.name()
885                    )));
886                }
887                computed(a.dtype(), &shape)
888            }
889            Self::ArgReduce { axis, .. } => {
890                let [a] = self.arity::<1>(inputs)?;
891                if a.dtype() == DType::Complex64 {
892                    return Err(Error::unsupported_dtype(self.name(), a.dtype()));
893                }
894                computed(DType::U32, &self.reduced_shape(a.dims(), &[*axis])?)
895            }
896            Self::GatherAxis { axis } => {
897                let [src, indices] = self.arity::<2>(inputs)?;
898                self.indexed(src, indices, *axis)?;
899                computed(src.dtype(), indices.dims())
900            }
901            Self::ScatterAxis { axis, .. } => {
902                let [dst, indices, updates] = self.arity::<3>(inputs)?;
903                self.indexed(dst, indices, *axis)?;
904                if updates.dims() != indices.dims() {
905                    return Err(Error::shape_mismatch(
906                        self.name(),
907                        indices.dims(),
908                        updates.dims(),
909                    ));
910                }
911                if updates.dtype() != dst.dtype() {
912                    return Err(Error::dtype_mismatch(
913                        self.name(),
914                        dst.dtype(),
915                        updates.dtype(),
916                    ));
917                }
918                computed(dst.dtype(), dst.dims())
919            }
920            Self::Sort { axis } | Self::ArgSort { axis } | Self::Scan { axis, .. } => {
921                let [a] = self.arity::<1>(inputs)?;
922                if *axis >= a.ndim() {
923                    return Err(Error::InvalidArgument(format!(
924                        "{}: axis {axis} out of range for {} dimensions",
925                        self.name(),
926                        a.ndim()
927                    )));
928                }
929                let ordered = matches!(
930                    self,
931                    Self::Sort { .. }
932                        | Self::ArgSort { .. }
933                        | Self::Scan {
934                            op: ScanOp::Max | ScanOp::Min,
935                            ..
936                        }
937                );
938                let float_only = matches!(
939                    self,
940                    Self::Scan {
941                        op: ScanOp::LogAddExp,
942                        ..
943                    }
944                );
945                let category = a.dtype().category();
946                if (ordered && category == crate::Category::Complex)
947                    || (float_only && category != crate::Category::Float)
948                {
949                    return Err(Error::unsupported_dtype(self.name(), a.dtype()));
950                }
951                let dtype = match self {
952                    Self::ArgSort { .. } => DType::U32,
953                    _ => a.dtype(),
954                };
955                computed(dtype, a.dims())
956            }
957            Self::Concatenate { axis } => {
958                let first = inputs.first().ok_or_else(|| {
959                    Error::InvalidArgument("concatenate: no arrays to join".into())
960                })?;
961                if *axis >= first.ndim() {
962                    return Err(Error::InvalidArgument(format!(
963                        "concatenate: axis {axis} out of range for {} dimensions",
964                        first.ndim()
965                    )));
966                }
967                let mut shape: Shape = first.dims().into();
968                for a in &inputs[1..] {
969                    let fits = a.ndim() == first.ndim()
970                        && a.dims()
971                            .iter()
972                            .zip(first.dims())
973                            .enumerate()
974                            .all(|(d, (x, y))| d == *axis || x == y);
975                    if !fits {
976                        return Err(Error::shape_mismatch("concatenate", first.dims(), a.dims()));
977                    }
978                    if a.dtype() != first.dtype() {
979                        return Err(Error::dtype_mismatch(
980                            "concatenate",
981                            first.dtype(),
982                            a.dtype(),
983                        ));
984                    }
985                    shape[*axis] = shape[*axis].checked_add(a.dims()[*axis]).ok_or_else(|| {
986                        Error::InvalidArgument("concatenate: the joined size overflows".into())
987                    })?;
988                }
989                computed(first.dtype(), &shape)
990            }
991            Self::Matmul | Self::AddMM { .. } => {
992                let (a, b) = match self {
993                    Self::Matmul => {
994                        let [a, b] = self.arity::<2>(inputs)?;
995                        (a, b)
996                    }
997                    _ => {
998                        let [a, b, _] = self.arity::<3>(inputs)?;
999                        (a, b)
1000                    }
1001                };
1002                let (sa, sb) = (a.dims(), b.dims());
1003                let n = sa.len();
1004                let valid =
1005                    n >= 2 && sb.len() == n && sa[..n - 2] == sb[..n - 2] && sa[n - 1] == sb[n - 2];
1006                if !valid {
1007                    return Err(Error::shape_mismatch(self.name(), sa, sb));
1008                }
1009                if a.dtype() != b.dtype() {
1010                    return Err(Error::dtype_mismatch(self.name(), a.dtype(), b.dtype()));
1011                }
1012                if !matches!(a.dtype().category(), Category::Float | Category::Complex) {
1013                    return Err(Error::unsupported_dtype(self.name(), a.dtype()));
1014                }
1015                let mut shape: Shape = sa.into();
1016                shape[n - 1] = sb[n - 1];
1017                if let Self::AddMM { .. } = self {
1018                    let c = &inputs[2];
1019                    if c.dims() != shape.as_slice() || c.dtype() != a.dtype() {
1020                        return Err(Error::InvalidArgument(format!(
1021                            "addmm: c must be {} of shape {shape:?}, got {} {:?}",
1022                            a.dtype(),
1023                            c.dtype(),
1024                            c.dims()
1025                        )));
1026                    }
1027                }
1028                computed(a.dtype(), &shape)
1029            }
1030            Self::RandomBits { shape, width } => {
1031                let [keys] = self.arity::<1>(inputs)?;
1032                if keys.dtype() != DType::U32 || keys.dims().last() != Some(&2) {
1033                    return Err(Error::InvalidArgument(format!(
1034                        "random_bits: keys must be uint32 of shape [..., 2], got {} {:?}",
1035                        keys.dtype(),
1036                        keys.dims()
1037                    )));
1038                }
1039                let dtype = match width {
1040                    1 => DType::U8,
1041                    2 => DType::U16,
1042                    4 => DType::U32,
1043                    _ => {
1044                        return Err(Error::InvalidArgument(format!(
1045                            "random_bits: width must be 1, 2 or 4, got {width}"
1046                        )));
1047                    }
1048                };
1049                let out: Vec<usize> = keys.dims()[..keys.ndim() - 1]
1050                    .iter()
1051                    .chain(shape)
1052                    .copied()
1053                    .collect();
1054                computed(dtype, &out)
1055            }
1056            Self::Fast(op) => {
1057                let strides = op.output_strides(inputs);
1058                return Ok(op
1059                    .outputs(inputs)?
1060                    .into_iter()
1061                    .map(|(dtype, shape)| OutputSpec {
1062                        dtype,
1063                        shape: shape.into(),
1064                        strides: strides.clone(),
1065                    })
1066                    .collect());
1067            }
1068            Self::Compiled(fused) => {
1069                let dtypes: Vec<DType> = inputs.iter().map(Array::dtype).collect();
1070                if dtypes != fused.inputs() {
1071                    return Err(Error::InvalidArgument(format!(
1072                        "compiled: inputs {dtypes:?}, expected {:?}",
1073                        fused.inputs()
1074                    )));
1075                }
1076                let shape = inputs[0].dims();
1077                if let Some(other) = inputs.iter().find(|a| a.dims() != shape) {
1078                    return Err(Error::shape_mismatch("compiled", shape, other.dims()));
1079                }
1080                return Ok(fused
1081                    .dtypes()
1082                    .into_iter()
1083                    .map(|d| computed(d, shape))
1084                    .collect());
1085            }
1086            Self::Quantize { quant } => {
1087                let [w] = self.arity::<1>(inputs)?;
1088                let (group_size, bits) = quant.checked(self.name())?;
1089                let n = w.ndim();
1090                if !matches!(w.dtype(), DType::F16 | DType::BF16 | DType::F32) {
1091                    return Err(Error::unsupported_dtype("quantize", w.dtype()));
1092                }
1093                if n < 2 || w.dims()[n - 1] % group_size != 0 {
1094                    return Err(Error::InvalidArgument(format!(
1095                        "quantize: needs at least 2 dimensions with the last divisible by \
1096                         the group size {group_size}, got shape {:?}",
1097                        w.dims()
1098                    )));
1099                }
1100                let last = w.dims()[n - 1];
1101                let with_last = |d: usize| {
1102                    let mut s: Shape = w.dims().into();
1103                    s[n - 1] = d;
1104                    s
1105                };
1106                let packed = computed(DType::U32, &with_last(last * bits / 32));
1107                if quant.mode != crate::QuantMode::Affine {
1108                    return Ok(smallvec![
1109                        packed,
1110                        computed(DType::U8, &with_last(last / group_size))
1111                    ]);
1112                }
1113                let groups = computed(w.dtype(), &with_last(last / group_size));
1114                return Ok(smallvec![packed, groups.clone(), groups]);
1115            }
1116            Self::Dequantize { quant, dtype } => {
1117                let affine = quant.mode == crate::QuantMode::Affine;
1118                let w = self.quantized_arity(inputs, 2 + usize::from(affine))?;
1119                let mut shape: Shape = w.dims().into();
1120                let n = shape.len();
1121                shape[n - 1] = self.quantized_columns(w, &inputs[1..], *quant)?;
1122                if (affine && *dtype != inputs[1].dtype())
1123                    || !matches!(dtype, DType::F16 | DType::BF16 | DType::F32)
1124                {
1125                    return Err(Error::unsupported_dtype("dequantize", *dtype));
1126                }
1127                computed(*dtype, &shape)
1128            }
1129            Self::QuantizedMatmul { quant, transpose } => {
1130                let affine = quant.mode == crate::QuantMode::Affine;
1131                let x = self.quantized_arity(inputs, 3 + usize::from(affine))?;
1132                let (w, scales) = (&inputs[1], &inputs[2]);
1133                let columns = self.quantized_columns(w, &inputs[2..], *quant)?;
1134                let rows = w.dims()[w.ndim() - 2];
1135                let (inner, outer) = if *transpose {
1136                    (columns, rows)
1137                } else {
1138                    (rows, columns)
1139                };
1140                let (xn, wn) = (x.ndim(), w.ndim());
1141                let batch_ok = wn == 2 || (xn == wn && x.dims()[..xn - 2] == w.dims()[..wn - 2]);
1142                if xn == 0 || x.dims()[xn - 1] != inner || !batch_ok {
1143                    return Err(Error::shape_mismatch(
1144                        "quantized_matmul",
1145                        x.dims(),
1146                        w.dims(),
1147                    ));
1148                }
1149                if affine && x.dtype() != scales.dtype() {
1150                    return Err(Error::dtype_mismatch(
1151                        "quantized_matmul",
1152                        x.dtype(),
1153                        scales.dtype(),
1154                    ));
1155                }
1156                if !matches!(x.dtype(), DType::F16 | DType::BF16 | DType::F32) {
1157                    return Err(Error::unsupported_dtype("quantized_matmul", x.dtype()));
1158                }
1159                let mut shape: Shape = x.dims().into();
1160                shape[xn - 1] = outer;
1161                computed(x.dtype(), &shape)
1162            }
1163            Self::MetalKernel(call) => {
1164                return Ok(call
1165                    .outputs
1166                    .iter()
1167                    .map(|(dtype, shape)| computed(*dtype, shape))
1168                    .collect());
1169            }
1170            Self::Custom(call) => {
1171                let outputs = inputs.get(call.primals..).unwrap_or_default();
1172                if outputs.is_empty() {
1173                    return Err(Error::InvalidArgument(
1174                        "custom_function: the function must return at least one array".into(),
1175                    ));
1176                }
1177                return Ok(outputs
1178                    .iter()
1179                    .map(|a| OutputSpec {
1180                        dtype: a.dtype(),
1181                        shape: a.dims().into(),
1182                        strides: Some(a.strides().into()),
1183                    })
1184                    .collect());
1185            }
1186            #[cfg(test)]
1187            Self::AddMultiply => {
1188                let [a, b] = self.arity::<2>(inputs)?;
1189                self.same_shape_and_dtype(a, b)?;
1190                let spec = computed(a.dtype(), a.dims());
1191                return Ok(smallvec![spec.clone(), spec]);
1192            }
1193        };
1194        Ok(smallvec![spec])
1195    }
1196
1197    /// The per-dimension slices of a `Slice` or `SliceUpdate` primitive over `ndim`
1198    /// dimensions.
1199    pub(crate) fn dim_slices(&self, ndim: usize) -> Result<Vec<DimSlice>> {
1200        match self {
1201            Self::Slice { start, step, len } | Self::SliceUpdate { start, step, len }
1202                if start.len() == ndim && step.len() == ndim && len.len() == ndim =>
1203            {
1204                Ok((0..ndim)
1205                    .map(|i| DimSlice {
1206                        start: start[i],
1207                        step: step[i],
1208                        len: len[i],
1209                    })
1210                    .collect())
1211            }
1212            _ => Err(Error::InvalidArgument(
1213                "slice: one start, step and length per dimension".into(),
1214            )),
1215        }
1216    }
1217
1218    /// The first input of a quantized primitive with `arity` inputs (affine has one more:
1219    /// the biases).
1220    fn quantized_arity<'a>(&self, inputs: &'a [Array], arity: usize) -> Result<&'a Array> {
1221        if inputs.len() != arity {
1222            return Err(Error::InvalidArgument(format!(
1223                "{}: {arity} inputs expected, got {}",
1224                self.name(),
1225                inputs.len()
1226            )));
1227        }
1228        Ok(&inputs[0])
1229    }
1230
1231    /// The number of values per row of packed codes `w` (uint32, at least 2-D), after
1232    /// validating it against `[scales[, biases]]` (affine: one float dtype, equal shapes;
1233    /// floating-point modes: uint8 scales), one value per group, as MLX's
1234    /// `validate_quantized_input`.
1235    pub(crate) fn quantized_columns(
1236        &self,
1237        w: &Array,
1238        groups: &[Array],
1239        quant: crate::Quant,
1240    ) -> Result<usize> {
1241        let (group_size, bits) = quant.checked(self.name())?;
1242        let scales = &groups[0];
1243        let n = w.ndim();
1244        let invalid =
1245            |what: String| Err(Error::InvalidArgument(format!("{}: {what}", self.name())));
1246        if w.dtype() != DType::U32 {
1247            return invalid(format!("packed weights must be uint32, got {}", w.dtype()));
1248        }
1249        if quant.mode == crate::QuantMode::Affine {
1250            let biases = &groups[1];
1251            if !matches!(scales.dtype(), DType::F16 | DType::BF16 | DType::F32)
1252                || biases.dtype() != scales.dtype()
1253                || biases.dims() != scales.dims()
1254            {
1255                return invalid(format!(
1256                    "scales and biases must share a float dtype and shape, got {} {:?} and {} {:?}",
1257                    scales.dtype(),
1258                    scales.dims(),
1259                    biases.dtype(),
1260                    biases.dims()
1261                ));
1262            }
1263        } else if scales.dtype() != DType::U8 {
1264            return invalid(format!(
1265                "scale type must be uint8 but received type {}",
1266                scales.dtype()
1267            ));
1268        }
1269        let packed = w.dims().last().copied().unwrap_or(0) * 32;
1270        let groups = scales.dims().last().copied().unwrap_or(0) * group_size;
1271        if n < 2
1272            || scales.ndim() != n
1273            || w.dims()[..n - 1] != scales.dims()[..n - 1]
1274            || packed != groups * bits
1275        {
1276            return invalid(format!(
1277                "packed weights {:?} and scales {:?} do not match group size {group_size} \
1278                 and {bits} bits",
1279                w.dims(),
1280                scales.dims()
1281            ));
1282        }
1283        Ok(groups)
1284    }
1285
1286    /// Validates an indexing operation's array and indices: integer indices with the
1287    /// array's shape except along `axis`.
1288    fn indexed(&self, array: &Array, indices: &Array, axis: usize) -> Result<()> {
1289        if !matches!(
1290            indices.dtype().category(),
1291            crate::Category::Signed | crate::Category::Unsigned
1292        ) {
1293            return Err(Error::InvalidArgument(format!(
1294                "{}: indices must be integers, got {}",
1295                self.name(),
1296                indices.dtype()
1297            )));
1298        }
1299        let fits = indices.ndim() == array.ndim()
1300            && axis < array.ndim()
1301            && indices
1302                .dims()
1303                .iter()
1304                .zip(array.dims())
1305                .enumerate()
1306                .all(|(d, (i, a))| d == axis || i == a);
1307        if !fits {
1308            return Err(Error::InvalidArgument(format!(
1309                "{}: indices of shape {:?} do not fit an array of shape {:?} along axis {axis}",
1310                self.name(),
1311                indices.dims(),
1312                array.dims()
1313            )));
1314        }
1315        if array.dims()[axis] == 0 && indices.size() > 0 {
1316            return Err(Error::InvalidArgument(format!(
1317                "{}: cannot index an empty axis",
1318                self.name()
1319            )));
1320        }
1321        Ok(())
1322    }
1323
1324    /// `shape` with `axes` set to 1, after validating the axes.
1325    fn reduced_shape(&self, shape: &[usize], axes: &[usize]) -> Result<Shape> {
1326        let mut out: Shape = shape.into();
1327        for &axis in axes {
1328            *out.get_mut(axis).ok_or_else(|| {
1329                Error::InvalidArgument(format!(
1330                    "{}: axis {axis} out of range for shape {shape:?}",
1331                    self.name()
1332                ))
1333            })? = 1;
1334        }
1335        Ok(out)
1336    }
1337
1338    fn arity<'a, const N: usize>(&self, inputs: &'a [Array]) -> Result<&'a [Array; N]> {
1339        inputs.try_into().map_err(|_| {
1340            Error::InvalidArgument(format!(
1341                "{} takes {N} inputs, got {}",
1342                self.name(),
1343                inputs.len()
1344            ))
1345        })
1346    }
1347
1348    fn same_shape_and_dtype(&self, a: &Array, b: &Array) -> Result<()> {
1349        if a.dims() != b.dims() {
1350            return Err(Error::shape_mismatch(self.name(), a.dims(), b.dims()));
1351        }
1352        if a.dtype() != b.dtype() {
1353            return Err(Error::dtype_mismatch(self.name(), a.dtype(), b.dtype()));
1354        }
1355        Ok(())
1356    }
1357}
1358
1359/// A row-contiguous copy of `x` ([`Primitive::Copy`]).
1360pub(crate) fn copy(x: &Array) -> Result<Array> {
1361    Ok(Array::from_op(Primitive::Copy, vec![x.clone()])?.swap_remove(0))
1362}
1363
1364/// `x`, evaluated after `deps` ([`Primitive::Depends`]).
1365pub(crate) fn depends(x: &Array, deps: &[Array]) -> Result<Array> {
1366    let mut inputs = Vec::with_capacity(deps.len() + 1);
1367    inputs.push(x.clone());
1368    inputs.extend_from_slice(deps);
1369    let primitive = Primitive::Depends { n_deps: deps.len() };
1370    Ok(Array::from_op(primitive, inputs)?.swap_remove(0))
1371}
1372
1373/// Whether each dimension's slice selects indices within `shape` with a non-zero step (a
1374/// zero-length slice starts at most at the end), so that no offset computed from it
1375/// overflows. The step of a slice of at most one index is never applied (any value).
1376pub(crate) fn region_fits(dims: &[DimSlice], shape: &[usize]) -> bool {
1377    dims.len() == shape.len()
1378        && dims.iter().zip(shape).all(|(d, &n)| {
1379            if d.step == 0 {
1380                return false;
1381            }
1382            if d.len == 0 {
1383                return d.start <= n;
1384            }
1385            let last = isize::try_from(d.len - 1)
1386                .ok()
1387                .and_then(|steps| steps.checked_mul(d.step))
1388                .and_then(|span| isize::try_from(d.start).ok()?.checked_add(span));
1389            d.start < n && last.is_some_and(|last| (0..n as isize).contains(&last))
1390        })
1391}
1392
1393#[cfg(test)]
1394mod tests {
1395    use std::collections::hash_map::DefaultHasher;
1396    use std::hash::{Hash, Hasher};
1397
1398    use super::*;
1399
1400    fn hash(p: &Primitive) -> u64 {
1401        let mut h = DefaultHasher::new();
1402        p.hash(&mut h);
1403        h.finish()
1404    }
1405
1406    #[test]
1407    fn equal_primitives_compare_and_hash_equal() {
1408        let add = Primitive::Binary(BinaryOp::Add);
1409        assert_eq!(add, Primitive::Binary(BinaryOp::Add));
1410        assert_eq!(hash(&add), hash(&Primitive::Binary(BinaryOp::Add)));
1411        assert_ne!(add, Primitive::Binary(BinaryOp::Multiply));
1412        let s = |start: Vec<usize>| Primitive::Slice {
1413            start,
1414            step: vec![1],
1415            len: vec![2],
1416        };
1417        assert_eq!(s(vec![1]), s(vec![1]));
1418        assert_ne!(s(vec![1]), s(vec![2]));
1419    }
1420}