Skip to main content

sim_lib_numbers_tensor/implementation/
value.rs

1//! The uniform `Tensor` value type: its shape, dtype, and cell storage, with
2//! indexing, construction, and number-value behavior backing the tensor domain.
3
4use std::sync::Arc;
5
6use sim_kernel::{
7    ClassRef, Cx, DefaultFactory, Error, Expr, Factory, NumberValue, Object, ObjectCompat,
8    ObjectEncode, ObjectEncoding, Result, Symbol, Value,
9};
10
11use super::citizen::tensor_value_class_symbol;
12use super::domain::number_domain;
13use super::storage::{BoxedTensorStorage, TensorLocation, TensorStorage};
14use super::validation::{
15    choose_dtype, validate_cells, validate_dtype_accepts_cells, validate_exact_cell_dtype,
16    validate_shape_and_data_len,
17};
18
19/// The uniform tensor value: an n-dimensional array of scalar number cells.
20///
21/// A tensor is row-major (last axis varies fastest) and homogeneous: every cell
22/// shares the [`dtype`](Tensor::dtype) number domain. An empty [`shape`](Tensor::shape)
23/// denotes a rank-0 scalar holding a single cell. Tensors are the value backing
24/// the `numbers/tensor` domain and are constructed through
25/// [`build_tensor_value`] rather than parsed from literals.
26#[derive(Clone)]
27pub struct Tensor {
28    /// Length of each axis, outermost first. Empty for a rank-0 scalar.
29    shape: Arc<[usize]>,
30    /// The shared scalar number domain of every cell (for example
31    /// `numbers/i64` or `numbers/f64`).
32    dtype: Symbol,
33    /// Row-major host or resident storage.
34    storage: Arc<dyn TensorStorage>,
35}
36
37impl Tensor {
38    /// Builds a tensor after checking shape, scalar-cell, and promotion
39    /// invariants against the loaded number-domain registry.
40    pub fn new_checked(
41        cx: &mut Cx,
42        shape: Vec<usize>,
43        dtype: Symbol,
44        data: Vec<Value>,
45    ) -> Result<Self> {
46        validate_shape_and_data_len(&shape, data.len())?;
47        validate_cells(cx, &data)?;
48        validate_dtype_accepts_cells(cx, &dtype, &data)?;
49        Self::from_storage(
50            shape,
51            dtype.clone(),
52            Arc::new(BoxedTensorStorage::new(dtype, data)),
53        )
54    }
55
56    /// Builds a tensor whose cells already exactly match `dtype`.
57    ///
58    /// Specialized tensor backends use this when converting packed storage into
59    /// uniform tensor cells without needing a loaded registry. It is stricter
60    /// than [`Tensor::new_checked`]: every cell must report exactly `dtype`.
61    pub fn new_exact(shape: Vec<usize>, dtype: Symbol, data: Vec<Value>) -> Result<Self> {
62        validate_shape_and_data_len(&shape, data.len())?;
63        validate_exact_cell_dtype(&dtype, &data)?;
64        Self::from_storage(
65            shape,
66            dtype.clone(),
67            Arc::new(BoxedTensorStorage::new(dtype, data)),
68        )
69    }
70
71    /// Builds a canonical tensor around externally supplied storage.
72    ///
73    /// This validates shape, dtype, and logical length without materializing
74    /// resident storage. The storage implementation is responsible for
75    /// returning scalar values consistent with its declared dtype.
76    pub fn from_storage(
77        shape: Vec<usize>,
78        dtype: Symbol,
79        storage: Arc<dyn TensorStorage>,
80    ) -> Result<Self> {
81        validate_shape_and_data_len(&shape, storage.len())?;
82        if storage.dtype() != &dtype {
83            return Err(Error::Eval(format!(
84                "tensor dtype {dtype} does not match storage dtype {}",
85                storage.dtype()
86            )));
87        }
88        Ok(Self {
89            shape: shape.into(),
90            dtype,
91            storage,
92        })
93    }
94
95    /// The tensor shape, outermost axis first. Empty means a rank-0 scalar.
96    pub fn shape(&self) -> &[usize] {
97        &self.shape
98    }
99
100    /// The shared scalar number domain accepted by every tensor cell.
101    pub fn dtype(&self) -> &Symbol {
102        &self.dtype
103    }
104
105    /// The current host or resident storage location.
106    pub fn location(&self) -> TensorLocation {
107        self.storage.location()
108    }
109
110    /// Borrows the open storage implementation.
111    ///
112    /// Typed adapters can safely downcast through
113    /// [`TensorStorage::as_any`], while execution providers can preserve
114    /// storage identity by cloning this `Arc`.
115    pub fn storage(&self) -> &Arc<dyn TensorStorage> {
116        &self.storage
117    }
118
119    /// The logical row-major cell count.
120    pub fn len(&self) -> usize {
121        self.storage.len()
122    }
123
124    /// Whether this tensor has no logical cells.
125    pub fn is_empty(&self) -> bool {
126        self.storage.is_empty()
127    }
128
129    /// Observes one row-major scalar cell.
130    pub fn cell(&self, index: usize) -> Result<Value> {
131        if index >= self.len() {
132            return Err(Error::Eval(
133                "tensor cell index was out of bounds".to_owned(),
134            ));
135        }
136        self.storage.cell(index)
137    }
138
139    /// Materializes storage into a checked host-observable form.
140    pub fn materialize(&self) -> Result<Arc<dyn TensorStorage>> {
141        let storage = if self.storage.location() == TensorLocation::Host {
142            self.storage.clone()
143        } else {
144            self.storage.materialize()?
145        };
146        if storage.location() != TensorLocation::Host {
147            return Err(Error::Eval(
148                "tensor materialization did not produce host storage".to_owned(),
149            ));
150        }
151        if storage.dtype() != &self.dtype {
152            return Err(Error::Eval(format!(
153                "materialized tensor dtype {} does not match {}",
154                storage.dtype(),
155                self.dtype
156            )));
157        }
158        if storage.len() != self.len() {
159            return Err(Error::Eval(format!(
160                "materialized tensor length {} does not match {}",
161                storage.len(),
162                self.len()
163            )));
164        }
165        Ok(storage)
166    }
167
168    /// Observes all row-major scalar cells.
169    ///
170    /// Boxed host storage returns its shared cell slice directly. Other host
171    /// layouts are read through the open storage contract after one checked
172    /// materialization.
173    pub fn cells(&self) -> Result<Arc<[Value]>> {
174        let storage = self.materialize()?;
175        if let Some(boxed) = storage.as_any().downcast_ref::<BoxedTensorStorage>() {
176            return Ok(boxed.cells());
177        }
178        (0..storage.len())
179            .map(|index| storage.cell(index))
180            .collect::<Result<Vec<_>>>()
181            .map(Arc::from)
182    }
183
184    /// The number of axes, i.e. the length of [`shape`](Tensor::shape). Zero
185    /// for a scalar.
186    pub fn rank(&self) -> usize {
187        self.shape.len()
188    }
189
190    /// Computes the row-major flat offset into storage for a
191    /// multi-dimensional `indices` coordinate against `shape`.
192    ///
193    /// Returns an error if the index rank does not match `shape` or any
194    /// component is out of bounds.
195    ///
196    /// # Examples
197    ///
198    /// ```
199    /// use sim_lib_numbers_tensor::Tensor;
200    ///
201    /// // Row-major 2x3 tensor: element (1, 2) is at flat offset 5.
202    /// assert_eq!(Tensor::flat_offset(&[2, 3], &[1, 2]).unwrap(), 5);
203    /// assert_eq!(Tensor::flat_offset(&[2, 3], &[0, 0]).unwrap(), 0);
204    /// // Out-of-bounds and rank-mismatched indices are rejected.
205    /// assert!(Tensor::flat_offset(&[2, 3], &[2, 0]).is_err());
206    /// assert!(Tensor::flat_offset(&[2, 3], &[0]).is_err());
207    /// ```
208    pub fn flat_offset(shape: &[usize], indices: &[usize]) -> Result<usize> {
209        if shape.len() != indices.len() {
210            return Err(Error::Eval("tensor index rank mismatch".to_owned()));
211        }
212        let mut stride = 1usize;
213        let mut offset = 0usize;
214        for (dim, index) in shape.iter().rev().zip(indices.iter().rev()) {
215            if *index >= *dim {
216                return Err(Error::Eval("tensor index was out of bounds".to_owned()));
217            }
218            offset += index * stride;
219            stride = stride.saturating_mul(*dim);
220        }
221        Ok(offset)
222    }
223
224    /// Enumerates every multi-dimensional coordinate of `shape` in row-major
225    /// order. An empty shape yields a single empty coordinate (the scalar cell).
226    pub fn coordinates(shape: &[usize]) -> Vec<Vec<usize>> {
227        if shape.is_empty() {
228            return vec![Vec::new()];
229        }
230        if shape.contains(&0) {
231            return Vec::new();
232        }
233        let mut out = Vec::new();
234        let mut coord = vec![0usize; shape.len()];
235        loop {
236            out.push(coord.clone());
237            let mut axis = shape.len();
238            while axis > 0 {
239                axis -= 1;
240                coord[axis] += 1;
241                if coord[axis] < shape[axis] {
242                    break;
243                }
244                coord[axis] = 0;
245                if axis == 0 {
246                    return out;
247                }
248            }
249        }
250    }
251}
252
253impl Object for Tensor {
254    fn display(&self, cx: &mut Cx) -> Result<String> {
255        match self.as_expr(cx)? {
256            Expr::Call { .. } => Ok(format!("{}<{:?}>", tensor_display_name(), self.shape)),
257            expr => Ok(format!("{expr:?}")),
258        }
259    }
260
261    fn as_any(&self) -> &dyn std::any::Any {
262        self
263    }
264}
265
266impl sim_kernel::ObjectCompat for Tensor {
267    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
268        if let Some(value) = cx.registry().class_by_symbol(&tensor_value_class_symbol()) {
269            return Ok(value.clone());
270        }
271        if let Some(value) = cx
272            .registry()
273            .class_by_symbol(&Symbol::qualified("core", "Number"))
274        {
275            return Ok(value.clone());
276        }
277        DefaultFactory.class_stub(
278            sim_kernel::CORE_NUMBER_CLASS_ID,
279            Symbol::qualified("core", "Number"),
280        )
281    }
282    fn as_expr(&self, cx: &mut Cx) -> Result<Expr> {
283        let cells = self.cells()?;
284        match self.rank() {
285            0 => Ok(Expr::Call {
286                operator: Box::new(Expr::Symbol(Symbol::new("scalar"))),
287                args: vec![
288                    cells
289                        .first()
290                        .ok_or_else(|| Error::Eval("scalar tensor is missing its cell".to_owned()))?
291                        .object()
292                        .as_expr(cx)?,
293                ],
294            }),
295            1 => Ok(Expr::Vector(exprs(cx, &cells)?)),
296            2 => {
297                let width = self.shape[1];
298                let rows = if width == 0 {
299                    vec![Expr::Vector(Vec::new()); self.shape[0]]
300                } else {
301                    cells
302                        .chunks(width)
303                        .map(|row| exprs(cx, row).map(Expr::Vector))
304                        .collect::<Result<Vec<_>>>()?
305                };
306                Ok(Expr::Vector(rows))
307            }
308            _ => Ok(Expr::Call {
309                operator: Box::new(Expr::Symbol(Symbol::new("tensor"))),
310                args: vec![
311                    Expr::Vector(
312                        self.shape
313                            .iter()
314                            .map(|dim| Expr::String(dim.to_string()))
315                            .collect(),
316                    ),
317                    Expr::Symbol(self.dtype.clone()),
318                    Expr::Vector(exprs(cx, &cells)?),
319                ],
320            }),
321        }
322    }
323    fn as_table(&self, cx: &mut Cx) -> Result<Value> {
324        let shape = cx.factory().list(
325            self.shape
326                .iter()
327                .map(|dim| cx.factory().string(dim.to_string()))
328                .collect::<Result<Vec<_>>>()?,
329        )?;
330        let data = cx.factory().list(self.cells()?.to_vec())?;
331        cx.factory().table(vec![
332            (
333                Symbol::new("kind"),
334                cx.factory().string("tensor".to_owned())?,
335            ),
336            (Symbol::new("shape"), shape),
337            (
338                Symbol::new("dtype"),
339                cx.factory().symbol(self.dtype.clone())?,
340            ),
341            (Symbol::new("data"), data),
342        ])
343    }
344    fn as_number_value(&self) -> Option<&dyn NumberValue> {
345        Some(self)
346    }
347
348    fn as_object_encoder(&self) -> Option<&dyn ObjectEncode> {
349        Some(self)
350    }
351}
352
353impl NumberValue for Tensor {
354    fn number_domain(&self, _cx: &mut Cx) -> Result<Symbol> {
355        Ok(number_domain())
356    }
357}
358
359impl ObjectEncode for Tensor {
360    fn object_encoding(&self, cx: &mut Cx) -> Result<ObjectEncoding> {
361        let cells = self.cells()?;
362        Ok(ObjectEncoding::Constructor {
363            class: tensor_value_class_symbol(),
364            args: vec![
365                Expr::Symbol(Symbol::new("v1")),
366                Expr::List(
367                    self.shape
368                        .iter()
369                        .map(|dim| {
370                            Expr::Number(sim_kernel::NumberLiteral {
371                                domain: Symbol::qualified("citizen", "int"),
372                                canonical: dim.to_string(),
373                            })
374                        })
375                        .collect(),
376                ),
377                Expr::List(exprs(cx, &cells)?),
378                Expr::Symbol(self.dtype.clone()),
379            ],
380        })
381    }
382}
383
384impl sim_citizen::Citizen for Tensor {
385    fn citizen_symbol() -> Symbol {
386        tensor_value_class_symbol()
387    }
388
389    fn citizen_version() -> u32 {
390        1
391    }
392
393    fn citizen_arity() -> usize {
394        3
395    }
396
397    fn citizen_fields() -> &'static [&'static str] {
398        &["shape", "data", "domain"]
399    }
400}
401
402/// Builds a tensor [`Value`] of the given `shape` from row-major `data` cells.
403///
404/// The cell count must equal the product of `shape` (one for an empty, scalar
405/// shape). Every cell must be a scalar number value (not a nested tensor). When
406/// `dtype_hint` is `Some`, all cells must promote to that domain; otherwise the
407/// element domain is chosen as the cheapest join of the cell domains. Returns an
408/// error on a cell-count mismatch, a non-scalar cell, or an impossible dtype.
409pub fn build_tensor_value(
410    cx: &mut Cx,
411    shape: Vec<usize>,
412    dtype_hint: Option<Symbol>,
413    data: Vec<Value>,
414) -> Result<Value> {
415    validate_shape_and_data_len(&shape, data.len())?;
416    let dtype = choose_dtype(cx, dtype_hint, &data)?;
417    let tensor = Tensor::new_checked(cx, shape, dtype, data)?;
418    cx.factory().opaque(Arc::new(tensor))
419}
420
421/// Builds a rank-0 scalar tensor wrapping a single scalar number `value`.
422pub fn build_scalar_tensor_value(cx: &mut Cx, value: Value) -> Result<Value> {
423    build_tensor_value(cx, Vec::new(), None, vec![value])
424}
425
426/// Borrows the [`Tensor`] backing a value, or `None` if it is not a tensor.
427pub fn tensor_value_ref(value: &Value) -> Option<&Tensor> {
428    value.object().downcast_ref::<Tensor>()
429}
430
431/// The shared element number domain (dtype) of a tensor's cells.
432pub fn tensor_dtype(tensor: &Tensor) -> &Symbol {
433    tensor.dtype()
434}
435
436/// Observes a tensor's row-major scalar cells as a shared flat slice.
437pub fn flatten_tensor_scalar_cells(tensor: &Tensor) -> Result<Arc<[Value]>> {
438    tensor.cells()
439}
440
441pub fn tensor_display_name() -> &'static str {
442    "tensor"
443}
444
445fn exprs(cx: &mut Cx, data: &[Value]) -> Result<Vec<Expr>> {
446    data.iter()
447        .map(|value| value.object().as_expr(cx))
448        .collect()
449}