1use crate::builtins::common::tensor::tensor_element_len;
2use crate::indexing::selectors::IndexScalar;
3use crate::runtime_error::semantic_error as mex;
4use crate::RuntimeError;
5use runmat_value::{CellArray, NumericScalar, StructValue, Tensor, Value};
6
7const CELL_END_PLUS_TAG_MASK: u64 = 0xffff_ffff_0000_0000;
8const CELL_END_PLUS_TAG_VALUE: u64 = 0x7ff8_c311_0000_0000;
9const CELL_END_PLUS_OFFSET_MASK: u64 = 0x0000_0000_ffff_ffff;
10
11fn map_cell_shape_error(context: &str, err: impl std::fmt::Display) -> RuntimeError {
12 mex("ShapeMismatch", format!("{context}: {err}"))
13}
14
15fn empty_numeric_cell_value() -> Result<Value, RuntimeError> {
16 Tensor::new(Vec::new(), vec![0, 0])
17 .map(Value::Tensor)
18 .map_err(|e| map_cell_shape_error("cell growth empty filler", e))
19}
20
21fn exact_index_from_f64(value: f64) -> Option<i64> {
22 if !value.is_finite() {
23 return None;
24 }
25 let rounded = value.round();
26 if (rounded - value).abs() > f64::EPSILON {
27 return None;
28 }
29 if rounded < i64::MIN as f64 || rounded > i64::MAX as f64 {
30 return None;
31 }
32 Some(rounded as i64)
33}
34
35fn parse_positive_cell_index(index: IndexScalar) -> Result<usize, RuntimeError> {
36 index
37 .positive_usize()
38 .ok_or_else(|| mex("CellIndexOutOfBounds", "Cell index out of bounds"))
39}
40
41fn index_scalar_from_numeric(value: NumericScalar) -> Result<IndexScalar, RuntimeError> {
42 match value {
43 NumericScalar::F64(value) => exact_index_from_f64(value).map(IndexScalar::Signed),
44 NumericScalar::F32(value) => {
45 exact_index_from_f64(f64::from(value)).map(IndexScalar::Signed)
46 }
47 value => value
48 .into_int_value()
49 .map(|value| IndexScalar::from_int(&value)),
50 }
51 .ok_or_else(|| mex("CellIndexType", "Unsupported cell index type"))
52}
53
54fn parse_cell_index_value(value: &Value) -> Result<usize, RuntimeError> {
55 let index = match value {
56 Value::Num(n) => IndexScalar::Signed(
57 exact_index_from_f64(*n)
58 .ok_or_else(|| mex("CellIndexType", "Unsupported cell index type"))?,
59 ),
60 Value::Int(i) => IndexScalar::from_int(i),
61 Value::Tensor(t)
62 if tensor_element_len(t) == 1 && t.shape.iter().product::<usize>() == 1 =>
63 {
64 index_scalar_from_numeric(
65 t.numeric_value_at(0)
66 .expect("validated scalar numeric tensor storage"),
67 )?
68 }
69 other => {
70 let n: f64 = other
71 .try_into()
72 .map_err(|_| mex("CellIndexType", "Unsupported cell index type"))?;
73 IndexScalar::Signed(
74 exact_index_from_f64(n)
75 .ok_or_else(|| mex("CellIndexType", "Unsupported cell index type"))?,
76 )
77 }
78 };
79 parse_positive_cell_index(index)
80}
81
82fn parse_cell_index_value_for_len(value: &Value, len: usize) -> Result<usize, RuntimeError> {
83 match value {
84 Value::Num(n) => {
85 if let Some(idx) = resolve_cell_end_relative_index(*n, len)? {
86 return Ok(idx);
87 }
88 if n.is_nan() {
89 return Err(mex("CellIndexOutOfBounds", "Cell index out of bounds"));
90 }
91 }
92 Value::Tensor(t) if t.len() == 1 && t.shape.iter().product::<usize>() == 1 => {
93 let value = t
94 .numeric_value_at(0)
95 .expect("validated scalar numeric tensor storage");
96 if matches!(value, NumericScalar::F64(_) | NumericScalar::F32(_)) {
97 let scalar = value.materialize_f64();
98 if let Some(idx) = resolve_cell_end_relative_index(scalar, len)? {
99 return Ok(idx);
100 }
101 if scalar.is_nan() {
102 return Err(mex("CellIndexOutOfBounds", "Cell index out of bounds"));
103 }
104 }
105 }
106 _ => {}
107 }
108 parse_cell_index_value(value)
109}
110
111fn parse_cell_index_values_for_assignment(value: &Value) -> Result<Vec<usize>, RuntimeError> {
112 match value {
113 Value::Tensor(t) if tensor_element_len(t) > 1 => (0..t.len())
114 .map(|index| {
115 parse_positive_cell_index(index_scalar_from_numeric(
116 t.numeric_value_at(index)
117 .expect("validated numeric tensor storage"),
118 )?)
119 })
120 .collect(),
121 _ => Ok(vec![parse_cell_index_value(value)?]),
122 }
123}
124
125fn decode_cell_end_plus(value: f64) -> Option<usize> {
126 if !value.is_nan() {
127 return None;
128 }
129 let bits = value.to_bits();
130 if (bits & CELL_END_PLUS_TAG_MASK) != CELL_END_PLUS_TAG_VALUE {
131 return None;
132 }
133 Some((bits & CELL_END_PLUS_OFFSET_MASK) as usize)
134}
135
136fn resolve_cell_end_relative_index(value: f64, len: usize) -> Result<Option<usize>, RuntimeError> {
137 if let Some(offset) = decode_cell_end_plus(value) {
138 let idx = len + offset;
139 if idx < 1 || idx > len {
140 return Err(mex("CellIndexOutOfBounds", "Cell index out of bounds"));
141 }
142 return Ok(Some(idx));
143 }
144 if value == 0.0 && value.is_sign_negative() {
145 return Ok(Some(len));
146 }
147 if value < 0.0 {
148 let offset = exact_index_from_f64(value)
149 .ok_or_else(|| mex("CellIndexType", "Unsupported cell index type"))?;
150 let idx = len as i64 + offset;
151 if idx < 1 || idx > len as i64 {
152 return Err(mex("CellIndexOutOfBounds", "Cell index out of bounds"));
153 }
154 return Ok(Some(idx as usize));
155 }
156 Ok(None)
157}
158
159fn is_empty_tensor(value: &Value) -> bool {
160 matches!(value, Value::Tensor(t) if t.is_empty() || t.rows == 0 || t.cols == 0)
161}
162
163fn row_major_pos_from_linear(ca: &CellArray, idx: usize) -> Result<usize, RuntimeError> {
164 if idx == 0 || idx > ca.data.len() {
165 return Err(mex("CellIndexOutOfBounds", "Cell index out of bounds"));
166 }
167 if ca.rows <= 1 || ca.cols <= 1 {
168 return Ok(idx - 1);
169 }
170 let zero = idx - 1;
171 let row = zero % ca.rows;
172 let col = zero / ca.rows;
173 Ok(row * ca.cols + col)
174}
175
176pub fn create_cell_2d(values: Vec<Value>, rows: usize, cols: usize) -> Result<Value, RuntimeError> {
177 crate::make_cell_with_shape(values, vec![rows, cols])
178 .map_err(|e| map_cell_shape_error("cell creation error", e))
179}
180
181pub fn index_cell_value(ca: &CellArray, indices: &[usize]) -> Result<Value, RuntimeError> {
182 match indices.len() {
183 1 => {
184 let i = indices[0];
185 Ok(ca.data[row_major_pos_from_linear(ca, i)?].clone())
186 }
187 2 => {
188 let r = indices[0];
189 let c = indices[1];
190 if r == 0 || r > ca.rows || c == 0 || c > ca.cols {
191 return Err(mex(
192 "CellSubscriptOutOfBounds",
193 "Cell subscript out of bounds",
194 ));
195 }
196 Ok(ca.data[(r - 1) * ca.cols + (c - 1)].clone())
197 }
198 _ => Err(mex(
199 "UnsupportedCellIndexCount",
200 "Unsupported number of cell indices",
201 )),
202 }
203}
204
205pub fn expand_cell_values(
206 ca: &CellArray,
207 indices: &[usize],
208 out_count: usize,
209) -> Result<Vec<Value>, RuntimeError> {
210 let mut values: Vec<Value> = Vec::new();
211 if indices.is_empty() {
212 for idx in 1..=ca.data.len() {
213 values.push(index_cell_value(ca, &[idx])?);
214 }
215 } else {
216 values.push(index_cell_value(ca, indices)?);
217 }
218 if values.len() >= out_count {
219 Ok(values.into_iter().take(out_count).collect())
220 } else {
221 let mut out = values;
222 out.resize(out_count, Value::Num(0.0));
223 Ok(out)
224 }
225}
226
227pub fn expand_all_cell_values(ca: &CellArray) -> Result<Vec<Value>, RuntimeError> {
228 (1..=ca.data.len())
229 .map(|idx| index_cell_value(ca, &[idx]))
230 .collect()
231}
232
233pub fn gather_cell_paren_linear_indices(
234 ca: &CellArray,
235 indices: &[usize],
236 output_shape: &[usize],
237) -> Result<Value, RuntimeError> {
238 let mut handles = Vec::with_capacity(indices.len());
239 for &idx in indices {
240 let pos = row_major_pos_from_linear(ca, idx)?;
241 handles.push(ca.data[pos].clone());
242 }
243 let shape = if output_shape.is_empty() {
244 vec![1, handles.len().max(1)]
245 } else {
246 output_shape.to_vec()
247 };
248 Ok(Value::Cell(
249 CellArray::new_with_shape(handles, shape)
250 .map_err(|e| map_cell_shape_error("cell paren indexing error", e))?,
251 ))
252}
253
254pub fn gather_cell_member(ca: &CellArray, field: &str) -> Result<Value, RuntimeError> {
255 if ca.data.len() == 1 {
256 return Ok(match &ca.data[0] {
257 Value::Struct(st) => st.fields.get(field).cloned().unwrap_or(Value::Num(0.0)),
258 other => other.clone(),
259 });
260 }
261
262 let mut out: Vec<Value> = Vec::with_capacity(ca.data.len());
263 for value in &ca.data {
264 match value {
265 Value::Struct(st) => out.push(st.fields.get(field).cloned().unwrap_or(Value::Num(0.0))),
266 other => out.push(other.clone()),
267 }
268 }
269 let cell = CellArray::new(out, ca.rows, ca.cols)
270 .map_err(|e| map_cell_shape_error("cell field gather", e))?;
271 Ok(Value::Cell(cell))
272}
273
274pub fn assign_cell_member<OnWrite>(
275 mut ca: CellArray,
276 field: String,
277 rhs: Value,
278 mut on_write: OnWrite,
279) -> Result<Value, RuntimeError>
280where
281 OnWrite: FnMut(&Value, &Value),
282{
283 let rhs_cell = if let Value::Cell(rc) = &rhs {
284 if rc.rows != ca.rows || rc.cols != ca.cols {
285 return Err(mex(
286 "CellMemberRhsShapeMismatch",
287 "field assignment cell RHS shape mismatch",
288 ));
289 }
290 Some(rc)
291 } else {
292 None
293 };
294
295 for i in 0..ca.data.len() {
296 let rv = if let Some(rc) = rhs_cell {
297 rc.data[i].clone()
298 } else {
299 rhs.clone()
300 };
301 match &mut ca.data[i] {
302 Value::Struct(st) => {
303 if let Some(oldv) = st.fields.get(&field) {
304 on_write(oldv, &rv);
305 }
306 st.fields.insert(field.clone(), rv);
307 }
308 other => {
309 let mut st = StructValue::new();
310 st.fields.insert(field.clone(), rv);
311 *other = Value::Struct(st);
312 }
313 }
314 }
315 Ok(Value::Cell(ca))
316}
317
318pub fn expand_cell_indices(ca: &CellArray, indices: &[Value]) -> Result<Vec<Value>, RuntimeError> {
319 fn is_colon_selector(value: &Value) -> bool {
320 matches!(value, Value::String(text) if text == ":")
321 || matches!(value, Value::CharArray(chars) if chars.row_string().as_deref() == Some(":"))
322 }
323
324 match indices.len() {
325 1 => match &indices[0] {
326 value if is_colon_selector(value) => expand_all_cell_values(ca),
327 Value::Num(n) => {
328 if let Some(idx) = resolve_cell_end_relative_index(*n, ca.data.len())? {
329 return Ok(vec![index_cell_value(ca, &[idx])?]);
330 }
331 if n.is_nan() {
332 return Err(mex("CellIndexOutOfBounds", "Cell index out of bounds"));
333 }
334 let idx = parse_cell_index_value(&indices[0])?;
335 Ok(vec![index_cell_value(ca, &[idx])?])
336 }
337 Value::Int(_) => {
338 let idx = parse_cell_index_value(&indices[0])?;
339 Ok(vec![index_cell_value(ca, &[idx])?])
340 }
341 Value::Tensor(t) => {
342 if tensor_element_len(t) == 1 && t.shape.iter().product::<usize>() == 1 {
343 let idx = parse_cell_index_value_for_len(&indices[0], ca.data.len())?;
344 return Ok(vec![index_cell_value(ca, &[idx])?]);
345 }
346 (0..t.len())
347 .map(|index| {
348 let idx = parse_positive_cell_index(index_scalar_from_numeric(
349 t.numeric_value_at(index)
350 .expect("validated numeric tensor storage"),
351 )?)?;
352 index_cell_value(ca, &[idx])
353 })
354 .collect()
355 }
356 _ => Err(mex("CellIndexType", "Unsupported cell index type")),
357 },
358 2 => {
359 let row_colon = is_colon_selector(&indices[0]);
360 let col_colon = is_colon_selector(&indices[1]);
361 if row_colon && col_colon {
362 return expand_all_cell_values(ca);
363 }
364 if row_colon {
365 let c = parse_cell_index_value_for_len(&indices[1], ca.cols)?;
366 let mut values = Vec::with_capacity(ca.rows);
367 for r in 1..=ca.rows {
368 values.push(index_cell_value(ca, &[r, c])?);
369 }
370 return Ok(values);
371 }
372 if col_colon {
373 let r = parse_cell_index_value_for_len(&indices[0], ca.rows)?;
374 let mut values = Vec::with_capacity(ca.cols);
375 for c in 1..=ca.cols {
376 values.push(index_cell_value(ca, &[r, c])?);
377 }
378 return Ok(values);
379 }
380 let r = parse_cell_index_value_for_len(&indices[0], ca.rows)?;
381 let c = parse_cell_index_value_for_len(&indices[1], ca.cols)?;
382 Ok(vec![index_cell_value(ca, &[r, c])?])
383 }
384 _ => Err(mex("CellIndexType", "Unsupported cell index type")),
385 }
386}
387
388pub fn assign_cell_value<OnWrite>(
389 mut ca: CellArray,
390 indices: &[usize],
391 rhs: Value,
392 mut on_write: OnWrite,
393) -> Result<Value, RuntimeError>
394where
395 OnWrite: FnMut(&Value, &Value),
396{
397 match indices.len() {
398 1 => {
399 let i = indices[0];
400 let old_len = ca.data.len();
401 if i > old_len {
402 if !(ca.data.is_empty() || ca.rows <= 1 || ca.cols <= 1) {
403 return Err(mex(
404 "UnsupportedCellGrowth",
405 "Cell growth via linear brace assignment is only supported for vectors",
406 ));
407 }
408 while ca.data.len() < i {
409 ca.data.push(empty_numeric_cell_value()?);
410 }
411 let len = ca.data.len();
412 if old_len == 0 {
413 ca.rows = 1;
416 ca.cols = len;
417 ca.shape = vec![1, len];
418 } else if ca.rows <= 1 {
419 ca.rows = 1;
420 ca.cols = len;
421 ca.shape = vec![1, len];
422 } else {
423 ca.rows = len;
424 ca.cols = 1;
425 ca.shape = vec![len, 1];
426 }
427 }
428 let pos = row_major_pos_from_linear(&ca, i)?;
429 if let Some(oldv) = ca.data.get(pos) {
430 on_write(oldv, &rhs);
431 }
432 ca.data[pos] = rhs;
433 Ok(Value::Cell(ca))
434 }
435 2 => {
436 let i = indices[0];
437 let j = indices[1];
438 if i == 0 || j == 0 {
439 return Err(mex(
440 "CellSubscriptOutOfBounds",
441 "Cell subscript out of bounds",
442 ));
443 }
444 if i > ca.rows || j > ca.cols {
445 let old_rows = ca.rows;
446 let old_cols = ca.cols;
447 let new_rows = old_rows.max(i);
448 let new_cols = old_cols.max(j);
449 let total = new_rows.checked_mul(new_cols).ok_or_else(|| {
450 mex(
451 "CellSubscriptOutOfBounds",
452 "Cell array expansion exceeds supported size",
453 )
454 })?;
455 let mut grown = Vec::with_capacity(total);
456 for _ in 0..total {
457 grown.push(empty_numeric_cell_value()?);
458 }
459 for row in 0..old_rows {
460 for col in 0..old_cols {
461 let old_lin = row * old_cols + col;
462 let new_lin = row * new_cols + col;
463 grown[new_lin] = ca.data[old_lin].clone();
464 }
465 }
466 ca.data = grown;
467 ca.rows = new_rows;
468 ca.cols = new_cols;
469 ca.shape = vec![new_rows, new_cols];
470 }
471 let lin = (i - 1) * ca.cols + (j - 1);
472 if let Some(oldv) = ca.data.get(lin) {
473 on_write(oldv, &rhs);
474 }
475 ca.data[lin] = rhs;
476 Ok(Value::Cell(ca))
477 }
478 _ => Err(mex(
479 "UnsupportedCellIndexCount",
480 "Unsupported number of cell indices",
481 )),
482 }
483}
484
485pub fn assign_cell_value_multi<OnWrite>(
486 mut ca: CellArray,
487 positions: &[usize],
488 rhs_values: &[Value],
489 mut on_write: OnWrite,
490) -> Result<Value, RuntimeError>
491where
492 OnWrite: FnMut(&Value, &Value),
493{
494 if positions.is_empty() {
495 return Ok(Value::Cell(ca));
496 }
497 if positions.len() != rhs_values.len() {
498 return Err(mex(
499 "CellAssignmentArityMismatch",
500 "Cell brace assignment target count does not match source value count",
501 ));
502 }
503 let old_len = ca.data.len();
504 if let Some(max_pos) = positions.iter().copied().max() {
505 if max_pos > old_len {
506 if !(ca.data.is_empty() || ca.rows <= 1 || ca.cols <= 1) {
507 return Err(mex(
508 "UnsupportedCellGrowth",
509 "Cell growth via linear brace assignment is only supported for vectors",
510 ));
511 }
512 while ca.data.len() < max_pos {
513 ca.data.push(empty_numeric_cell_value()?);
514 }
515 let len = ca.data.len();
516 if old_len == 0 || ca.rows <= 1 {
517 ca.rows = 1;
518 ca.cols = len;
519 ca.shape = vec![1, len];
520 } else {
521 ca.rows = len;
522 ca.cols = 1;
523 ca.shape = vec![len, 1];
524 }
525 }
526 }
527 for (position, rhs) in positions.iter().zip(rhs_values.iter()) {
528 if *position == 0 || *position > ca.data.len() {
529 return Err(mex("CellIndexOutOfBounds", "Cell index out of bounds"));
530 }
531 let pos = row_major_pos_from_linear(&ca, *position)?;
532 if let Some(oldv) = ca.data.get(pos) {
533 on_write(oldv, rhs);
534 }
535 ca.data[pos] = rhs.clone();
536 }
537 Ok(Value::Cell(ca))
538}
539
540pub fn resolve_cell_assignment_positions(
541 ca: &CellArray,
542 indices: &[Value],
543) -> Result<Vec<usize>, RuntimeError> {
544 if indices.len() != 1 {
545 return Err(mex("CellIndexType", "Unsupported cell index type"));
546 }
547 let _ = ca;
548 parse_cell_index_values_for_assignment(&indices[0])
549}
550
551pub fn delete_cell_linear(mut ca: CellArray, idx: usize) -> Result<Value, RuntimeError> {
552 let total = ca.data.len();
553 if idx == 0 || idx > total {
554 return Err(mex("CellIndexOutOfBounds", "Cell index out of bounds"));
555 }
556 if !(ca.rows == 1 || ca.cols == 1) {
557 return Err(mex(
558 "UnsupportedCellDeletion",
559 "Linear cell deletion is only supported for vectors",
560 ));
561 }
562 ca.data.remove(idx - 1);
563 let len = ca.data.len();
564 let shape = if len == 0 {
565 vec![0, 0]
566 } else if ca.rows == 1 {
567 vec![1, len]
568 } else {
569 vec![len, 1]
570 };
571 Ok(Value::Cell(
572 CellArray::new_with_shape(ca.data, shape)
573 .map_err(|e| map_cell_shape_error("cell deletion error", e))?,
574 ))
575}
576
577pub fn assign_cell_paren_with_policy(
578 ca: CellArray,
579 indices: &[usize],
580 rhs: &Value,
581 delete: bool,
582) -> Result<Value, RuntimeError> {
583 if delete {
584 if indices.len() != 1 {
585 return Err(mex(
586 "UnsupportedCellDeletion",
587 "Linear cell deletion is only supported for vector indices",
588 ));
589 }
590 if !is_empty_tensor(rhs) {
591 return Err(mex(
592 "DeletionRequiresEmptyRhs",
593 "Cell deletion requires empty RHS",
594 ));
595 }
596 return delete_cell_linear(ca, indices[0]);
597 }
598 if let Value::Cell(rhs_cell) = rhs {
599 return assign_cell_paren_from_cell(ca, indices, rhs_cell);
600 }
601 Err(mex(
602 "UnsupportedCellParenAssignment",
603 "Cell paren assignment requires a cell RHS",
604 ))
605}
606
607pub fn assign_cell_paren_linear_indices_with_policy(
608 mut ca: CellArray,
609 indices: &[usize],
610 rhs: &Value,
611 delete: bool,
612) -> Result<Value, RuntimeError> {
613 if delete {
614 if !is_empty_tensor(rhs) {
615 return Err(mex(
616 "DeletionRequiresEmptyRhs",
617 "Cell deletion requires empty RHS",
618 ));
619 }
620 if !(ca.rows == 1 || ca.cols == 1) {
621 return Err(mex(
622 "UnsupportedCellDeletion",
623 "Linear cell deletion is only supported for vectors",
624 ));
625 }
626 let mut positions = indices
627 .iter()
628 .map(|&idx| row_major_pos_from_linear(&ca, idx))
629 .collect::<Result<Vec<_>, _>>()?;
630 positions.sort_unstable();
631 positions.dedup();
632 for pos in positions.into_iter().rev() {
633 ca.data.remove(pos);
634 }
635 let len = ca.data.len();
636 let shape = if len == 0 {
637 vec![0, 0]
638 } else if ca.rows == 1 {
639 vec![1, len]
640 } else {
641 vec![len, 1]
642 };
643 return Ok(Value::Cell(
644 CellArray::new_with_shape(ca.data, shape)
645 .map_err(|e| map_cell_shape_error("cell deletion error", e))?,
646 ));
647 }
648 let Value::Cell(rhs_cell) = rhs else {
649 return Err(mex(
650 "UnsupportedCellParenAssignment",
651 "Cell paren assignment requires a cell RHS",
652 ));
653 };
654 if rhs_cell.data.len() != indices.len() && rhs_cell.data.len() != 1 {
655 return Err(mex(
656 "UnsupportedCellParenAssignment",
657 "Cell RHS must be scalar or match assignment size",
658 ));
659 }
660 for (k, &idx) in indices.iter().enumerate() {
661 let pos = row_major_pos_from_linear(&ca, idx)?;
662 let rhs_pos = if rhs_cell.data.len() == 1 { 0 } else { k };
663 let newv = rhs_cell.data[rhs_pos].clone();
664 if let Some(oldv) = ca.data.get(pos) {
665 runmat_gc::gc_record_write(oldv, &newv);
666 }
667 ca.data[pos] = newv;
668 }
669 Ok(Value::Cell(ca))
670}
671
672fn assign_cell_paren_from_cell(
673 mut ca: CellArray,
674 indices: &[usize],
675 rhs: &CellArray,
676) -> Result<Value, RuntimeError> {
677 if rhs.data.len() != 1 {
678 return Err(mex(
679 "UnsupportedCellParenAssignment",
680 "Only scalar cell paren assignment is supported",
681 ));
682 }
683 let newv = rhs.data[0].clone();
684 let lin = match indices.len() {
685 1 => {
686 let i = indices[0];
687 row_major_pos_from_linear(&ca, i)?
688 }
689 2 => {
690 let i = indices[0];
691 let j = indices[1];
692 if i == 0 || i > ca.rows || j == 0 || j > ca.cols {
693 return Err(mex(
694 "CellSubscriptOutOfBounds",
695 "Cell subscript out of bounds",
696 ));
697 }
698 (i - 1) * ca.cols + (j - 1)
699 }
700 _ => {
701 return Err(mex(
702 "UnsupportedCellIndexCount",
703 "Unsupported number of cell indices",
704 ))
705 }
706 };
707 if let Some(oldv) = ca.data.get(lin) {
708 runmat_gc::gc_record_write(oldv, &newv);
709 }
710 ca.data[lin] = newv;
711 Ok(Value::Cell(ca))
712}
713
714#[cfg(test)]
715mod tests {
716 use super::{
717 assign_cell_member, expand_cell_indices, map_cell_shape_error,
718 resolve_cell_assignment_positions,
719 };
720 use runmat_value::{CellArray, IntegerStorage, StructValue, Tensor, Value};
721
722 #[test]
723 fn assign_cell_member_rejects_shape_mismatch_cell_rhs() {
724 let base = CellArray::new(
725 vec![
726 Value::Struct(StructValue::new()),
727 Value::Struct(StructValue::new()),
728 ],
729 1,
730 2,
731 )
732 .expect("base cell");
733 let rhs = CellArray::new(vec![Value::Num(1.0)], 1, 1).expect("rhs cell");
734 let err = assign_cell_member(base, "field".to_string(), Value::Cell(rhs), |_old, _new| {})
735 .expect_err("shape mismatch should fail");
736 assert_eq!(err.identifier(), Some("RunMat:CellMemberRhsShapeMismatch"));
737 }
738
739 #[test]
740 fn cell_shape_error_mapping_reports_identifier() {
741 let err = map_cell_shape_error("cell creation", "invalid shape");
742 assert_eq!(err.identifier(), Some("RunMat:ShapeMismatch"));
743 }
744
745 #[test]
746 fn assign_cell_value_linear_growth_records_write_barrier() {
747 let cell = CellArray::new(Vec::new(), 0, 0).expect("empty cell");
748 let mut writes = Vec::new();
749 let result = super::assign_cell_value(cell, &[3], Value::Num(42.0), |old, new| {
750 writes.push((old.clone(), new.clone()));
751 })
752 .expect("linear growth assignment");
753
754 assert_eq!(writes.len(), 1);
755 let empty_filler = Tensor::new(Vec::new(), vec![0, 0]).expect("empty filler");
756 assert_eq!(writes[0], (Value::Tensor(empty_filler), Value::Num(42.0)));
757 let Value::Cell(cell) = result else {
758 panic!("expected cell result");
759 };
760 assert_eq!(cell.data[2], Value::Num(42.0));
761 }
762
763 #[test]
764 fn expand_cell_indices_rejects_fractional_linear_index() {
765 let cell = CellArray::new(vec![Value::Num(10.0), Value::Num(20.0)], 1, 2).expect("cell");
766 let err = expand_cell_indices(&cell, &[Value::Num(1.5)])
767 .expect_err("fractional index should fail");
768 assert_eq!(err.identifier(), Some("RunMat:CellIndexType"));
769 }
770
771 #[test]
772 fn expand_cell_indices_rejects_fractional_tensor_indices() {
773 let cell = CellArray::new(vec![Value::Num(10.0), Value::Num(20.0)], 1, 2).expect("cell");
774 let tensor = Tensor::new(vec![1.0, 1.25], vec![1, 2]).expect("tensor");
775 let err = expand_cell_indices(&cell, &[Value::Tensor(tensor)])
776 .expect_err("fractional tensor index should fail");
777 assert_eq!(err.identifier(), Some("RunMat:CellIndexType"));
778 }
779
780 #[test]
781 fn expand_cell_indices_accepts_scalar_tensor_subscripts_for_2d_cells() {
782 let cell = CellArray::new(
783 vec![
784 Value::Num(11.0),
785 Value::Num(12.0),
786 Value::Num(21.0),
787 Value::Num(22.0),
788 ],
789 2,
790 2,
791 )
792 .expect("2d cell");
793 let row = Tensor::new(vec![2.0], vec![1, 1]).expect("row scalar tensor");
794 let col = Tensor::new(vec![1.0], vec![1, 1]).expect("col scalar tensor");
795 let values = expand_cell_indices(&cell, &[Value::Tensor(row), Value::Tensor(col)])
796 .expect("scalar tensor selectors should index 2d cell expansion");
797 assert_eq!(values, vec![Value::Num(21.0)]);
798 }
799
800 #[test]
801 fn expand_cell_indices_reads_scalar_typed_integer_storage_exactly() {
802 let cell = CellArray::new(
803 vec![
804 Value::Num(11.0),
805 Value::Num(12.0),
806 Value::Num(21.0),
807 Value::Num(22.0),
808 ],
809 2,
810 2,
811 )
812 .expect("2d cell");
813 let row =
814 Tensor::new_integer(IntegerStorage::U16(vec![2]), vec![1, 1]).expect("row selector");
815 let col =
816 Tensor::new_integer(IntegerStorage::U16(vec![1]), vec![1, 1]).expect("col selector");
817
818 let values = expand_cell_indices(&cell, &[Value::Tensor(row), Value::Tensor(col)])
819 .expect("typed integer selectors should use exact storage");
820 assert_eq!(values, vec![Value::Num(21.0)]);
821 }
822
823 #[test]
824 fn cell_scalar_indices_read_all_integer_classes_exactly() {
825 let cell = CellArray::new(vec![Value::Num(10.0), Value::Num(20.0)], 1, 2).expect("cell");
826 macro_rules! assert_index {
827 ($storage:expr) => {{
828 let index = Tensor::new_integer($storage, vec![1, 1]).expect("index");
829 let values = expand_cell_indices(&cell, &[Value::Tensor(index)])
830 .expect("exact typed scalar index");
831 assert_eq!(values, vec![Value::Num(20.0)]);
832 }};
833 }
834
835 assert_index!(IntegerStorage::I8(vec![2]));
836 assert_index!(IntegerStorage::I16(vec![2]));
837 assert_index!(IntegerStorage::I32(vec![2]));
838 assert_index!(IntegerStorage::I64(vec![2]));
839 assert_index!(IntegerStorage::U8(vec![2]));
840 assert_index!(IntegerStorage::U16(vec![2]));
841 assert_index!(IntegerStorage::U32(vec![2]));
842 assert_index!(IntegerStorage::U64(vec![2]));
843 }
844
845 #[test]
846 fn expand_cell_indices_reads_vector_typed_integer_storage_exactly() {
847 let cell = CellArray::new(
848 vec![Value::Num(10.0), Value::Num(20.0), Value::Num(30.0)],
849 1,
850 3,
851 )
852 .expect("cell");
853 let indices =
854 Tensor::new_integer(IntegerStorage::U16(vec![3, 1]), vec![1, 2]).expect("indices");
855
856 let values = expand_cell_indices(&cell, &[Value::Tensor(indices)])
857 .expect("typed integer vector indices should use exact storage");
858 assert_eq!(values, vec![Value::Num(30.0), Value::Num(10.0)]);
859 }
860
861 #[test]
862 fn expand_cell_indices_accepts_native_single_vector_indices() {
863 let cell = CellArray::new(
864 vec![Value::Num(10.0), Value::Num(20.0), Value::Num(30.0)],
865 1,
866 3,
867 )
868 .expect("cell");
869 let indices = Tensor::from_f32(vec![3.0, 1.0], vec![1, 2]).expect("single indices");
870 let values = expand_cell_indices(&cell, &[Value::Tensor(indices)])
871 .expect("native-single vector indices");
872 assert_eq!(values, vec![Value::Num(30.0), Value::Num(10.0)]);
873 }
874
875 #[test]
876 fn typed_integer_cell_subscripts_use_exact_storage() {
877 let cell = CellArray::new(
878 vec![Value::Num(10.0), Value::Num(20.0), Value::Num(30.0)],
879 1,
880 3,
881 )
882 .expect("cell");
883
884 let scalar = Tensor::new_integer(IntegerStorage::U64(vec![3]), vec![1, 1])
885 .expect("wide scalar selector");
886 let values =
887 expand_cell_indices(&cell, &[Value::Tensor(scalar)]).expect("typed scalar selector");
888 assert_eq!(values, vec![Value::Num(30.0)]);
889
890 let vector = Tensor::new_integer(IntegerStorage::I64(vec![3, 1]), vec![1, 2])
891 .expect("signed vector selector");
892 assert_eq!(
893 resolve_cell_assignment_positions(&cell, &[Value::Tensor(vector)])
894 .expect("typed vector selector"),
895 vec![3, 1]
896 );
897
898 let wide = Tensor::new_integer(IntegerStorage::U64(vec![(1_u64 << 53) + 1]), vec![1, 1])
899 .expect("wide unsigned selector");
900 let err = expand_cell_indices(&cell, &[Value::Tensor(wide)])
901 .expect_err("wide uint64 index must remain exact");
902 assert_eq!(err.identifier(), Some("RunMat:CellIndexOutOfBounds"));
903 }
904
905 #[test]
906 fn typed_integer_cell_subscript_bounds_use_exact_storage() {
907 let cell = CellArray::new(vec![Value::Num(10.0)], 1, 1).expect("cell");
908 let index = Tensor::new_integer(IntegerStorage::I64(vec![i64::MIN]), vec![1, 1])
909 .expect("signed edge selector");
910
911 let err = expand_cell_indices(&cell, &[Value::Tensor(index)])
912 .expect_err("signed edge index must be rejected from exact storage");
913 assert_eq!(err.identifier(), Some("RunMat:CellIndexOutOfBounds"));
914 }
915
916 #[test]
917 fn expand_cell_indices_rejects_nonscalar_tensor_subscripts_for_2d_cells() {
918 let cell = CellArray::new(
919 vec![
920 Value::Num(11.0),
921 Value::Num(12.0),
922 Value::Num(21.0),
923 Value::Num(22.0),
924 ],
925 2,
926 2,
927 )
928 .expect("2d cell");
929 let row = Tensor::new(vec![1.0, 2.0], vec![1, 2]).expect("non-scalar row tensor");
930 let err = expand_cell_indices(&cell, &[Value::Tensor(row), Value::Num(1.0)])
931 .expect_err("non-scalar tensor row selector should fail");
932 assert_eq!(err.identifier(), Some("RunMat:CellIndexType"));
933 }
934
935 #[test]
936 fn expand_cell_indices_supports_end_selectors_for_2d_cells() {
937 let cell = CellArray::new(
938 vec![
939 Value::Num(11.0),
940 Value::Num(12.0),
941 Value::Num(21.0),
942 Value::Num(22.0),
943 ],
944 2,
945 2,
946 )
947 .expect("2d cell");
948 let row_end = expand_cell_indices(&cell, &[Value::Num(-0.0), Value::Num(1.0)])
949 .expect("row end selector should resolve");
950 assert_eq!(row_end, vec![Value::Num(21.0)]);
951
952 let col_end = expand_cell_indices(&cell, &[Value::Num(1.0), Value::Num(-0.0)])
953 .expect("col end selector should resolve");
954 assert_eq!(col_end, vec![Value::Num(12.0)]);
955 }
956}