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}