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/// The largest number of cells a tensor operation will materialize in one
135/// allocation. A dimension product can be far below `usize::MAX` and still be
136/// hopeless to allocate (a `[1_000_000, 1_000_000]` broadcast is `1e12` cells);
137/// this ceiling is the line past which the input is rejected rather than driven
138/// into an out-of-memory abort.
139pub const MAX_TENSOR_CELLS: usize = 1 << 28;
140
141/// The number of cells in a tensor of the given shape, failing closed both when
142/// the dimension product overflows `usize` (via [`checked_element_count`]) and
143/// when it exceeds [`MAX_TENSOR_CELLS`].
144///
145/// This is the form to use before sizing an allocation from untrusted
146/// dimensions -- a broadcast result shape, a `zeros`/`ones`/`eye` size -- where a
147/// legal-but-hostile shape whose product still fits in `usize` would otherwise
148/// OOM the process.
149///
150/// # Examples
151///
152/// ```
153/// use sim_lib_numbers_tensor::bounded_element_count;
154///
155/// assert_eq!(bounded_element_count(&[2, 3]).unwrap(), 6); // 2x3 matrix
156/// assert!(bounded_element_count(&[usize::MAX, 2]).is_err()); // overflow
157/// assert!(bounded_element_count(&[1_000_000, 1_000_000]).is_err()); // over ceiling
158/// ```
159pub fn bounded_element_count(shape: &[usize]) -> Result<usize> {
160    let cells = checked_element_count(shape)?;
161    if cells > MAX_TENSOR_CELLS {
162        return Err(sim_kernel::Error::Eval(format!(
163            "tensor shape {shape:?} has {cells} cells, exceeding the {MAX_TENSOR_CELLS}-cell limit"
164        )));
165    }
166    Ok(cells)
167}
168
169/// Extracts the canonical [`NumberLiteral`] of a scalar tensor cell `value`, or
170/// `None` if the value is not a number. Shared backing for the typed
171/// literal-cell parsers below.
172pub fn number_literal_for_tensor_cell(value: &Value) -> Option<NumberLiteral> {
173    let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
174    value
175        .object()
176        .as_number_value()?
177        .number_literal(&mut cx)
178        .ok()?
179}
180
181/// Parses a tensor cell as an `i64`, returning `None` unless it is a number in
182/// the `numbers/i64` domain whose canonical form parses cleanly.
183pub fn parse_i64_literal_cell(value: &Value) -> Option<i64> {
184    let literal = number_literal_for_tensor_cell(value)?;
185    (literal.domain == domains::i64())
186        .then(|| literal.canonical.parse::<i64>().ok())
187        .flatten()
188}
189
190/// Parses a tensor cell as an `f64`, returning `None` unless it is a number in
191/// the `numbers/f64` domain whose canonical form parses cleanly.
192pub fn parse_f64_literal_cell(value: &Value) -> Option<f64> {
193    let literal = number_literal_for_tensor_cell(value)?;
194    (literal.domain == domains::f64())
195        .then(|| literal.canonical.parse::<f64>().ok())
196        .flatten()
197}
198
199/// Parses a tensor cell as a `(numerator, denominator)` rational pair,
200/// returning `None` unless it is a number in the `numbers/rational` domain
201/// whose canonical `num/den` form parses cleanly.
202pub fn parse_rational_literal_cell(value: &Value) -> Option<(i64, i64)> {
203    let literal = number_literal_for_tensor_cell(value)?;
204    if literal.domain != domains::rational() {
205        return None;
206    }
207    let (num, den) = literal.canonical.split_once('/')?;
208    Some((num.parse::<i64>().ok()?, den.parse::<i64>().ok()?))
209}
210
211/// Parses a tensor cell as a `(real, imaginary)` pair, returning `None` unless
212/// it is a number in the `numbers/complex` domain whose canonical `a+bi` form
213/// parses cleanly.
214pub fn parse_complex_literal_cell(value: &Value) -> Option<(f64, f64)> {
215    let literal = number_literal_for_tensor_cell(value)?;
216    if literal.domain != domains::complex() {
217        return None;
218    }
219    let text = literal.canonical.strip_suffix('i')?;
220    let split = text
221        .char_indices()
222        .skip(1)
223        .find(|(_, ch)| *ch == '+' || *ch == '-')
224        .map(|(index, _)| index)?;
225    let (real, imag) = text.split_at(split);
226    Some((real.parse::<f64>().ok()?, imag.parse::<f64>().ok()?))
227}