sim_lib_numbers_tensor/implementation/
value.rs1use std::cmp::Ordering;
5use std::collections::{BTreeMap, BinaryHeap};
6use std::sync::Arc;
7
8use sim_kernel::{
9 ClassRef, Cx, DefaultFactory, Error, Expr, Factory, NoopEvalPolicy, NumberValue, Object,
10 ObjectCompat, ObjectEncode, ObjectEncoding, Result, Symbol, Value,
11};
12
13use super::citizen::tensor_value_class_symbol;
14use super::domain::number_domain;
15
16#[derive(Clone)]
24pub struct Tensor {
25 shape: Vec<usize>,
27 dtype: Symbol,
30 data: Vec<Value>,
33}
34
35impl Tensor {
36 pub fn new_checked(
39 cx: &mut Cx,
40 shape: Vec<usize>,
41 dtype: Symbol,
42 data: Vec<Value>,
43 ) -> Result<Self> {
44 validate_shape_and_data_len(&shape, data.len())?;
45 validate_cells(cx, &data)?;
46 validate_dtype_accepts_cells(cx, &dtype, &data)?;
47 Ok(Self { shape, dtype, data })
48 }
49
50 pub fn new_exact(shape: Vec<usize>, dtype: Symbol, data: Vec<Value>) -> Result<Self> {
56 validate_shape_and_data_len(&shape, data.len())?;
57 validate_exact_cell_dtype(&dtype, &data)?;
58 Ok(Self { shape, dtype, data })
59 }
60
61 pub fn shape(&self) -> &[usize] {
63 &self.shape
64 }
65
66 pub fn dtype(&self) -> &Symbol {
68 &self.dtype
69 }
70
71 pub fn data(&self) -> &[Value] {
73 &self.data
74 }
75
76 pub fn rank(&self) -> usize {
79 self.shape.len()
80 }
81
82 pub fn flat_offset(shape: &[usize], indices: &[usize]) -> Result<usize> {
101 if shape.len() != indices.len() {
102 return Err(Error::Eval("tensor index rank mismatch".to_owned()));
103 }
104 let mut stride = 1usize;
105 let mut offset = 0usize;
106 for (dim, index) in shape.iter().rev().zip(indices.iter().rev()) {
107 if *index >= *dim {
108 return Err(Error::Eval("tensor index was out of bounds".to_owned()));
109 }
110 offset += index * stride;
111 stride = stride.saturating_mul(*dim);
112 }
113 Ok(offset)
114 }
115
116 pub fn coordinates(shape: &[usize]) -> Vec<Vec<usize>> {
119 if shape.is_empty() {
120 return vec![Vec::new()];
121 }
122 let mut out = Vec::new();
123 let mut coord = vec![0usize; shape.len()];
124 loop {
125 out.push(coord.clone());
126 let mut axis = shape.len();
127 while axis > 0 {
128 axis -= 1;
129 coord[axis] += 1;
130 if coord[axis] < shape[axis] {
131 break;
132 }
133 coord[axis] = 0;
134 if axis == 0 {
135 return out;
136 }
137 }
138 }
139 }
140}
141
142impl Object for Tensor {
143 fn display(&self, cx: &mut Cx) -> Result<String> {
144 match self.as_expr(cx)? {
145 Expr::Call { .. } => Ok(format!("{}<{:?}>", tensor_display_name(), self.shape)),
146 expr => Ok(format!("{expr:?}")),
147 }
148 }
149
150 fn as_any(&self) -> &dyn std::any::Any {
151 self
152 }
153}
154
155impl sim_kernel::ObjectCompat for Tensor {
156 fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
157 if let Some(value) = cx.registry().class_by_symbol(&tensor_value_class_symbol()) {
158 return Ok(value.clone());
159 }
160 if let Some(value) = cx
161 .registry()
162 .class_by_symbol(&Symbol::qualified("core", "Number"))
163 {
164 return Ok(value.clone());
165 }
166 DefaultFactory.class_stub(
167 sim_kernel::CORE_NUMBER_CLASS_ID,
168 Symbol::qualified("core", "Number"),
169 )
170 }
171 fn as_expr(&self, cx: &mut Cx) -> Result<Expr> {
172 match self.rank() {
173 0 => Ok(Expr::Call {
174 operator: Box::new(Expr::Symbol(Symbol::new("scalar"))),
175 args: vec![
176 self.data
177 .first()
178 .ok_or_else(|| Error::Eval("scalar tensor is missing its cell".to_owned()))?
179 .object()
180 .as_expr(cx)?,
181 ],
182 }),
183 1 => Ok(Expr::Vector(exprs(cx, &self.data)?)),
184 2 => {
185 let width = self.shape[1];
186 let rows = self
187 .data
188 .chunks(width)
189 .map(|row| exprs(cx, row).map(Expr::Vector))
190 .collect::<Result<Vec<_>>>()?;
191 Ok(Expr::Vector(rows))
192 }
193 _ => Ok(Expr::Call {
194 operator: Box::new(Expr::Symbol(Symbol::new("tensor"))),
195 args: vec![
196 Expr::Vector(
197 self.shape
198 .iter()
199 .map(|dim| Expr::String(dim.to_string()))
200 .collect(),
201 ),
202 Expr::Symbol(self.dtype.clone()),
203 Expr::Vector(exprs(cx, &self.data)?),
204 ],
205 }),
206 }
207 }
208 fn as_table(&self, cx: &mut Cx) -> Result<Value> {
209 let shape = cx.factory().list(
210 self.shape
211 .iter()
212 .map(|dim| cx.factory().string(dim.to_string()))
213 .collect::<Result<Vec<_>>>()?,
214 )?;
215 let data = cx.factory().list(self.data.clone())?;
216 cx.factory().table(vec![
217 (
218 Symbol::new("kind"),
219 cx.factory().string("tensor".to_owned())?,
220 ),
221 (Symbol::new("shape"), shape),
222 (
223 Symbol::new("dtype"),
224 cx.factory().symbol(self.dtype.clone())?,
225 ),
226 (Symbol::new("data"), data),
227 ])
228 }
229 fn as_number_value(&self) -> Option<&dyn NumberValue> {
230 Some(self)
231 }
232
233 fn as_object_encoder(&self) -> Option<&dyn ObjectEncode> {
234 Some(self)
235 }
236}
237
238impl NumberValue for Tensor {
239 fn number_domain(&self, _cx: &mut Cx) -> Result<Symbol> {
240 Ok(number_domain())
241 }
242}
243
244impl ObjectEncode for Tensor {
245 fn object_encoding(&self, cx: &mut Cx) -> Result<ObjectEncoding> {
246 Ok(ObjectEncoding::Constructor {
247 class: tensor_value_class_symbol(),
248 args: vec![
249 Expr::Symbol(Symbol::new("v1")),
250 Expr::List(
251 self.shape
252 .iter()
253 .map(|dim| {
254 Expr::Number(sim_kernel::NumberLiteral {
255 domain: Symbol::qualified("citizen", "int"),
256 canonical: dim.to_string(),
257 })
258 })
259 .collect(),
260 ),
261 Expr::List(exprs(cx, &self.data)?),
262 Expr::Symbol(self.dtype.clone()),
263 ],
264 })
265 }
266}
267
268impl sim_citizen::Citizen for Tensor {
269 fn citizen_symbol() -> Symbol {
270 tensor_value_class_symbol()
271 }
272
273 fn citizen_version() -> u32 {
274 1
275 }
276
277 fn citizen_arity() -> usize {
278 3
279 }
280
281 fn citizen_fields() -> &'static [&'static str] {
282 &["shape", "data", "domain"]
283 }
284}
285
286pub fn build_tensor_value(
294 cx: &mut Cx,
295 shape: Vec<usize>,
296 dtype_hint: Option<Symbol>,
297 data: Vec<Value>,
298) -> Result<Value> {
299 validate_shape_and_data_len(&shape, data.len())?;
300 let dtype = choose_dtype(cx, dtype_hint, &data)?;
301 let tensor = Tensor::new_checked(cx, shape, dtype, data)?;
302 cx.factory().opaque(Arc::new(tensor))
303}
304
305pub fn build_scalar_tensor_value(cx: &mut Cx, value: Value) -> Result<Value> {
307 build_tensor_value(cx, Vec::new(), None, vec![value])
308}
309
310pub fn tensor_value_ref(value: &Value) -> Option<&Tensor> {
312 value.object().downcast_ref::<Tensor>()
313}
314
315pub fn tensor_dtype(tensor: &Tensor) -> &Symbol {
317 tensor.dtype()
318}
319
320pub fn flatten_tensor_scalar_cells(tensor: &Tensor) -> Vec<Value> {
322 tensor.data().to_vec()
323}
324
325pub fn tensor_display_name() -> &'static str {
326 "tensor"
327}
328
329fn exprs(cx: &mut Cx, data: &[Value]) -> Result<Vec<Expr>> {
330 data.iter()
331 .map(|value| value.object().as_expr(cx))
332 .collect()
333}
334
335use crate::spec::checked_element_count;
336
337fn validate_shape_and_data_len(shape: &[usize], data_len: usize) -> Result<()> {
338 let expected = checked_element_count(shape)?;
339 if data_len != expected {
340 return Err(Error::Eval(format!(
341 "tensor shape {:?} expects {expected} cells, found {data_len}",
342 shape
343 )));
344 }
345 if data_len == 0 {
346 return Err(Error::Eval("tensor requires at least one cell".to_owned()));
347 }
348 Ok(())
349}
350
351fn validate_cells(cx: &mut Cx, data: &[Value]) -> Result<()> {
352 for cell in data {
353 let Some(number) = cx.number_value_ref(cell.clone())? else {
354 return Err(Error::Eval(
355 "tensor cells must all be scalar number values".to_owned(),
356 ));
357 };
358 if number.domain == number_domain() {
359 return Err(Error::Eval(
360 "tensor cells must be scalar numbers, not nested tensors".to_owned(),
361 ));
362 }
363 }
364 Ok(())
365}
366
367fn validate_dtype_accepts_cells(cx: &mut Cx, dtype: &Symbol, data: &[Value]) -> Result<()> {
368 let domains = cell_domains(cx, data)?;
369 if domains
370 .iter()
371 .all(|domain| promotion_cost(cx, domain, dtype).is_some())
372 {
373 return Ok(());
374 }
375 Err(Error::Eval(format!(
376 "tensor dtype {dtype} is not a valid join for cell domains {domains:?}"
377 )))
378}
379
380fn validate_exact_cell_dtype(dtype: &Symbol, data: &[Value]) -> Result<()> {
381 let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
382 for cell in data {
383 let Some(number) = cell.object().as_number_value() else {
384 return Err(Error::Eval(
385 "tensor cells must all be scalar number values".to_owned(),
386 ));
387 };
388 let domain = number.number_domain(&mut cx)?;
389 if domain == number_domain() {
390 return Err(Error::Eval(
391 "tensor cells must be scalar numbers, not nested tensors".to_owned(),
392 ));
393 }
394 if &domain != dtype {
395 return Err(Error::Eval(format!(
396 "tensor dtype {dtype} does not match cell domain {domain}"
397 )));
398 }
399 }
400 Ok(())
401}
402
403fn choose_dtype(cx: &mut Cx, dtype_hint: Option<Symbol>, data: &[Value]) -> Result<Symbol> {
404 if data.is_empty() {
405 return Err(Error::Eval("tensor requires at least one cell".to_owned()));
406 }
407 let domains = cell_domains(cx, data)?;
408 if domains.is_empty() {
409 return Err(Error::Eval("tensor requires at least one cell".to_owned()));
410 }
411 if let Some(dtype) = dtype_hint {
412 if domains
413 .iter()
414 .all(|domain| promotion_cost(cx, domain, &dtype).is_some())
415 {
416 return Ok(dtype);
417 }
418 return Err(Error::Eval(format!(
419 "tensor dtype {dtype} is not a valid join for cell domains {domains:?}"
420 )));
421 }
422 let candidates = cx
423 .registry()
424 .number_domains()
425 .keys()
426 .filter(|symbol| **symbol != number_domain())
427 .cloned()
428 .collect::<Vec<_>>();
429 let mut best = None::<(u32, Symbol)>;
430 for candidate in candidates {
431 let mut total = 0u32;
432 let mut valid = true;
433 for domain in &domains {
434 let Some(cost) = promotion_cost(cx, domain, &candidate) else {
435 valid = false;
436 break;
437 };
438 total += cost;
439 }
440 if !valid {
441 continue;
442 }
443 match &best {
444 Some((best_cost, best_symbol))
445 if total > *best_cost || (total == *best_cost && candidate >= *best_symbol) => {}
446 _ => best = Some((total, candidate)),
447 }
448 }
449 best.map(|(_, symbol)| symbol).ok_or_else(|| {
450 Error::Eval(format!(
451 "no join domain exists for tensor cells {domains:?}"
452 ))
453 })
454}
455
456fn cell_domains(cx: &mut Cx, data: &[Value]) -> Result<Vec<Symbol>> {
457 data.iter()
458 .map(|value| {
459 cx.number_value_ref(value.clone())?
460 .map(|number| number.domain)
461 .ok_or_else(|| {
462 Error::Eval("tensor cells must all be scalar number values".to_owned())
463 })
464 })
465 .collect()
466}
467
468fn promotion_cost(cx: &Cx, from: &Symbol, to: &Symbol) -> Option<u32> {
469 if from == to {
470 return Some(0);
471 }
472
473 #[derive(Clone, Eq, PartialEq)]
474 struct State {
475 cost: u32,
476 symbol: Symbol,
477 }
478
479 impl Ord for State {
480 fn cmp(&self, other: &Self) -> Ordering {
481 other
482 .cost
483 .cmp(&self.cost)
484 .then_with(|| other.symbol.cmp(&self.symbol))
485 }
486 }
487
488 impl PartialOrd for State {
489 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
490 Some(self.cmp(other))
491 }
492 }
493
494 let mut best = BTreeMap::<Symbol, u32>::new();
495 let mut heap = BinaryHeap::new();
496 best.insert(from.clone(), 0);
497 heap.push(State {
498 cost: 0,
499 symbol: from.clone(),
500 });
501
502 while let Some(State { cost, symbol }) = heap.pop() {
503 if &symbol == to {
504 return Some(cost);
505 }
506 if best.get(&symbol).copied().unwrap_or(u32::MAX) < cost {
507 continue;
508 }
509 for rule in cx
510 .registry()
511 .value_promotion_rules()
512 .iter()
513 .filter(|rule| rule.from_domain == symbol)
514 {
515 let next = cost + rule.cost as u32;
516 let entry = best.entry(rule.to_domain.clone()).or_insert(u32::MAX);
517 if next < *entry {
518 *entry = next;
519 heap.push(State {
520 cost: next,
521 symbol: rule.to_domain.clone(),
522 });
523 }
524 }
525 }
526 None
527}