sim_lib_numbers_tensor/spec.rs
1//! The `SpecTensor` interface and descriptor types that let specialized
2//! element-type backends expose typed views over the uniform `Tensor` storage,
3//! plus literal-cell parsing helpers shared across those backends.
4
5use std::sync::Arc;
6
7use half::{bf16, f16};
8use sim_kernel::{
9 Cx, DefaultFactory, Factory, NoopEvalPolicy, NumberLiteral, Result, Symbol, Value,
10};
11
12use crate::Tensor;
13use sim_lib_numbers_core::domains;
14
15/// Interface a specialized element-type tensor backend implements to bridge its
16/// typed view and the uniform [`Tensor`] value.
17///
18/// Typed backends (for example dense `f64` or `i64` tensors) wrap the canonical
19/// `Tensor` value and use typed [`TensorStorage`](crate::TensorStorage)
20/// implementations for their native cells. Conversions should clone that
21/// canonical tensor when storage already matches the backend, preserving shared
22/// storage identity for runtime projection and Citizen round-trips.
23pub trait SpecTensor: Send + Sync + 'static {
24 /// The length of each axis of the specialized tensor, outermost first.
25 fn shape(&self) -> &[usize];
26 /// The element number domain (dtype) of the specialized tensor's cells.
27 fn dtype(&self) -> Symbol;
28 /// Returns the canonical uniform [`Tensor`] backing this typed view.
29 fn to_uniform(&self) -> Tensor;
30 /// Rebuilds a specialized tensor view from uniform storage, or `None` if
31 /// the uniform tensor's dtype or shape does not fit this backend.
32 fn from_uniform(tensor: &Tensor) -> Option<Self>
33 where
34 Self: Sized;
35}
36
37/// Metadata describing one registered `SpecTensor` backend, surfaced as a
38/// descriptor value so the registry can advertise the specialized tensor.
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct SpecTensorDescriptor {
41 /// The symbol under which the backend's descriptor value is installed.
42 pub symbol: Symbol,
43 /// The element number domain (dtype) the backend specializes on.
44 pub dtype: Symbol,
45 /// Human-readable name of the implementing crate or strategy.
46 pub implementation: &'static str,
47 /// Human-readable description of the backend's storage layout.
48 pub storage: &'static str,
49}
50
51/// Builds a descriptor symbol (`numbers/tensor-spec/<name>`) for a specialized
52/// tensor backend.
53///
54/// # Examples
55///
56/// ```
57/// use sim_lib_numbers_tensor::spec_tensor_symbol;
58///
59/// let symbol = spec_tensor_symbol("dense-f64");
60/// assert_eq!(symbol.to_string(), "numbers/tensor-spec/dense-f64");
61/// ```
62pub fn spec_tensor_symbol(name: &str) -> Symbol {
63 Symbol::qualified("numbers/tensor-spec", name)
64}
65
66/// Encodes a [`SpecTensorDescriptor`] as a registry descriptor table value with
67/// `kind`, `symbol`, `dtype`, `implementation`, and `storage` entries.
68pub fn spec_tensor_descriptor_value(
69 factory: &dyn Factory,
70 descriptor: SpecTensorDescriptor,
71) -> Result<Value> {
72 factory.table(vec![
73 (
74 Symbol::new("kind"),
75 factory.string("spec-tensor".to_owned())?,
76 ),
77 (Symbol::new("symbol"), factory.symbol(descriptor.symbol)?),
78 (Symbol::new("dtype"), factory.symbol(descriptor.dtype)?),
79 (
80 Symbol::new("implementation"),
81 factory.string(descriptor.implementation.to_owned())?,
82 ),
83 (
84 Symbol::new("storage"),
85 factory.string(descriptor.storage.to_owned())?,
86 ),
87 ])
88}
89
90/// The number of cells in a tensor of the given shape. An empty shape is a
91/// scalar (one cell). This is the one home for the `element_count` helper that
92/// the generic, broadcast, linalg, and every typed tensor crate re-grew.
93///
94/// # Examples
95///
96/// ```
97/// use sim_lib_numbers_tensor::element_count;
98///
99/// assert_eq!(element_count(&[]), 1); // rank-0 scalar
100/// assert_eq!(element_count(&[3]), 3); // length-3 vector
101/// assert_eq!(element_count(&[2, 3]), 6); // 2x3 matrix
102/// ```
103pub fn element_count(shape: &[usize]) -> usize {
104 if shape.is_empty() {
105 1
106 } else {
107 shape.iter().product()
108 }
109}
110
111/// The number of cells in a tensor of the given shape, failing closed when the
112/// dimension product overflows `usize` instead of wrapping (release) or
113/// panicking (debug).
114///
115/// [`element_count`] assumes an already-validated shape; this is the form to use
116/// at the untrusted-input boundary -- for example a user-supplied `reshape`
117/// shape parsed from arbitrary dimensions -- where a hostile dimension product
118/// would otherwise overflow.
119///
120/// # Examples
121///
122/// ```
123/// use sim_lib_numbers_tensor::checked_element_count;
124///
125/// assert_eq!(checked_element_count(&[]).unwrap(), 1); // rank-0 scalar
126/// assert_eq!(checked_element_count(&[2, 3]).unwrap(), 6); // 2x3 matrix
127/// assert!(checked_element_count(&[usize::MAX, 2]).is_err()); // overflow
128/// ```
129pub fn checked_element_count(shape: &[usize]) -> Result<usize> {
130 shape.iter().try_fold(1_usize, |acc, &dim| {
131 acc.checked_mul(dim).ok_or_else(|| {
132 sim_kernel::Error::Eval(format!("tensor shape {shape:?} cell count overflows usize"))
133 })
134 })
135}
136
137/// The largest number of cells a tensor operation will materialize in one
138/// allocation. A dimension product can be far below `usize::MAX` and still be
139/// hopeless to allocate (a `[1_000_000, 1_000_000]` broadcast is `1e12` cells);
140/// this ceiling is the line past which the input is rejected rather than driven
141/// into an out-of-memory abort.
142pub const MAX_TENSOR_CELLS: usize = 1 << 28;
143
144/// The number of cells in a tensor of the given shape, failing closed both when
145/// the dimension product overflows `usize` (via [`checked_element_count`]) and
146/// when it exceeds [`MAX_TENSOR_CELLS`].
147///
148/// This is the form to use before sizing an allocation from untrusted
149/// dimensions -- a broadcast result shape, a `zeros`/`ones`/`eye` size -- where a
150/// legal-but-hostile shape whose product still fits in `usize` would otherwise
151/// OOM the process.
152///
153/// # Examples
154///
155/// ```
156/// use sim_lib_numbers_tensor::bounded_element_count;
157///
158/// assert_eq!(bounded_element_count(&[2, 3]).unwrap(), 6); // 2x3 matrix
159/// assert!(bounded_element_count(&[usize::MAX, 2]).is_err()); // overflow
160/// assert!(bounded_element_count(&[1_000_000, 1_000_000]).is_err()); // over ceiling
161/// ```
162pub fn bounded_element_count(shape: &[usize]) -> Result<usize> {
163 let cells = checked_element_count(shape)?;
164 if cells > MAX_TENSOR_CELLS {
165 return Err(sim_kernel::Error::Eval(format!(
166 "tensor shape {shape:?} has {cells} cells, exceeding the {MAX_TENSOR_CELLS}-cell limit"
167 )));
168 }
169 Ok(cells)
170}
171
172/// Extracts the canonical [`NumberLiteral`] of a scalar tensor cell `value`, or
173/// `None` if the value is not a number. Shared backing for the typed
174/// literal-cell parsers below.
175pub fn number_literal_for_tensor_cell(value: &Value) -> Option<NumberLiteral> {
176 let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
177 value
178 .object()
179 .as_number_value()?
180 .number_literal(&mut cx)
181 .ok()?
182}
183
184/// Parses a tensor cell as an `i64`, returning `None` unless it is a number in
185/// the `numbers/i64` domain whose canonical form parses cleanly.
186pub fn parse_i64_literal_cell(value: &Value) -> Option<i64> {
187 let literal = number_literal_for_tensor_cell(value)?;
188 (literal.domain == domains::i64())
189 .then(|| literal.canonical.parse::<i64>().ok())
190 .flatten()
191}
192
193/// Parses a tensor cell as an `f64`, returning `None` unless it is a number in
194/// the `numbers/f64` domain whose canonical form parses cleanly.
195pub fn parse_f64_literal_cell(value: &Value) -> Option<f64> {
196 let literal = number_literal_for_tensor_cell(value)?;
197 (literal.domain == domains::f64())
198 .then(|| literal.canonical.parse::<f64>().ok())
199 .flatten()
200}
201
202/// Parses a tensor cell as an `f32`, returning `None` unless it is a number in
203/// the `numbers/f32` domain whose canonical form parses cleanly.
204pub fn parse_f32_literal_cell(value: &Value) -> Option<f32> {
205 let literal = number_literal_for_tensor_cell(value)?;
206 (literal.domain == domains::f32())
207 .then(|| literal.canonical.parse::<f32>().ok())
208 .flatten()
209}
210
211/// Parses a tensor cell as an IEEE half-precision value, returning `None`
212/// unless it is a number in the `numbers/f16` domain whose canonical f32-form
213/// text parses cleanly.
214pub fn parse_f16_literal_cell(value: &Value) -> Option<f16> {
215 let literal = number_literal_for_tensor_cell(value)?;
216 (literal.domain == domains::f16())
217 .then(|| literal.canonical.parse::<f32>().ok().map(f16::from_f32))
218 .flatten()
219}
220
221/// Parses a tensor cell as a bfloat16 value, returning `None` unless it is a
222/// number in the `numbers/bf16` domain whose canonical f32-form text parses
223/// cleanly.
224pub fn parse_bf16_literal_cell(value: &Value) -> Option<bf16> {
225 let literal = number_literal_for_tensor_cell(value)?;
226 (literal.domain == domains::bf16())
227 .then(|| literal.canonical.parse::<f32>().ok().map(bf16::from_f32))
228 .flatten()
229}
230
231/// Parses a tensor cell as a `(numerator, denominator)` rational pair,
232/// returning `None` unless it is a number in the `numbers/rational` domain
233/// whose canonical `num/den` form parses cleanly.
234pub fn parse_rational_literal_cell(value: &Value) -> Option<(i64, i64)> {
235 let literal = number_literal_for_tensor_cell(value)?;
236 if literal.domain != domains::rational() {
237 return None;
238 }
239 let (num, den) = literal.canonical.split_once('/')?;
240 Some((num.parse::<i64>().ok()?, den.parse::<i64>().ok()?))
241}
242
243/// Parses a tensor cell as a `(real, imaginary)` pair, returning `None` unless
244/// it is a number in the `numbers/complex` domain whose canonical `a+bi` form
245/// parses cleanly.
246pub fn parse_complex_literal_cell(value: &Value) -> Option<(f64, f64)> {
247 let literal = number_literal_for_tensor_cell(value)?;
248 if literal.domain != domains::complex() {
249 return None;
250 }
251 let text = literal.canonical.strip_suffix('i')?;
252 let split = text
253 .char_indices()
254 .skip(1)
255 .find(|(_, ch)| *ch == '+' || *ch == '-')
256 .map(|(index, _)| index)?;
257 let (real, imag) = text.split_at(split);
258 Some((real.parse::<f64>().ok()?, imag.parse::<f64>().ok()?))
259}