Skip to main content

sim_lib_numbers_tensor/
spec.rs

1//! The `SpecTensor` interface and descriptor types that let specialized
2//! element-type backends convert to and from the uniform `Tensor` storage, plus
3//! literal-cell parsing helpers shared across those backends.
4
5use std::sync::Arc;
6
7use sim_kernel::{
8    Cx, DefaultFactory, Factory, NoopEvalPolicy, NumberLiteral, Result, Symbol, Value,
9};
10
11use crate::Tensor;
12use sim_lib_numbers_core::domains;
13
14/// Interface a specialized element-type tensor backend implements to bridge its
15/// own storage and the uniform [`Tensor`] value.
16///
17/// Typed backends (for example dense `f64` or `i64` tensors) keep their own
18/// packed representation and use this trait to convert to and from the shared
19/// uniform storage the `numbers/tensor` domain operates on.
20pub trait SpecTensor: Send + Sync + 'static {
21    /// The length of each axis of the specialized tensor, outermost first.
22    fn shape(&self) -> &[usize];
23    /// The element number domain (dtype) of the specialized tensor's cells.
24    fn dtype(&self) -> Symbol;
25    /// Converts this specialized tensor into the uniform [`Tensor`] storage.
26    fn to_uniform(&self) -> Tensor;
27    /// Rebuilds a specialized tensor from uniform storage, or `None` if the
28    /// uniform tensor's dtype or shape does not fit this backend.
29    fn from_uniform(tensor: &Tensor) -> Option<Self>
30    where
31        Self: Sized;
32}
33
34/// Metadata describing one registered `SpecTensor` backend, surfaced as a
35/// descriptor value so the registry can advertise the specialized tensor.
36#[derive(Clone, Debug, PartialEq, Eq)]
37pub struct SpecTensorDescriptor {
38    /// The symbol under which the backend's descriptor value is installed.
39    pub symbol: Symbol,
40    /// The element number domain (dtype) the backend specializes on.
41    pub dtype: Symbol,
42    /// Human-readable name of the implementing crate or strategy.
43    pub implementation: &'static str,
44    /// Human-readable description of the backend's storage layout.
45    pub storage: &'static str,
46}
47
48/// Builds a descriptor symbol (`numbers/tensor-spec/<name>`) for a specialized
49/// tensor backend.
50///
51/// # Examples
52///
53/// ```
54/// use sim_lib_numbers_tensor::spec_tensor_symbol;
55///
56/// let symbol = spec_tensor_symbol("dense-f64");
57/// assert_eq!(symbol.to_string(), "numbers/tensor-spec/dense-f64");
58/// ```
59pub fn spec_tensor_symbol(name: &str) -> Symbol {
60    Symbol::qualified("numbers/tensor-spec", name)
61}
62
63/// Encodes a [`SpecTensorDescriptor`] as a registry descriptor table value with
64/// `kind`, `symbol`, `dtype`, `implementation`, and `storage` entries.
65pub fn spec_tensor_descriptor_value(
66    factory: &dyn Factory,
67    descriptor: SpecTensorDescriptor,
68) -> Result<Value> {
69    factory.table(vec![
70        (
71            Symbol::new("kind"),
72            factory.string("spec-tensor".to_owned())?,
73        ),
74        (Symbol::new("symbol"), factory.symbol(descriptor.symbol)?),
75        (Symbol::new("dtype"), factory.symbol(descriptor.dtype)?),
76        (
77            Symbol::new("implementation"),
78            factory.string(descriptor.implementation.to_owned())?,
79        ),
80        (
81            Symbol::new("storage"),
82            factory.string(descriptor.storage.to_owned())?,
83        ),
84    ])
85}
86
87/// The number of cells in a tensor of the given shape. An empty shape is a
88/// scalar (one cell). This is the one home for the `element_count` helper that
89/// the generic, broadcast, linalg, and every typed tensor crate re-grew.
90///
91/// # Examples
92///
93/// ```
94/// use sim_lib_numbers_tensor::element_count;
95///
96/// assert_eq!(element_count(&[]), 1); // rank-0 scalar
97/// assert_eq!(element_count(&[3]), 3); // length-3 vector
98/// assert_eq!(element_count(&[2, 3]), 6); // 2x3 matrix
99/// ```
100pub fn element_count(shape: &[usize]) -> usize {
101    if shape.is_empty() {
102        1
103    } else {
104        shape.iter().product()
105    }
106}
107
108/// The number of cells in a tensor of the given shape, failing closed when the
109/// dimension product overflows `usize` instead of wrapping (release) or
110/// panicking (debug).
111///
112/// [`element_count`] assumes an already-validated shape; this is the form to use
113/// at the untrusted-input boundary -- for example a user-supplied `reshape`
114/// shape parsed from arbitrary dimensions -- where a hostile dimension product
115/// would otherwise overflow.
116///
117/// # Examples
118///
119/// ```
120/// use sim_lib_numbers_tensor::checked_element_count;
121///
122/// assert_eq!(checked_element_count(&[]).unwrap(), 1); // rank-0 scalar
123/// assert_eq!(checked_element_count(&[2, 3]).unwrap(), 6); // 2x3 matrix
124/// assert!(checked_element_count(&[usize::MAX, 2]).is_err()); // overflow
125/// ```
126pub fn checked_element_count(shape: &[usize]) -> Result<usize> {
127    shape.iter().try_fold(1_usize, |acc, &dim| {
128        acc.checked_mul(dim).ok_or_else(|| {
129            sim_kernel::Error::Eval(format!("tensor shape {shape:?} cell count overflows usize"))
130        })
131    })
132}
133
134/// Extracts the canonical [`NumberLiteral`] of a scalar tensor cell `value`, or
135/// `None` if the value is not a number. Shared backing for the typed
136/// literal-cell parsers below.
137pub fn number_literal_for_tensor_cell(value: &Value) -> Option<NumberLiteral> {
138    let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
139    value
140        .object()
141        .as_number_value()?
142        .number_literal(&mut cx)
143        .ok()?
144}
145
146/// Parses a tensor cell as an `i64`, returning `None` unless it is a number in
147/// the `numbers/i64` domain whose canonical form parses cleanly.
148pub fn parse_i64_literal_cell(value: &Value) -> Option<i64> {
149    let literal = number_literal_for_tensor_cell(value)?;
150    (literal.domain == domains::i64())
151        .then(|| literal.canonical.parse::<i64>().ok())
152        .flatten()
153}
154
155/// Parses a tensor cell as an `f64`, returning `None` unless it is a number in
156/// the `numbers/f64` domain whose canonical form parses cleanly.
157pub fn parse_f64_literal_cell(value: &Value) -> Option<f64> {
158    let literal = number_literal_for_tensor_cell(value)?;
159    (literal.domain == domains::f64())
160        .then(|| literal.canonical.parse::<f64>().ok())
161        .flatten()
162}
163
164/// Parses a tensor cell as a `(numerator, denominator)` rational pair,
165/// returning `None` unless it is a number in the `numbers/rational` domain
166/// whose canonical `num/den` form parses cleanly.
167pub fn parse_rational_literal_cell(value: &Value) -> Option<(i64, i64)> {
168    let literal = number_literal_for_tensor_cell(value)?;
169    if literal.domain != domains::rational() {
170        return None;
171    }
172    let (num, den) = literal.canonical.split_once('/')?;
173    Some((num.parse::<i64>().ok()?, den.parse::<i64>().ok()?))
174}
175
176/// Parses a tensor cell as a `(real, imaginary)` pair, returning `None` unless
177/// it is a number in the `numbers/complex` domain whose canonical `a+bi` form
178/// parses cleanly.
179pub fn parse_complex_literal_cell(value: &Value) -> Option<(f64, f64)> {
180    let literal = number_literal_for_tensor_cell(value)?;
181    if literal.domain != domains::complex() {
182        return None;
183    }
184    let text = literal.canonical.strip_suffix('i')?;
185    let split = text
186        .char_indices()
187        .skip(1)
188        .find(|(_, ch)| *ch == '+' || *ch == '-')
189        .map(|(index, _)| index)?;
190    let (real, imag) = text.split_at(split);
191    Some((real.parse::<f64>().ok()?, imag.parse::<f64>().ok()?))
192}