Skip to main content

nuvec_core/
array.rs

1//! The lazy array handle and the graph nodes behind it.
2
3use std::collections::{HashMap, HashSet};
4use std::fmt;
5use std::hash::{BuildHasherDefault, Hasher};
6use std::panic::Location;
7use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock, PoisonError};
8use std::thread::ThreadId;
9
10use smallvec::SmallVec;
11
12use crate::buffer::Buffer;
13use crate::primitive::Primitive;
14use crate::shape::{self, Flags, Shape, Strides};
15use crate::transforms::TraceId;
16use crate::{DType, Device, Element, Error, Result, Stream, default_stream};
17
18/// A lazily evaluated n-dimensional array.
19///
20/// Cloning is cheap: clones share the computation and its result. Operations record a
21/// graph node and return immediately; values are computed on [`Array::eval`],
22/// [`crate::eval`], or when read back.
23///
24/// An operation that rejects its arguments returns a *failed* array instead of panicking:
25/// it remembers the operation, the caller's source location and the reason, every array
26/// computed from it fails with the same error, and the error surfaces as `Err` from
27/// [`Array::check`], [`Array::eval`], [`Array::item`] or [`Array::to_vec`].
28#[derive(Clone)]
29#[must_use = "arrays are lazy: an unused result computes nothing (and may hold an error)"]
30pub struct Array {
31    pub(crate) node: Arc<Node>,
32    pub(crate) output: u32,
33}
34
35/// One recorded computation, possibly with several outputs.
36pub(crate) struct Node {
37    pub(crate) stream: Stream,
38    /// The outermost transform whose tracers the node depends on (see `transforms`). The
39    /// node is a tracer while that transform runs, and then keeps its graph when evaluated.
40    pub(crate) trace: Option<TraceId>,
41    pub(crate) outputs: SmallVec<[Output; 1]>,
42    pub(crate) state: Mutex<State>,
43    /// Whether the node is a failed array: an operation rejected its arguments, or one of
44    /// its inputs had failed, when it was recorded (see [`Array::is_failed`]).
45    pub(crate) failed: bool,
46}
47
48/// Metadata and (once evaluated) data of one node output.
49pub(crate) struct Output {
50    pub(crate) dtype: DType,
51    pub(crate) shape: Shape,
52    /// `shape` as the public `i32` dimensions (validated to fit when the output is made).
53    pub(crate) ishape: SmallVec<[i32; 4]>,
54    pub(crate) strides: Strides,
55    pub(crate) flags: Flags,
56    pub(crate) size: usize,
57    /// Set exactly once, when the node is evaluated (or at creation for leaves).
58    pub(crate) slot: OnceLock<Result<Data>>,
59    /// GPU data whose command buffer is committed on the device timeline but may not have
60    /// completed: later GPU work (ordered after it on the device) may read it before `slot` is set.
61    /// Shared with the committed work that publishes it, which locks it before it reaches
62    /// the node (so a consumer holding the lock sees the node's real handles).
63    pub(crate) inflight: Arc<InFlight>,
64}
65
66/// An output's in-flight data (see [`Output::inflight`]). It is published to the slot from
67/// here on completion, unless a consumer took it over first (donation): the lock orders
68/// the two.
69#[derive(Default)]
70pub(crate) struct InFlight {
71    state: Mutex<InFlightState>,
72    /// The failure cell of the command buffer producing the data: consumers in other
73    /// evaluations that read the data while it is in flight fail with it.
74    chunk: OnceLock<ChunkStatus>,
75}
76
77/// Set when a committed chunk of GPU work fails (see `gpu`).
78pub(crate) type ChunkStatus = Arc<OnceLock<Error>>;
79
80#[derive(Default)]
81pub(crate) enum InFlightState {
82    #[default]
83    Unset,
84    #[cfg_attr(not(all(target_os = "macos", not(miri))), allow(dead_code))]
85    Set(Data),
86    /// Taken over by a consumer that writes into it in place; never published.
87    Donated,
88}
89
90impl InFlight {
91    pub(crate) fn lock(&self) -> MutexGuard<'_, InFlightState> {
92        self.state.lock().unwrap_or_else(PoisonError::into_inner)
93    }
94
95    /// Exposes `data` produced by the chunk whose failure cell is `chunk`.
96    #[cfg_attr(not(all(target_os = "macos", not(miri))), allow(dead_code))]
97    pub(crate) fn set_from(&self, data: Data, chunk: &ChunkStatus) {
98        let _ = self.chunk.set(chunk.clone());
99        self.set(data);
100    }
101
102    /// The failure cell of the chunk producing the in-flight data, if known.
103    #[cfg_attr(not(all(target_os = "macos", not(miri))), allow(dead_code))]
104    pub(crate) fn chunk(&self) -> Option<ChunkStatus> {
105        self.chunk.get().cloned()
106    }
107
108    /// Exposes `data` (once; only GPU work has in-flight data).
109    #[cfg_attr(not(all(target_os = "macos", not(miri))), allow(dead_code))]
110    pub(crate) fn set(&self, data: Data) {
111        let mut state = self.lock();
112        if matches!(*state, InFlightState::Unset) {
113            *state = InFlightState::Set(data);
114        }
115    }
116
117    /// The data, if exposed and not donated.
118    pub(crate) fn get(&self) -> Option<Data> {
119        match &*self.lock() {
120            InFlightState::Set(data) => Some(data.clone()),
121            _ => None,
122        }
123    }
124}
125
126/// Evaluated data: elements of a (possibly shared) buffer, starting at `offset` and laid
127/// out by the output's strides. Views share their input's buffer.
128#[derive(Clone)]
129pub(crate) struct Data {
130    pub(crate) buffer: Arc<Buffer>,
131    /// Position of the first element, in elements.
132    pub(crate) offset: isize,
133}
134
135impl Data {
136    pub(crate) fn new(buffer: Buffer) -> Self {
137        Self {
138            buffer: Arc::new(buffer),
139            offset: 0,
140        }
141    }
142}
143
144/// Whether a node still has to run. `Done` nodes have dropped their inputs (detach);
145/// `Retained` nodes are evaluated but keep their graph for a running transform.
146///
147/// Transitions: `Pending → Running → Done | Retained | Failed`, and `Retained → Done` when
148/// the transform that retained the node exits. The node's lock is held only for a
149/// transition, never while the node computes or waits for another node.
150pub(crate) enum State {
151    Pending {
152        primitive: Primitive,
153        inputs: Vec<Array>,
154    },
155    /// One thread (`owner`) is computing the node; `done` is signalled once it has moved
156    /// on to `Done`, `Retained` or `Failed` (its outputs set, or deferred in the owner's
157    /// GPU work).
158    Running {
159        owner: ThreadId,
160        done: Arc<Completion>,
161    },
162    Retained {
163        primitive: Primitive,
164        inputs: Vec<Array>,
165    },
166    Done,
167    /// The evaluation failed; the outputs' slots hold `error`.
168    Failed {
169        error: Error,
170    },
171}
172
173/// Signalled once when a running node's evaluation has finished.
174#[derive(Default)]
175pub(crate) struct Completion {
176    done: Mutex<bool>,
177    cv: Condvar,
178}
179
180impl Completion {
181    /// Marks the evaluation finished and wakes every waiter.
182    pub(crate) fn finish(&self) {
183        *self.done.lock().unwrap_or_else(PoisonError::into_inner) = true;
184        self.cv.notify_all();
185    }
186
187    /// Blocks until [`Completion::finish`] has been called.
188    pub(crate) fn wait(&self) {
189        let mut done = self.done.lock().unwrap_or_else(PoisonError::into_inner);
190        while !*done {
191            done = self.cv.wait(done).unwrap_or_else(PoisonError::into_inner);
192        }
193    }
194}
195
196/// Hashes node addresses, the keys of the graph walks' sets and maps. Addresses are
197/// unique and not attacker-controlled, so a multiplicative hash replaces SipHash.
198#[derive(Default, Clone, Copy)]
199pub(crate) struct NodeHasher(u64);
200
201impl Hasher for NodeHasher {
202    fn finish(&self) -> u64 {
203        // Move the well-mixed high bits of the product to the low bits the table indexes
204        // with.
205        self.0.rotate_left(26)
206    }
207    fn write(&mut self, bytes: &[u8]) {
208        for &b in bytes {
209            self.write_u64(u64::from(b));
210        }
211    }
212    fn write_u64(&mut self, n: u64) {
213        self.0 = (self.0.rotate_left(5) ^ n).wrapping_mul(0x9E37_79B9_7F4A_7C15);
214    }
215    fn write_u32(&mut self, n: u32) {
216        self.write_u64(u64::from(n));
217    }
218    fn write_usize(&mut self, n: usize) {
219        self.write_u64(n as u64);
220    }
221}
222
223/// A set of nodes by address.
224pub(crate) type NodeSet = HashSet<*const Node, BuildHasherDefault<NodeHasher>>;
225/// A map keyed by node address (optionally with an output index).
226pub(crate) type NodeMap<K, V> = HashMap<K, V, BuildHasherDefault<NodeHasher>>;
227
228const _: () = {
229    const fn assert_send_sync<T: Send + Sync>() {}
230    assert_send_sync::<Array>();
231};
232
233impl Node {
234    pub(crate) fn lock(&self) -> MutexGuard<'_, State> {
235        self.state.lock().unwrap_or_else(PoisonError::into_inner)
236    }
237
238    /// Whether the transform this node depends on is running (see [`Array::is_tracer`]).
239    pub(crate) fn is_tracer(&self) -> bool {
240        self.trace.is_some_and(crate::transforms::is_live)
241    }
242}
243
244impl Output {
245    /// Why `shape` cannot describe an output of `dtype`.
246    #[cold]
247    fn invalid(shape: &[usize], dtype: DType) -> Error {
248        if shape.iter().any(|&d| i32::try_from(d).is_err()) {
249            Error::invalid_shape("shape", shape.iter().map(|&d| d as i64))
250        } else {
251            shape::checked_nbytes(shape, dtype.size())
252                .err()
253                .unwrap_or_else(|| Error::invalid_shape("shape", shape.iter().map(|&d| d as i64)))
254        }
255    }
256
257    pub(crate) fn new(
258        dtype: DType,
259        shape: Shape,
260        strides: Option<Strides>,
261        slot: OnceLock<Result<Data>>,
262    ) -> Result<Self> {
263        // One pass: the element count (checked), the byte size (checked) and the public
264        // `i32` dimensions (each must fit, as in MLX's `Shape`).
265        let mut size = 1usize;
266        let mut ishape = SmallVec::<[i32; 4]>::new();
267        for &d in &shape {
268            let (Some(n), Ok(di)) = (size.checked_mul(d), i32::try_from(d)) else {
269                return Err(Self::invalid(&shape, dtype));
270            };
271            size = n;
272            ishape.push(di);
273        }
274        if size
275            .checked_mul(dtype.size())
276            .is_none_or(|n| n > isize::MAX as usize)
277        {
278            return Err(Self::invalid(&shape, dtype));
279        }
280        let strides = strides.unwrap_or_else(|| shape::row_contiguous_strides(&shape));
281        let flags = shape::flags_for(&shape, &strides);
282        Ok(Self {
283            dtype,
284            shape,
285            ishape,
286            strides,
287            flags,
288            size,
289            slot,
290            inflight: Arc::default(),
291        })
292    }
293}
294
295impl Array {
296    // ── Construction ─────────────────────────────────────────────────────────
297
298    /// An evaluated array over a row-contiguous `buffer`.
299    pub(crate) fn from_buffer(buffer: Buffer, dtype: DType, shape: &[usize]) -> Result<Self> {
300        Self::leaf(Ok(Data::new(buffer)), dtype, shape.into())
301    }
302
303    /// A node that is already evaluated, holding `data` (or a sticky error).
304    pub(crate) fn leaf(data: Result<Data>, dtype: DType, shape: Shape) -> Result<Self> {
305        let output = Output::new(dtype, shape, None, OnceLock::from(data))?;
306        Ok(Self::from_node(Node {
307            stream: default_stream(),
308            trace: None,
309            outputs: SmallVec::from_buf([output]),
310            state: Mutex::new(State::Done),
311            failed: false,
312        }))
313    }
314
315    /// A failed array: a leaf whose evaluation returns `error`, with the given metadata
316    /// (a 0-D shape if `shape` is not a valid one). It takes no buffer.
317    #[cold]
318    pub(crate) fn failed(error: Error, dtype: DType, shape: &[usize]) -> Self {
319        let output = Output::new(
320            dtype,
321            shape.into(),
322            None,
323            OnceLock::from(Err(error.clone())),
324        )
325        .unwrap_or_else(|_| {
326            Output::new(dtype, Shape::new(), None, OnceLock::from(Err(error)))
327                .unwrap_or_else(|_| unreachable!("a 0-D output is always valid"))
328        });
329        Self::from_node(Node {
330            stream: default_stream(),
331            trace: None,
332            outputs: SmallVec::from_buf([output]),
333            state: Mutex::new(State::Done),
334            failed: true,
335        })
336    }
337
338    /// Runs the body `f` of the public operation `op`, called at `at` with the array
339    /// operands `inputs`, and returns its array; on error, a failed array instead (the
340    /// sticky error model). The error is the first failed input's (argument order), else
341    /// `f`'s error located at `at` under the name `op`; the failed array takes the first
342    /// operand's shape and dtype.
343    #[inline]
344    pub(crate) fn sticky(
345        op: &'static str,
346        at: &'static Location<'static>,
347        inputs: &[&Array],
348        f: impl FnOnce() -> Result<Array>,
349    ) -> Array {
350        match f() {
351            Ok(out) => out,
352            Err(error) => Self::failed_like(Self::sticky_error(op, at, inputs, error), inputs),
353        }
354    }
355
356    /// [`Array::sticky`] for an operation with `N` outputs: on error, `N` failed arrays.
357    #[inline]
358    pub(crate) fn sticky_n<const N: usize>(
359        op: &'static str,
360        at: &'static Location<'static>,
361        inputs: &[&Array],
362        f: impl FnOnce() -> Result<[Array; N]>,
363    ) -> [Array; N] {
364        match f() {
365            Ok(out) => out,
366            Err(error) => {
367                let error = Self::sticky_error(op, at, inputs, error);
368                std::array::from_fn(|_| Self::failed_like(error.clone(), inputs))
369            }
370        }
371    }
372
373    /// [`Array::sticky`] for an operation with a variable number of outputs: on error,
374    /// `count` failed arrays.
375    #[inline]
376    pub(crate) fn sticky_vec(
377        op: &'static str,
378        at: &'static Location<'static>,
379        inputs: &[&Array],
380        count: usize,
381        f: impl FnOnce() -> Result<Vec<Array>>,
382    ) -> Vec<Array> {
383        match f() {
384            Ok(out) => out,
385            Err(error) => {
386                let error = Self::sticky_error(op, at, inputs, error);
387                (0..count)
388                    .map(|_| Self::failed_like(error.clone(), inputs))
389                    .collect()
390            }
391        }
392    }
393
394    /// The error a failed operation reports: the first failed input's, else `error` with
395    /// the caller's location.
396    #[cold]
397    fn sticky_error(
398        op: &'static str,
399        at: &'static Location<'static>,
400        inputs: &[&Array],
401        error: Error,
402    ) -> Error {
403        inputs
404            .iter()
405            .find_map(|a| a.slot_error())
406            .unwrap_or_else(|| error.locate(op, at))
407    }
408
409    /// A failed array with the first operand's shape and dtype (0-D float32 without one).
410    #[cold]
411    pub(crate) fn failed_like(error: Error, inputs: &[&Array]) -> Self {
412        match inputs.first() {
413            Some(a) => Self::failed(error, a.dtype(), a.dims()),
414            None => Self::failed(error, DType::F32, &[]),
415        }
416    }
417
418    /// Records `primitive` applied to `inputs` and returns one array per output.
419    ///
420    /// Inputs that already failed short-circuit with their error.
421    pub(crate) fn from_op(primitive: Primitive, inputs: Vec<Array>) -> Result<Vec<Array>> {
422        let trace = crate::transforms::inherited(&inputs);
423        Self::record(primitive, inputs, trace)
424    }
425
426    /// [`Array::from_op`] with an explicit trace (`None`: not a tracer, whatever the
427    /// inputs).
428    ///
429    /// Failure is sticky: if an input has failed, nothing is recorded and every output is
430    /// a failed array carrying the first failed input's error, with the shape and dtype the
431    /// operation would have produced (an error if those cannot be inferred).
432    pub(crate) fn record(
433        primitive: Primitive,
434        inputs: Vec<Array>,
435        trace: Option<TraceId>,
436    ) -> Result<Vec<Array>> {
437        if let Some(err) = inputs.iter().find_map(Array::slot_error) {
438            return Self::record_failed(&primitive, &inputs, err);
439        }
440        Self::record_unchecked(primitive, inputs, trace)
441    }
442
443    #[cold]
444    fn record_failed(primitive: &Primitive, inputs: &[Array], err: Error) -> Result<Vec<Array>> {
445        let specs = primitive.output_specs(inputs).map_err(|_| err.clone())?;
446        Ok(specs
447            .into_iter()
448            .map(|spec| Self::failed(err.clone(), spec.dtype, &spec.shape))
449            .collect())
450    }
451
452    /// [`Array::record`] without the failed-input check: the node fails only when
453    /// evaluated. Used for `vmap` placeholders.
454    pub(crate) fn record_unchecked(
455        primitive: Primitive,
456        inputs: Vec<Array>,
457        trace: Option<TraceId>,
458    ) -> Result<Vec<Array>> {
459        let outputs = primitive
460            .output_specs(&inputs)?
461            .into_iter()
462            .map(|spec| Output::new(spec.dtype, spec.shape, spec.strides, OnceLock::new()))
463            .collect::<Result<SmallVec<_>>>()?;
464        let count = outputs.len() as u32;
465        let node = Arc::new(Node {
466            stream: default_stream(),
467            trace,
468            outputs,
469            state: Mutex::new(State::Pending { primitive, inputs }),
470            failed: false,
471        });
472        Ok((0..count)
473            .map(|output| Self {
474                node: node.clone(),
475                output,
476            })
477            .collect())
478    }
479
480    fn from_node(node: Node) -> Self {
481        Self {
482            node: Arc::new(node),
483            output: 0,
484        }
485    }
486
487    // ── Graph (for graph transformations such as `compile`) ──────────────────
488
489    /// A placeholder with this dtype and shape, standing for an input while a function is
490    /// traced (as in MLX): operations can be recorded on it, but evaluating anything that
491    /// depends on it fails. Placeholders are tracers.
492    pub(crate) fn placeholder(dtype: DType, shape: &[usize]) -> Result<Array> {
493        let failed = Self::failed(
494            Error::InvalidArgument(
495                "arrays cannot be evaluated while a function is traced (compile)".into(),
496            ),
497            dtype,
498            &[],
499        );
500        let primitive = Primitive::Broadcast(shape.to_vec());
501        let trace = Some(crate::transforms::PLACEHOLDER);
502        Ok(Self::record_unchecked(primitive, vec![failed], trace)?.swap_remove(0))
503    }
504
505    pub(crate) fn out(&self) -> &Output {
506        &self.node.outputs[self.output as usize]
507    }
508
509    /// The error this array evaluated to, if it has already failed.
510    pub(crate) fn slot_error(&self) -> Option<Error> {
511        match self.out().slot.get() {
512            Some(Err(e)) => Some(e.clone()),
513            _ => None,
514        }
515    }
516
517    // ── Failure (record time, never evaluates) ───────────────────────────────
518
519    /// Whether this is a failed array: the operation that produced it rejected its
520    /// arguments, or one of its inputs had failed, when it was recorded. O(1); errors that
521    /// happen during evaluation are not visible here (they surface from [`Array::eval`]).
522    pub fn is_failed(&self) -> bool {
523        self.node.failed
524    }
525
526    /// A failed array (0-D, float32) carrying the op error `kind`, located at the caller,
527    /// under the name `op`: for functions built from operations that validate their own
528    /// arguments and report failures the same way the operations do.
529    ///
530    /// ```
531    /// use nuvec_core::{Array, OpErrorKind};
532    ///
533    /// let a = Array::failure("my_layer", OpErrorKind::InvalidArgument("wrong cache".into()));
534    /// assert_eq!(a.check().unwrap_err().op_name(), Some("my_layer"));
535    /// ```
536    #[track_caller]
537    pub fn failure(op: &'static str, kind: crate::OpErrorKind) -> Array {
538        Self::failed(
539            Error::op(op, kind).locate(op, Location::caller()),
540            DType::F32,
541            &[],
542        )
543    }
544
545    /// The error of a failed array (see [`Array::is_failed`]), without evaluating.
546    pub fn error(&self) -> Option<&Error> {
547        match (self.node.failed, self.out().slot.get()) {
548            (true, Some(Err(e))) => Some(e),
549            _ => None,
550        }
551    }
552
553    /// `Ok(self)`, or the error of a failed array (see [`Array::is_failed`]). Never
554    /// evaluates: use it before host decisions that depend on shapes, and at the boundary
555    /// of functions that return `Result`.
556    pub fn check(self) -> Result<Array> {
557        if self.node.failed
558            && let Some(error) = self.slot_error()
559        {
560            return Err(error);
561        }
562        Ok(self)
563    }
564
565    // ── Metadata (never evaluates) ───────────────────────────────────────────
566
567    /// Dimension sizes (MLX's `Shape`: `i32`).
568    ///
569    /// A failed array keeps a best-effort shape: the one the operation would have
570    /// produced if that can be inferred, else its first operand's. Call
571    /// [`Array::check`] first when host logic depends on the shape.
572    pub fn shape(&self) -> &[i32] {
573        &self.out().ishape
574    }
575
576    /// The shape as an array of `N` sizes, for destructuring and host decisions
577    /// (`let [b, l, d] = x.shape_n::<3>()?;`): the array's error if it is a failed array
578    /// (whose shape is only a best guess), or an error if it does not have `N` dimensions.
579    ///
580    /// ```
581    /// use nuvec_core::array;
582    ///
583    /// let x = array![[1, 2, 3], [4, 5, 6]];
584    /// let [rows, cols] = x.shape_n::<2>().unwrap();
585    /// assert_eq!((rows, cols), (2, 3));
586    /// assert!(x.shape_n::<3>().is_err());
587    /// ```
588    #[track_caller]
589    pub fn shape_n<const N: usize>(&self) -> Result<[i32; N]> {
590        if let Some(e) = self.error() {
591            return Err(e.clone());
592        }
593        <[i32; N]>::try_from(self.shape()).map_err(|_| {
594            Error::InvalidArgument(format!(
595                "shape_n: expected {N} dimensions, the array has shape {:?}",
596                self.shape()
597            ))
598            .locate("shape_n", Location::caller())
599        })
600    }
601
602    /// Dimension sizes as `usize`, for nuvec's internals.
603    pub(crate) fn dims(&self) -> &[usize] {
604        &self.out().shape
605    }
606
607    /// Number of dimensions.
608    pub fn ndim(&self) -> usize {
609        self.out().shape.len()
610    }
611
612    /// Number of elements.
613    pub fn size(&self) -> usize {
614        self.out().size
615    }
616
617    /// Element type.
618    pub fn dtype(&self) -> DType {
619        self.out().dtype
620    }
621
622    /// Size of one element in bytes.
623    pub fn itemsize(&self) -> usize {
624        self.dtype().size()
625    }
626
627    /// Size of all elements in bytes.
628    pub fn nbytes(&self) -> usize {
629        self.size() * self.itemsize()
630    }
631
632    /// Per-dimension steps, in elements.
633    pub(crate) fn flags(&self) -> Flags {
634        self.out().flags
635    }
636
637    /// Per-dimension steps, in elements.
638    pub(crate) fn strides(&self) -> &[isize] {
639        &self.out().strides
640    }
641
642    /// The stream the producing operation was recorded on.
643    pub fn stream(&self) -> Stream {
644        self.node.stream
645    }
646
647    /// The device the producing operation runs on.
648    pub fn device(&self) -> Device {
649        self.node.stream.device
650    }
651
652    /// Whether the array has been evaluated (successfully or not).
653    pub fn is_evaluated(&self) -> bool {
654        self.out().slot.get().is_some()
655    }
656
657    // ── Evaluation and readback ──────────────────────────────────────────────
658
659    /// Evaluates this array, if it has not been evaluated yet.
660    pub fn eval(&self) -> Result<()> {
661        crate::eval(std::slice::from_ref(self))
662    }
663
664    /// The elements in row-major order, evaluating first if needed.
665    pub fn to_vec<T: Element>(&self) -> Result<Vec<T>> {
666        if let Some(e) = self.error() {
667            return Err(e.clone());
668        }
669        if T::DTYPE != self.dtype() {
670            return Err(Error::dtype_mismatch("to_vec", self.dtype(), T::DTYPE));
671        }
672        self.eval()?;
673        let data = self.data().expect("eval succeeded, so data is set");
674        let out = self.out();
675        if out.size == 0 {
676            Ok(Vec::new())
677        } else if out.flags.row_contiguous || out.size == 1 {
678            let start = data.offset.max(0) as usize * out.dtype.size();
679            Ok(T::read_bytes(
680                &data.buffer.bytes()[start..start + self.nbytes()],
681            ))
682        } else {
683            let gathered = crate::kernels::contiguous(out, data)?;
684            Ok(T::read_bytes(gathered.bytes()))
685        }
686    }
687
688    /// The single element of a one-element array, evaluating first if needed.
689    pub fn item<T: Element>(&self) -> Result<T> {
690        if let Some(e) = self.error() {
691            return Err(e.clone());
692        }
693        if self.size() != 1 {
694            return Err(Error::InvalidArgument(format!(
695                "item() needs exactly one element, the array has shape {:?}",
696                self.dims()
697            )));
698        }
699        if T::DTYPE != self.dtype() {
700            return Err(Error::dtype_mismatch("item", self.dtype(), T::DTYPE));
701        }
702        Ok(self.to_vec::<T>()?[0])
703    }
704
705    /// The identity of the memory the evaluated array reads, or `None` before evaluation
706    /// (and for a failed array). Arrays with equal ids share storage (views of one array, or
707    /// tensors of one mapped or read checkpoint file); an array's id stays the same while it
708    /// is alive. The analogue of comparing MLX's data pointers for aliasing; it is not an
709    /// address to read from.
710    ///
711    /// ```
712    /// use nuvec_core::{array, Array};
713    ///
714    /// let a = array![[1, 2], [3, 4]].add(1);
715    /// let row = a.i(1);
716    /// assert_eq!(a.buffer_id(), None); // not evaluated yet
717    /// a.eval().unwrap();
718    /// row.eval().unwrap();
719    /// assert!(a.buffer_id().is_some());
720    /// assert_eq!(row.buffer_id(), a.buffer_id()); // a view shares the storage
721    /// assert_ne!(Array::from(1).buffer_id(), a.buffer_id());
722    /// ```
723    pub fn buffer_id(&self) -> Option<usize> {
724        self.data().map(|d| Arc::as_ptr(&d.buffer) as usize)
725    }
726
727    /// The evaluated data, if evaluation succeeded.
728    pub(crate) fn data(&self) -> Option<&Data> {
729        self.out().slot.get().and_then(|r| r.as_ref().ok())
730    }
731}
732
733/// Never evaluates: shape, dtype, device and status; the values only when the array is
734/// already evaluated, the error of a failed array.
735impl fmt::Debug for Array {
736    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
737        let mut d = f.debug_struct("Array");
738        d.field("shape", &self.shape())
739            .field("dtype", &self.dtype())
740            .field("device", &self.device());
741        match self.out().slot.get() {
742            None => d.field("status", &"unevaluated"),
743            Some(Ok(_)) => match crate::display::values(self) {
744                Ok(values) => d.field("values", &format_args!("{values}")),
745                Err(_) => d.field("status", &"evaluated"),
746            },
747            Some(Err(e)) if self.node.failed => d.field("failed", &format_args!("{e}")),
748            Some(Err(e)) => d.field("error", &format_args!("{e}")),
749        };
750        d.finish()
751    }
752}
753
754/// Evaluates and prints the values as MLX and NumPy do: `array([[1, 2], [3, 4]], dtype=int32)`
755/// (more than 1,000 elements are summarised with the first and last 3 along each axis), or
756/// `<failed: …>` with the error.
757impl fmt::Display for Array {
758    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
759        match crate::display::values(self) {
760            Ok(values) => write!(f, "array({values}, dtype={})", self.dtype()),
761            Err(e) => write!(f, "<failed: {e}>"),
762        }
763    }
764}
765
766/// A cheap placeholder: a 0-D float32 zero. Handy with [`std::mem::take`] to update an array
767/// held in a struct by value, so the update can write in place:
768/// `self.k = std::mem::take(&mut self.k).at(..).set(&v);`.
769impl Default for Array {
770    fn default() -> Self {
771        crate::creation::from_data(&[0.0f32], &[])
772            .unwrap_or_else(|e| Self::failed(e, DType::F32, &[]))
773    }
774}
775
776/// Drops unevaluated graphs iteratively. Recursive destruction would overflow the stack
777/// on long chains: each node's last reference would drop its inputs from inside its own
778/// destructor.
779impl Drop for Node {
780    fn drop(&mut self) {
781        let mut stack = take_inputs(self);
782        while let Some(Array { node, .. }) = stack.pop() {
783            if let Some(mut node) = Arc::into_inner(node) {
784                stack.append(&mut take_inputs(&mut node));
785            }
786        }
787    }
788}
789
790fn take_inputs(node: &mut Node) -> Vec<Array> {
791    let state = node.state.get_mut().unwrap_or_else(PoisonError::into_inner);
792    match std::mem::replace(state, State::Done) {
793        State::Pending { inputs, .. } | State::Retained { inputs, .. } => inputs,
794        State::Running { .. } | State::Done | State::Failed { .. } => Vec::new(),
795    }
796}
797
798/// Graph accessors as methods, for the unit tests.
799#[cfg(test)]
800impl Array {
801    pub(crate) fn primitive_t(&self) -> Option<Primitive> {
802        crate::graph::primitive(self)
803    }
804    pub(crate) fn inputs_t(&self) -> Vec<Array> {
805        crate::graph::inputs(self)
806    }
807    pub(crate) fn id_t(&self) -> usize {
808        crate::graph::id(self)
809    }
810    pub(crate) fn siblings_t(&self) -> Vec<Array> {
811        crate::graph::siblings(self)
812    }
813    pub(crate) fn output_index_t(&self) -> usize {
814        crate::graph::output_index(self)
815    }
816    pub(crate) fn is_tracer_t(&self) -> bool {
817        crate::graph::is_tracer(self)
818    }
819}
820
821#[cfg(test)]
822mod tests {
823    use super::*;
824    use crate::OpErrorKind;
825
826    #[test]
827    fn graph_accessors_describe_pending_nodes() {
828        let a = Array::from_slice(&[1i32, 2], [2]);
829        let b = Array::from_slice(&[5i32, 7], [2]);
830        let outs =
831            crate::graph::from_primitive(Primitive::AddMultiply, vec![a.clone(), b]).unwrap();
832        let (sum, product) = (&outs[0], &outs[1]);
833        assert_eq!(sum.id_t(), product.id_t(), "siblings share their operation");
834        assert_ne!(sum.id_t(), a.id_t());
835        assert_eq!((sum.output_index_t(), product.output_index_t()), (0, 1));
836        assert_eq!(product.siblings_t().len(), 2);
837        assert_eq!(sum.inputs_t()[0].id_t(), a.id_t());
838        assert!(a.inputs_t().is_empty(), "host data has no inputs");
839        assert!(!sum.is_tracer_t());
840        product.eval().unwrap();
841        assert!(sum.inputs_t().is_empty(), "evaluated: detached");
842
843        let p = Array::placeholder(DType::F32, &[3]).unwrap();
844        let q = &p + 1.0f32;
845        assert!(p.is_tracer_t() && q.is_tracer_t());
846        assert_eq!(q.dims(), [3]);
847        assert!(q.eval().is_err(), "placeholders cannot be evaluated");
848    }
849
850    #[test]
851    fn failed_leaves_are_sticky_and_typed() {
852        let error = Error::size_mismatch("from_slice", 6, 5);
853        let f = Array::failed(error.clone(), DType::F16, &[2, 3]);
854        assert!(f.is_failed());
855        assert_eq!(f.shape(), [2, 3]);
856        assert_eq!(f.dtype(), DType::F16);
857        assert_eq!(f.eval(), Err(error.clone()));
858        assert_eq!(f.clone().check().unwrap_err(), error);
859        // Recording on a failed input records nothing and keeps the inferred metadata.
860        let g = crate::ops::imp::reshape(&f, &[3, 2]).unwrap();
861        assert!(g.is_failed() && g.primitive_t().is_none());
862        assert_eq!(g.shape(), [3, 2]);
863        assert_eq!(g.eval(), Err(error));
864        // A valid array is not failed, and `check` does not evaluate it.
865        let ok = &Array::from(1.0f32) + 1.0;
866        assert!(!ok.is_failed());
867        let ok = ok.check().unwrap();
868        assert!(!ok.is_evaluated());
869    }
870
871    #[test]
872    fn shapes_are_i32_and_bounded() {
873        let a = crate::ops::imp::zeros(&[2, 3, 4], DType::U8).unwrap();
874        let shape: &[i32] = a.shape();
875        assert_eq!(shape, [2, 3, 4]);
876        assert_eq!(a.shape_n::<3>().unwrap(), [2, 3, 4]);
877        assert!(a.shape_n::<2>().is_err(), "rank mismatch");
878        // A single dimension above i32::MAX is an invalid shape, never a wrap.
879        let big = crate::ops::imp::broadcast_to(&Array::from(0u8), &[1usize << 31]).unwrap_err();
880        assert!(
881            matches!(big.kind(), Some(OpErrorKind::InvalidShape(_))),
882            "{big}"
883        );
884        let fits = crate::ops::imp::broadcast_to(&Array::from(0u8), &[i32::MAX as usize]).unwrap();
885        assert_eq!(fits.shape(), [i32::MAX]);
886    }
887
888    #[test]
889    fn node_hasher_spreads_aligned_addresses() {
890        use std::hash::BuildHasher;
891        let build = BuildHasherDefault::<NodeHasher>::default();
892        // 336-byte nodes, as allocated: every low table index should be reachable.
893        let buckets: HashSet<u64> = (0..1024u64)
894            .map(|i| {
895                build.hash_one(std::ptr::without_provenance::<Node>(
896                    0x6000_0000_0000 + i as usize * 336,
897                )) & 1023
898            })
899            .collect();
900        // A random hash fills about 1 - 1/e ≈ 63% of them.
901        assert!(
902            buckets.len() > 600,
903            "only {} of 1024 buckets used",
904            buckets.len()
905        );
906    }
907
908    #[test]
909    fn metadata_without_eval() {
910        let a = Array::from_slice(&[1i32, 2, 3, 4, 5, 6], [2, 3]);
911        let c = crate::ops::imp::add(&a, &a).unwrap();
912        assert!(!c.is_evaluated());
913        assert_eq!(c.dims(), &[2, 3]);
914        assert_eq!(c.ndim(), 2);
915        assert_eq!(c.size(), 6);
916        assert_eq!(c.dtype(), DType::I32);
917        assert_eq!(c.itemsize(), 4);
918        assert_eq!(c.nbytes(), 24);
919        assert_eq!(c.strides(), &[3, 1]);
920        assert!(c.flags().row_contiguous);
921        assert_eq!(c.device(), crate::default_device());
922        assert_eq!(
923            c.primitive_t(),
924            Some(Primitive::Binary(crate::graph::BinaryOp::Add))
925        );
926        assert!(!c.is_evaluated(), "metadata queries must not evaluate");
927    }
928
929    #[test]
930    fn item_on_scalar_and_non_scalar() {
931        assert_eq!(Array::from(7u16).item::<u16>().unwrap(), 7);
932        let a = Array::from_slice(&[1.0f32, 2.0], [2]);
933        assert!(matches!(a.item::<f32>(), Err(Error::InvalidArgument(_))));
934        assert!(matches!(
935            Array::from(1.0f32).item::<f64>(),
936            Err(Error::Op {
937                kind: OpErrorKind::DTypeMismatch { .. },
938                ..
939            })
940        ));
941    }
942
943    #[test]
944    fn debug_does_not_evaluate() {
945        let a = Array::from(1i8);
946        let c = crate::ops::imp::add(&a, &a).unwrap();
947        assert!(format!("{c:?}").contains("unevaluated"));
948        assert!(!c.is_evaluated());
949    }
950}