Skip to main content

runmat_runtime/object/
cell.rs

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                    // MATLAB linear growth from empty cell shapes (for example `0x5` or `5x0`)
414                    // normalizes to a row vector.
415                    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}