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}