runmat_runtime/builtins/structs/core/
getfield.rs

1//! MATLAB-compatible `getfield` builtin with struct array and object support.
2
3use crate::builtins::common::spec::{
4    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
5    ReductionNaN, ResidencyPolicy, ShapeRequirements,
6};
7use crate::builtins::common::tensor;
8use crate::call_builtin;
9use crate::indexing::perform_indexing;
10use crate::make_cell_with_shape;
11#[cfg(feature = "doc_export")]
12use crate::register_builtin_doc_text;
13use crate::{register_builtin_fusion_spec, register_builtin_gpu_spec};
14use runmat_builtins::{
15    Access, CellArray, CharArray, ComplexTensor, HandleRef, Listener, LogicalArray, MException,
16    ObjectInstance, StructValue, Tensor, Value,
17};
18use runmat_macros::runtime_builtin;
19
20#[cfg(feature = "doc_export")]
21pub const DOC_MD: &str = r#"---
22title: "getfield"
23category: "structs/core"
24keywords: ["getfield", "struct", "struct array", "object property", "metadata"]
25summary: "Access a field or property from structs, struct arrays, or MATLAB-style objects."
26references: []
27gpu_support:
28  elementwise: false
29  reduction: false
30  precisions: []
31  broadcasting: "none"
32  notes: "Runs on the host. Values that already reside on the GPU stay resident; no kernels are dispatched."
33fusion:
34  elementwise: false
35  reduction: false
36  max_inputs: 1
37  constants: "inline"
38requires_feature: null
39tested:
40  unit: "builtins::structs::core::getfield::tests"
41  integration: "runmat_ignition::tests::functions::member_get_set_and_method_call_skeleton"
42---
43
44# What does the `getfield` function do in MATLAB / RunMat?
45`value = getfield(S, field)` returns the contents of `S.field`. RunMat matches MATLAB by
46supporting nested field access, struct arrays (via index cells), and MATLAB-style objects
47created with `new_object`.
48
49## How does the `getfield` function behave in MATLAB / RunMat?
50- Field names must be character vectors or string scalars. Several field names in a row
51  navigate nested structs: `getfield(S, "outer", "inner")` is equivalent to
52  `S.outer.inner`.
53- When `S` is a struct array, `getfield(S, "field")` examines the first element by default.
54  Provide indices in a cell array to target another element:
55  `getfield(S, {k}, "field")` yields `S(k).field`.
56- After a field name you may supply an index cell to subscript the field value, e.g.
57  `getfield(S, "values", {row, col})`. Each position accepts positive integers or the
58  keyword `end` to reference the last element in that dimension.
59- MATLAB-style objects honour property attributes: static or private properties raise errors,
60  while dependent properties invoke `get.<name>` when available.
61- Handle objects dereference to their underlying instance automatically. Deleted handles raise
62  the standard MATLAB-style error.
63- `MException` values expose the `message`, `identifier`, and `stack` fields for compatibility
64  with MATLAB error handling.
65
66## `getfield` Function GPU Execution Behaviour
67`getfield` is metadata-only. When structs or objects contain GPU tensors, the tensors
68remain on the device. The builtin manipulates only the host-side metadata and does not
69dispatch GPU kernels or gather buffers back to the CPU. Results inherit residency from the
70values being returned.
71
72## Examples of using the `getfield` function in MATLAB / RunMat
73
74### Reading a scalar struct field
75```matlab
76stats = struct("mean", 42, "stdev", 3.5);
77mu = getfield(stats, "mean");
78```
79
80Expected output:
81```matlab
82mu = 42
83```
84
85### Navigating nested structs with multiple field names
86```matlab
87cfg = struct("solver", struct("name", "cg", "tolerance", 1e-6));
88tol = getfield(cfg, "solver", "tolerance");
89```
90
91Expected output:
92```matlab
93tol = 1.0000e-06
94```
95
96### Accessing an element of a struct array
97```matlab
98people = struct("name", {"Ada", "Grace"}, "id", {101, 102});
99lastId = getfield(people, {2}, "id");
100```
101
102Expected output:
103```matlab
104lastId = 102
105```
106
107### Reading the first element of a struct array automatically
108```matlab
109people = struct("name", {"Ada", "Grace"}, "id", {101, 102});
110firstName = getfield(people, "name");
111```
112
113Expected output:
114```matlab
115firstName = "Ada"
116```
117
118### Using `end` to reference the last item in a field
119```matlab
120series = struct("values", {1:5});
121lastValue = getfield(series, "values", {"end"});
122```
123
124Expected output:
125```matlab
126lastValue = 5
127```
128
129### Indexing into a numeric field value
130```matlab
131measurements = struct("values", {[1 2 3; 4 5 6]});
132entry = getfield(measurements, "values", {2, 3});
133```
134
135Expected output:
136```matlab
137entry = 6
138```
139
140### Gathering the exception message from a try/catch block
141```matlab
142try
143    error("MATLAB:domainError", "Bad input");
144catch e
145    msg = getfield(e, "message");
146end
147```
148
149Expected output:
150```matlab
151msg = 'Bad input'
152```
153
154## GPU residency in RunMat (Do I need `gpuArray`?)
155You do not have to move data between CPU and GPU explicitly when calling `getfield`. The
156builtin works entirely with metadata and returns handles or tensors in whatever memory space
157they already inhabit.
158
159## FAQ
160
161### Does `getfield` work on non-struct values?
162No. The first argument must be a scalar struct, a struct array (possibly empty), an object
163instance created with `new_object`, a handle object, or an `MException`. Passing other
164types raises an error.
165
166### How do I access nested struct fields?
167Provide every level explicitly: `getfield(S, "parent", "child", "leaf")` traverses the same
168path as `S.parent.child.leaf`.
169
170### How do I read from a struct array?
171Supply a cell array of indices before the first field name. For example,
172`getfield(S, {3}, "value")` mirrors `S(3).value`. Indices are one-based like MATLAB.
173
174### Can I index the value stored in a field?
175Yes. You may supply scalars or `end` inside the index cell to reference elements of the
176field value.
177
178### Do dependent properties run their getter methods?
179Yes. If a property is marked `Dependent` and a `get.propertyName` builtin exists, `getfield`
180invokes it. Otherwise the backing field `<property>_backing` is inspected.
181
182### What happens when I query a deleted handle object?
183RunMat mirrors MATLAB by raising `Invalid or deleted handle object 'ClassName'.`
184
185## See Also
186[fieldnames](./fieldnames), [isfield](./isfield), [setfield](./setfield), [struct](./struct), [class](../../introspection/class)
187"#;
188
189pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
190    name: "getfield",
191    op_kind: GpuOpKind::Custom("getfield"),
192    supported_precisions: &[],
193    broadcast: BroadcastSemantics::None,
194    provider_hooks: &[],
195    constant_strategy: ConstantStrategy::InlineLiteral,
196    residency: ResidencyPolicy::InheritInputs,
197    nan_mode: ReductionNaN::Include,
198    two_pass_threshold: None,
199    workgroup_size: None,
200    accepts_nan_mode: false,
201    notes: "Pure metadata operation; acceleration providers do not participate.",
202};
203
204register_builtin_gpu_spec!(GPU_SPEC);
205
206pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
207    name: "getfield",
208    shape: ShapeRequirements::Any,
209    constant_strategy: ConstantStrategy::InlineLiteral,
210    elementwise: None,
211    reduction: None,
212    emits_nan: false,
213    notes: "Acts as a fusion barrier because it inspects metadata on the host.",
214};
215
216register_builtin_fusion_spec!(FUSION_SPEC);
217
218#[cfg(feature = "doc_export")]
219register_builtin_doc_text!("getfield", DOC_MD);
220
221#[runtime_builtin(
222    name = "getfield",
223    category = "structs/core",
224    summary = "Access a field or property from structs, struct arrays, or MATLAB-style objects.",
225    keywords = "getfield,struct,object,field access"
226)]
227fn getfield_builtin(base: Value, rest: Vec<Value>) -> Result<Value, String> {
228    let parsed = parse_arguments(rest)?;
229
230    let mut current = base;
231    if let Some(index) = parsed.leading_index {
232        current = apply_indices(current, &index)?;
233    }
234
235    for step in parsed.fields {
236        current = get_field_value(current, &step.name)?;
237        if let Some(index) = step.index {
238            current = apply_indices(current, &index)?;
239        }
240    }
241
242    Ok(current)
243}
244
245#[derive(Default)]
246struct ParsedArguments {
247    leading_index: Option<IndexSelector>,
248    fields: Vec<FieldStep>,
249}
250
251struct FieldStep {
252    name: String,
253    index: Option<IndexSelector>,
254}
255
256#[derive(Clone)]
257struct IndexSelector {
258    components: Vec<IndexComponent>,
259}
260
261#[derive(Clone)]
262enum IndexComponent {
263    Scalar(usize),
264    End,
265}
266
267fn parse_arguments(mut rest: Vec<Value>) -> Result<ParsedArguments, String> {
268    if rest.is_empty() {
269        return Err("getfield: expected at least one field name".to_string());
270    }
271
272    let mut parsed = ParsedArguments::default();
273    if let Some(first) = rest.first() {
274        if is_index_selector(first) {
275            let value = rest.remove(0);
276            parsed.leading_index = Some(parse_index_selector(value)?);
277        }
278    }
279
280    if rest.is_empty() {
281        return Err("getfield: expected field name after indices".to_string());
282    }
283
284    let mut iter = rest.into_iter().peekable();
285    while let Some(arg) = iter.next() {
286        let field_name = parse_field_name(arg)?;
287        let mut step = FieldStep {
288            name: field_name,
289            index: None,
290        };
291        if let Some(next) = iter.peek() {
292            if is_index_selector(next) {
293                let selector = iter.next().unwrap();
294                step.index = Some(parse_index_selector(selector)?);
295            }
296        }
297        parsed.fields.push(step);
298    }
299
300    if parsed.fields.is_empty() {
301        return Err("getfield: expected field name arguments".to_string());
302    }
303
304    Ok(parsed)
305}
306
307fn is_index_selector(value: &Value) -> bool {
308    matches!(value, Value::Cell(_))
309}
310
311fn parse_index_selector(value: Value) -> Result<IndexSelector, String> {
312    let Value::Cell(cell) = value else {
313        return Err("getfield: indices must be provided in a cell array".to_string());
314    };
315
316    let mut components = Vec::with_capacity(cell.data.len());
317    for handle in &cell.data {
318        let entry = unsafe { &*handle.as_raw() };
319        components.push(parse_index_component(entry)?);
320    }
321
322    Ok(IndexSelector { components })
323}
324
325fn parse_index_component(value: &Value) -> Result<IndexComponent, String> {
326    match value {
327        Value::CharArray(ca) => {
328            let text: String = ca.data.iter().collect();
329            parse_index_text(text.trim())
330        }
331        Value::String(s) => parse_index_text(s.trim()),
332        Value::StringArray(sa) if sa.data.len() == 1 => parse_index_text(sa.data[0].trim()),
333        _ => {
334            let idx = parse_positive_scalar(value)
335                .map_err(|e| format!("getfield: invalid index element ({e})"))?;
336            Ok(IndexComponent::Scalar(idx))
337        }
338    }
339}
340
341fn parse_index_text(text: &str) -> Result<IndexComponent, String> {
342    if text.eq_ignore_ascii_case("end") {
343        return Ok(IndexComponent::End);
344    }
345    if text == ":" {
346        return Err("getfield: ':' indexing is not currently supported".to_string());
347    }
348    if text.is_empty() {
349        return Err("getfield: index elements must not be empty".to_string());
350    }
351    if let Ok(value) = text.parse::<usize>() {
352        if value == 0 {
353            return Err("getfield: index must be >= 1".to_string());
354        }
355        return Ok(IndexComponent::Scalar(value));
356    }
357    Err(format!("getfield: invalid index element '{}'", text))
358}
359
360fn parse_positive_scalar(value: &Value) -> Result<usize, String> {
361    let number = match value {
362        Value::Int(i) => i.to_i64() as f64,
363        Value::Num(n) => *n,
364        Value::Tensor(t) if t.data.len() == 1 => t.data[0],
365        _ => {
366            let repr = format!("{value:?}");
367            return Err(format!("expected positive integer index, got {repr}"));
368        }
369    };
370
371    if !number.is_finite() {
372        return Err("index must be a finite number".to_string());
373    }
374    if number.fract() != 0.0 {
375        return Err("index must be an integer".to_string());
376    }
377    if number <= 0.0 {
378        return Err("index must be >= 1".to_string());
379    }
380    if number > usize::MAX as f64 {
381        return Err("index exceeds platform limits".to_string());
382    }
383    Ok(number as usize)
384}
385
386fn parse_field_name(value: Value) -> Result<String, String> {
387    match value {
388        Value::String(s) => Ok(s),
389        Value::StringArray(sa) => {
390            if sa.data.len() == 1 {
391                Ok(sa.data[0].clone())
392            } else {
393                Err(
394                    "getfield: field names must be scalar string arrays or character vectors"
395                        .to_string(),
396                )
397            }
398        }
399        Value::CharArray(ca) => {
400            if ca.rows == 1 {
401                Ok(ca.data.iter().collect())
402            } else {
403                Err("getfield: field names must be 1-by-N character vectors".to_string())
404            }
405        }
406        other => Err(format!("getfield: expected field name, got {other:?}")),
407    }
408}
409
410fn apply_indices(value: Value, selector: &IndexSelector) -> Result<Value, String> {
411    if selector.components.is_empty() {
412        return Err("getfield: index cell must contain at least one element".to_string());
413    }
414
415    let value = match value {
416        Value::GpuTensor(handle) => crate::dispatcher::gather_if_needed(&Value::GpuTensor(handle))
417            .map_err(|e| format!("getfield: {e}"))?,
418        other => other,
419    };
420
421    let resolved = resolve_indices(&value, selector)?;
422    let resolved_f64: Vec<f64> = resolved.iter().map(|&idx| idx as f64).collect();
423
424    match &value {
425        Value::LogicalArray(logical) => {
426            let tensor =
427                tensor::logical_to_tensor(logical).map_err(|e| format!("getfield: {e}"))?;
428            let scratch = Value::Tensor(tensor);
429            let indexed =
430                perform_indexing(&scratch, &resolved_f64).map_err(|e| format!("getfield: {e}"))?;
431            match indexed {
432                Value::Num(n) => Ok(Value::Bool(n != 0.0)),
433                Value::Tensor(t) => {
434                    let bits: Vec<u8> = t
435                        .data
436                        .iter()
437                        .map(|&v| if v != 0.0 { 1 } else { 0 })
438                        .collect();
439                    let logical = LogicalArray::new(bits, t.shape.clone())
440                        .map_err(|e| format!("getfield: {e}"))?;
441                    Ok(Value::LogicalArray(logical))
442                }
443                other => Ok(other),
444            }
445        }
446        Value::CharArray(array) => index_char_array(array, &resolved),
447        Value::ComplexTensor(tensor) => index_complex_tensor(tensor, &resolved),
448        Value::Tensor(_)
449        | Value::StringArray(_)
450        | Value::Cell(_)
451        | Value::Num(_)
452        | Value::Int(_) => {
453            perform_indexing(&value, &resolved_f64).map_err(|e| format!("getfield: {e}"))
454        }
455        Value::Bool(_) => {
456            if resolved.len() == 1 && resolved[0] == 1 {
457                Ok(value)
458            } else {
459                Err("Index exceeds the number of array elements.".to_string())
460            }
461        }
462        _ => Err("Struct contents reference from a non-struct array object.".to_string()),
463    }
464}
465
466fn resolve_indices(value: &Value, selector: &IndexSelector) -> Result<Vec<usize>, String> {
467    let dims = selector.components.len();
468    let mut resolved = Vec::with_capacity(dims);
469    for (dim_idx, component) in selector.components.iter().enumerate() {
470        let index = match component {
471            IndexComponent::Scalar(idx) => *idx,
472            IndexComponent::End => dimension_length(value, dims, dim_idx)?,
473        };
474        resolved.push(index);
475    }
476    Ok(resolved)
477}
478
479fn dimension_length(value: &Value, dims: usize, dim_idx: usize) -> Result<usize, String> {
480    match value {
481        Value::Tensor(tensor) => tensor_dimension_length(tensor, dims, dim_idx),
482        Value::Cell(cell) => cell_dimension_length(cell, dims, dim_idx),
483        Value::StringArray(sa) => string_array_dimension_length(sa, dims, dim_idx),
484        Value::LogicalArray(logical) => logical_array_dimension_length(logical, dims, dim_idx),
485        Value::CharArray(array) => char_array_dimension_length(array, dims, dim_idx),
486        Value::ComplexTensor(tensor) => complex_tensor_dimension_length(tensor, dims, dim_idx),
487        Value::Num(_) | Value::Int(_) | Value::Bool(_) => {
488            if dims == 1 {
489                Ok(1)
490            } else {
491                Err(
492                    "getfield: indexing with more than one dimension is not supported for scalars"
493                        .to_string(),
494                )
495            }
496        }
497        _ => Err("Struct contents reference from a non-struct array object.".to_string()),
498    }
499}
500
501fn tensor_dimension_length(tensor: &Tensor, dims: usize, dim_idx: usize) -> Result<usize, String> {
502    if dims == 1 {
503        let total = tensor.data.len();
504        if total == 0 {
505            return Err("Index exceeds the number of array elements (0).".to_string());
506        }
507        return Ok(total);
508    }
509    if dims > 2 {
510        return Err(
511            "getfield: indexing with more than two indices is not supported yet".to_string(),
512        );
513    }
514    let len = if dim_idx == 0 {
515        tensor.rows()
516    } else {
517        tensor.cols()
518    };
519    if len == 0 {
520        return Err("Index exceeds the number of array elements (0).".to_string());
521    }
522    Ok(len)
523}
524
525fn cell_dimension_length(cell: &CellArray, dims: usize, dim_idx: usize) -> Result<usize, String> {
526    if dims == 1 {
527        let total = cell.data.len();
528        if total == 0 {
529            return Err("Index exceeds the number of array elements (0).".to_string());
530        }
531        return Ok(total);
532    }
533    if dims > 2 {
534        return Err(
535            "getfield: indexing with more than two indices is not supported yet".to_string(),
536        );
537    }
538    let len = if dim_idx == 0 { cell.rows } else { cell.cols };
539    if len == 0 {
540        return Err("Index exceeds the number of array elements (0).".to_string());
541    }
542    Ok(len)
543}
544
545fn string_array_dimension_length(
546    array: &runmat_builtins::StringArray,
547    dims: usize,
548    dim_idx: usize,
549) -> Result<usize, String> {
550    if dims == 1 {
551        let total = array.data.len();
552        if total == 0 {
553            return Err("Index exceeds the number of array elements (0).".to_string());
554        }
555        return Ok(total);
556    }
557    if dims > 2 {
558        return Err(
559            "getfield: indexing with more than two indices is not supported yet".to_string(),
560        );
561    }
562    let len = if dim_idx == 0 {
563        array.rows()
564    } else {
565        array.cols()
566    };
567    if len == 0 {
568        return Err("Index exceeds the number of array elements (0).".to_string());
569    }
570    Ok(len)
571}
572
573fn logical_array_dimension_length(
574    logical: &LogicalArray,
575    dims: usize,
576    dim_idx: usize,
577) -> Result<usize, String> {
578    if dims == 1 {
579        let total = logical.data.len();
580        if total == 0 {
581            return Err("Index exceeds the number of array elements (0).".to_string());
582        }
583        return Ok(total);
584    }
585    if dims > 2 {
586        return Err(
587            "getfield: indexing with more than two indices is not supported yet".to_string(),
588        );
589    }
590    let len = if dim_idx == 0 {
591        logical.shape.first().copied().unwrap_or(logical.data.len())
592    } else {
593        logical.shape.get(1).copied().unwrap_or(1)
594    };
595    if len == 0 {
596        return Err("Index exceeds the number of array elements (0).".to_string());
597    }
598    Ok(len)
599}
600
601fn char_array_dimension_length(
602    array: &CharArray,
603    dims: usize,
604    dim_idx: usize,
605) -> Result<usize, String> {
606    if dims == 1 {
607        let total = array.rows * array.cols;
608        if total == 0 {
609            return Err("Index exceeds the number of array elements (0).".to_string());
610        }
611        return Ok(total);
612    }
613    if dims > 2 {
614        return Err(
615            "getfield: indexing with more than two indices is not supported yet".to_string(),
616        );
617    }
618    let len = if dim_idx == 0 { array.rows } else { array.cols };
619    if len == 0 {
620        return Err("Index exceeds the number of array elements (0).".to_string());
621    }
622    Ok(len)
623}
624
625fn complex_tensor_dimension_length(
626    tensor: &ComplexTensor,
627    dims: usize,
628    dim_idx: usize,
629) -> Result<usize, String> {
630    if dims == 1 {
631        let total = tensor.data.len();
632        if total == 0 {
633            return Err("Index exceeds the number of array elements (0).".to_string());
634        }
635        return Ok(total);
636    }
637    if dims > 2 {
638        return Err(
639            "getfield: indexing with more than two indices is not supported yet".to_string(),
640        );
641    }
642    let len = if dim_idx == 0 {
643        tensor.rows
644    } else {
645        tensor.cols
646    };
647    if len == 0 {
648        return Err("Index exceeds the number of array elements (0).".to_string());
649    }
650    Ok(len)
651}
652
653fn index_char_array(array: &CharArray, indices: &[usize]) -> Result<Value, String> {
654    if indices.is_empty() {
655        return Err("getfield: at least one index is required for char arrays".to_string());
656    }
657    if indices.len() == 1 {
658        let total = array.rows * array.cols;
659        let idx = indices[0];
660        if idx == 0 || idx > total {
661            return Err("Index exceeds the number of array elements.".to_string());
662        }
663        let linear = idx - 1;
664        let rows = array.rows.max(1);
665        let col = linear / rows;
666        let row = linear % rows;
667        let pos = row * array.cols + col;
668        let ch = array
669            .data
670            .get(pos)
671            .copied()
672            .ok_or_else(|| "Index exceeds the number of array elements.".to_string())?;
673        let out = CharArray::new(vec![ch], 1, 1).map_err(|e| format!("getfield: {e}"))?;
674        return Ok(Value::CharArray(out));
675    }
676    if indices.len() == 2 {
677        let row = indices[0];
678        let col = indices[1];
679        if row == 0 || row > array.rows || col == 0 || col > array.cols {
680            return Err("Index exceeds the number of array elements.".to_string());
681        }
682        let pos = (row - 1) * array.cols + (col - 1);
683        let ch = array
684            .data
685            .get(pos)
686            .copied()
687            .ok_or_else(|| "Index exceeds the number of array elements.".to_string())?;
688        let out = CharArray::new(vec![ch], 1, 1).map_err(|e| format!("getfield: {e}"))?;
689        return Ok(Value::CharArray(out));
690    }
691    Err(
692        "getfield: indexing with more than two indices is not supported for char arrays"
693            .to_string(),
694    )
695}
696
697fn index_complex_tensor(tensor: &ComplexTensor, indices: &[usize]) -> Result<Value, String> {
698    if indices.is_empty() {
699        return Err("getfield: at least one index is required for complex tensors".to_string());
700    }
701    if indices.len() == 1 {
702        let total = tensor.data.len();
703        let idx = indices[0];
704        if idx == 0 || idx > total {
705            return Err("Index exceeds the number of array elements.".to_string());
706        }
707        let (re, im) = tensor.data[idx - 1];
708        return Ok(Value::Complex(re, im));
709    }
710    if indices.len() == 2 {
711        let row = indices[0];
712        let col = indices[1];
713        if row == 0 || row > tensor.rows || col == 0 || col > tensor.cols {
714            return Err("Index exceeds the number of array elements.".to_string());
715        }
716        let pos = (row - 1) + (col - 1) * tensor.rows;
717        let (re, im) = tensor
718            .data
719            .get(pos)
720            .copied()
721            .ok_or_else(|| "Index exceeds the number of array elements.".to_string())?;
722        return Ok(Value::Complex(re, im));
723    }
724    Err(
725        "getfield: indexing with more than two indices is not supported for complex tensors"
726            .to_string(),
727    )
728}
729
730fn get_field_value(value: Value, name: &str) -> Result<Value, String> {
731    match value {
732        Value::Struct(st) => get_struct_field(&st, name),
733        Value::Object(obj) => get_object_field(&obj, name),
734        Value::HandleObject(handle) => get_handle_field(&handle, name),
735        Value::Listener(listener) => get_listener_field(&listener, name),
736        Value::MException(ex) => get_exception_field(&ex, name),
737        Value::Cell(cell) if is_struct_array(&cell) => {
738            let Some(first) = struct_array_first(&cell)? else {
739                return Err("Struct contents reference from an empty struct array.".to_string());
740            };
741            get_field_value(first, name)
742        }
743        _ => Err("Struct contents reference from a non-struct array object.".to_string()),
744    }
745}
746
747fn get_struct_field(struct_value: &StructValue, name: &str) -> Result<Value, String> {
748    struct_value
749        .fields
750        .get(name)
751        .cloned()
752        .ok_or_else(|| format!("Reference to non-existent field '{}'.", name))
753}
754
755fn get_object_field(obj: &ObjectInstance, name: &str) -> Result<Value, String> {
756    if let Some((prop, _owner)) = runmat_builtins::lookup_property(&obj.class_name, name) {
757        if prop.is_static {
758            return Err(format!(
759                "You cannot access the static property '{}' through an instance of class '{}'.",
760                name, obj.class_name
761            ));
762        }
763        if prop.get_access == Access::Private {
764            return Err(format!(
765                "You cannot get the '{}' property of '{}' class.",
766                name, obj.class_name
767            ));
768        }
769        if prop.is_dependent {
770            let getter = format!("get.{name}");
771            match call_builtin(&getter, &[Value::Object(obj.clone())]) {
772                Ok(value) => return Ok(value),
773                Err(err) => {
774                    if !err.contains("MATLAB:UndefinedFunction") {
775                        return Err(err);
776                    }
777                }
778            }
779            if let Some(val) = obj.properties.get(&format!("{name}_backing")) {
780                return Ok(val.clone());
781            }
782        }
783    }
784
785    if let Some(value) = obj.properties.get(name) {
786        return Ok(value.clone());
787    }
788
789    if let Some((prop, _owner)) = runmat_builtins::lookup_property(&obj.class_name, name) {
790        if prop.get_access == Access::Private {
791            return Err(format!(
792                "You cannot get the '{}' property of '{}' class.",
793                name, obj.class_name
794            ));
795        }
796        return Err(format!(
797            "No public property '{}' for class '{}'.",
798            name, obj.class_name
799        ));
800    }
801
802    Err(format!(
803        "Undefined property '{}' for class {}",
804        name, obj.class_name
805    ))
806}
807
808fn get_handle_field(handle: &HandleRef, name: &str) -> Result<Value, String> {
809    if !handle.valid {
810        return Err(format!(
811            "Invalid or deleted handle object '{}'.",
812            handle.class_name
813        ));
814    }
815    let target = unsafe { &*handle.target.as_raw() }.clone();
816    get_field_value(target, name)
817}
818
819fn get_listener_field(listener: &Listener, name: &str) -> Result<Value, String> {
820    match name {
821        "Enabled" | "enabled" => Ok(Value::Bool(listener.enabled)),
822        "Valid" | "valid" => Ok(Value::Bool(listener.valid)),
823        "EventName" | "event_name" => Ok(Value::String(listener.event_name.clone())),
824        "Callback" | "callback" => {
825            let value = unsafe { &*listener.callback.as_raw() }.clone();
826            Ok(value)
827        }
828        "Target" | "target" => {
829            let value = unsafe { &*listener.target.as_raw() }.clone();
830            Ok(value)
831        }
832        "Id" | "id" => Ok(Value::Int(runmat_builtins::IntValue::U64(listener.id))),
833        other => Err(format!(
834            "getfield: unknown field '{}' on listener object",
835            other
836        )),
837    }
838}
839
840fn get_exception_field(exception: &MException, name: &str) -> Result<Value, String> {
841    match name {
842        "message" => Ok(Value::String(exception.message.clone())),
843        "identifier" => Ok(Value::String(exception.identifier.clone())),
844        "stack" => exception_stack_to_value(&exception.stack),
845        other => Err(format!("Reference to non-existent field '{}'.", other)),
846    }
847}
848
849fn exception_stack_to_value(stack: &[String]) -> Result<Value, String> {
850    if stack.is_empty() {
851        return make_cell_with_shape(Vec::new(), vec![0, 1]).map_err(|e| format!("getfield: {e}"));
852    }
853    let mut values = Vec::with_capacity(stack.len());
854    for frame in stack {
855        values.push(Value::String(frame.clone()));
856    }
857    make_cell_with_shape(values, vec![stack.len(), 1]).map_err(|e| format!("getfield: {e}"))
858}
859
860fn is_struct_array(cell: &CellArray) -> bool {
861    cell.data
862        .iter()
863        .all(|handle| matches!(unsafe { &*handle.as_raw() }, Value::Struct(_)))
864}
865
866fn struct_array_first(cell: &CellArray) -> Result<Option<Value>, String> {
867    if cell.data.is_empty() {
868        return Ok(None);
869    }
870    let handle = cell.data.first().unwrap();
871    let value = unsafe { &*handle.as_raw() };
872    match value {
873        Value::Struct(_) => Ok(Some(value.clone())),
874        _ => Err("getfield: expected struct array elements to be structs".to_string()),
875    }
876}
877
878#[cfg(test)]
879mod tests {
880    use super::*;
881    use runmat_builtins::{
882        Access, CellArray, CharArray, ClassDef, ComplexTensor, HandleRef, IntValue, Listener,
883        MException, ObjectInstance, PropertyDef, StructValue,
884    };
885    use runmat_gc_api::GcPtr;
886
887    #[cfg(feature = "wgpu")]
888    use runmat_accelerate::backend::wgpu::provider as wgpu_backend;
889    #[cfg(feature = "wgpu")]
890    use runmat_accelerate_api::HostTensorView;
891
892    #[cfg(feature = "doc_export")]
893    use crate::builtins::common::test_support;
894
895    #[test]
896    fn getfield_scalar_struct() {
897        let mut st = StructValue::new();
898        st.fields.insert("answer".to_string(), Value::Num(42.0));
899        let value =
900            getfield_builtin(Value::Struct(st), vec![Value::from("answer")]).expect("getfield");
901        assert_eq!(value, Value::Num(42.0));
902    }
903
904    #[test]
905    fn getfield_nested_structs() {
906        let mut inner = StructValue::new();
907        inner.fields.insert("depth".to_string(), Value::Num(3.0));
908        let mut outer = StructValue::new();
909        outer
910            .fields
911            .insert("inner".to_string(), Value::Struct(inner));
912        let result = getfield_builtin(
913            Value::Struct(outer),
914            vec![Value::from("inner"), Value::from("depth")],
915        )
916        .expect("nested getfield");
917        assert_eq!(result, Value::Num(3.0));
918    }
919
920    #[test]
921    fn getfield_struct_array_element() {
922        let mut first = StructValue::new();
923        first.fields.insert("name".to_string(), Value::from("Ada"));
924        let mut second = StructValue::new();
925        second
926            .fields
927            .insert("name".to_string(), Value::from("Grace"));
928        let array = CellArray::new_with_shape(
929            vec![Value::Struct(first), Value::Struct(second)],
930            vec![1, 2],
931        )
932        .unwrap();
933        let index =
934            CellArray::new_with_shape(vec![Value::Int(IntValue::I32(2))], vec![1, 1]).unwrap();
935        let result = getfield_builtin(
936            Value::Cell(array),
937            vec![Value::Cell(index), Value::from("name")],
938        )
939        .expect("struct array element");
940        assert_eq!(result, Value::from("Grace"));
941    }
942
943    #[test]
944    fn getfield_object_property() {
945        let mut obj = ObjectInstance::new("TestClass".to_string());
946        obj.properties.insert("value".to_string(), Value::Num(7.0));
947        let result =
948            getfield_builtin(Value::Object(obj), vec![Value::from("value")]).expect("object");
949        assert_eq!(result, Value::Num(7.0));
950    }
951
952    #[test]
953    fn getfield_missing_field_errors() {
954        let st = StructValue::new();
955        let err = getfield_builtin(Value::Struct(st), vec![Value::from("missing")]).unwrap_err();
956        assert!(err.contains("Reference to non-existent field 'missing'"));
957    }
958
959    #[test]
960    fn getfield_exception_fields() {
961        let ex = MException::new("MATLAB:Test".to_string(), "failure".to_string());
962        let msg = getfield_builtin(Value::MException(ex.clone()), vec![Value::from("message")])
963            .expect("message");
964        assert_eq!(msg, Value::String("failure".to_string()));
965        let ident = getfield_builtin(Value::MException(ex), vec![Value::from("identifier")])
966            .expect("identifier");
967        assert_eq!(ident, Value::String("MATLAB:Test".to_string()));
968    }
969
970    #[test]
971    fn getfield_exception_stack_cell() {
972        let mut ex = MException::new("MATLAB:Test".to_string(), "failure".to_string());
973        ex.stack.push("demo.m:5".to_string());
974        ex.stack.push("main.m:1".to_string());
975        let stack =
976            getfield_builtin(Value::MException(ex), vec![Value::from("stack")]).expect("stack");
977        let Value::Cell(cell) = stack else {
978            panic!("expected cell array");
979        };
980        assert_eq!(cell.rows, 2);
981        assert_eq!(cell.cols, 1);
982        let first = unsafe { &*cell.data[0].as_raw() }.clone();
983        assert_eq!(first, Value::String("demo.m:5".to_string()));
984    }
985
986    #[test]
987    #[cfg(feature = "doc_export")]
988    fn doc_examples_present() {
989        let blocks = test_support::doc_examples(DOC_MD);
990        assert!(!blocks.is_empty());
991    }
992
993    #[test]
994    fn indexing_missing_field_name_fails() {
995        let mut outer = StructValue::new();
996        outer.fields.insert("inner".to_string(), Value::Num(1.0));
997        let index =
998            CellArray::new_with_shape(vec![Value::Int(IntValue::I32(1))], vec![1, 1]).unwrap();
999        let err = getfield_builtin(Value::Struct(outer), vec![Value::Cell(index)]).unwrap_err();
1000        assert!(err.contains("expected field name"));
1001    }
1002
1003    #[test]
1004    fn getfield_supports_end_index() {
1005        let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
1006        let mut st = StructValue::new();
1007        st.fields
1008            .insert("values".to_string(), Value::Tensor(tensor));
1009        let idx_cell =
1010            CellArray::new(vec![Value::CharArray(CharArray::new_row("end"))], 1, 1).unwrap();
1011        let result = getfield_builtin(
1012            Value::Struct(st),
1013            vec![Value::from("values"), Value::Cell(idx_cell)],
1014        )
1015        .expect("end index");
1016        assert_eq!(result, Value::Num(3.0));
1017    }
1018
1019    #[test]
1020    fn getfield_struct_array_defaults_to_first() {
1021        let mut first = StructValue::new();
1022        first.fields.insert("name".to_string(), Value::from("Ada"));
1023        let mut second = StructValue::new();
1024        second
1025            .fields
1026            .insert("name".to_string(), Value::from("Grace"));
1027        let array = CellArray::new_with_shape(
1028            vec![Value::Struct(first), Value::Struct(second)],
1029            vec![1, 2],
1030        )
1031        .unwrap();
1032        let result =
1033            getfield_builtin(Value::Cell(array), vec![Value::from("name")]).expect("default index");
1034        assert_eq!(result, Value::from("Ada"));
1035    }
1036
1037    #[test]
1038    fn getfield_char_array_single_element() {
1039        let chars = CharArray::new_row("Ada");
1040        let mut st = StructValue::new();
1041        st.fields
1042            .insert("name".to_string(), Value::CharArray(chars));
1043        let index =
1044            CellArray::new_with_shape(vec![Value::Int(IntValue::I32(2))], vec![1, 1]).unwrap();
1045        let result = getfield_builtin(
1046            Value::Struct(st),
1047            vec![Value::from("name"), Value::Cell(index)],
1048        )
1049        .expect("char indexing");
1050        match result {
1051            Value::CharArray(ca) => {
1052                assert_eq!(ca.rows, 1);
1053                assert_eq!(ca.cols, 1);
1054                assert_eq!(ca.data, vec!['d']);
1055            }
1056            other => panic!("expected 1x1 CharArray, got {other:?}"),
1057        }
1058    }
1059
1060    #[test]
1061    fn getfield_complex_tensor_index() {
1062        let tensor =
1063            ComplexTensor::new(vec![(1.0, 2.0), (3.0, 4.0)], vec![2, 1]).expect("complex tensor");
1064        let mut st = StructValue::new();
1065        st.fields
1066            .insert("vals".to_string(), Value::ComplexTensor(tensor));
1067        let index =
1068            CellArray::new_with_shape(vec![Value::Int(IntValue::I32(2))], vec![1, 1]).unwrap();
1069        let result = getfield_builtin(
1070            Value::Struct(st),
1071            vec![Value::from("vals"), Value::Cell(index)],
1072        )
1073        .expect("complex index");
1074        assert_eq!(result, Value::Complex(3.0, 4.0));
1075    }
1076
1077    #[test]
1078    fn getfield_dependent_property_invokes_getter() {
1079        let class_name = "runmat.unittest.GetfieldDependent";
1080        let mut def = ClassDef {
1081            name: class_name.to_string(),
1082            parent: None,
1083            properties: std::collections::HashMap::new(),
1084            methods: std::collections::HashMap::new(),
1085        };
1086        def.properties.insert(
1087            "p".to_string(),
1088            PropertyDef {
1089                name: "p".to_string(),
1090                is_static: false,
1091                is_dependent: true,
1092                get_access: Access::Public,
1093                set_access: Access::Public,
1094                default_value: None,
1095            },
1096        );
1097        runmat_builtins::register_class(def);
1098
1099        let mut obj = ObjectInstance::new(class_name.to_string());
1100        obj.properties
1101            .insert("p_backing".to_string(), Value::Num(42.0));
1102
1103        let result =
1104            getfield_builtin(Value::Object(obj), vec![Value::from("p")]).expect("dependent");
1105        assert_eq!(result, Value::Num(42.0));
1106    }
1107
1108    #[test]
1109    fn getfield_invalid_handle_errors() {
1110        let target = unsafe { GcPtr::from_raw(Box::into_raw(Box::new(Value::Num(1.0)))) };
1111        let handle = HandleRef {
1112            class_name: "Demo".to_string(),
1113            target,
1114            valid: false,
1115        };
1116        let err =
1117            getfield_builtin(Value::HandleObject(handle), vec![Value::from("x")]).unwrap_err();
1118        assert!(err.contains("Invalid or deleted handle object 'Demo'"));
1119    }
1120
1121    #[test]
1122    fn getfield_listener_fields_resolved() {
1123        let target = unsafe { GcPtr::from_raw(Box::into_raw(Box::new(Value::Num(7.0)))) };
1124        let callback = unsafe {
1125            GcPtr::from_raw(Box::into_raw(Box::new(Value::FunctionHandle(
1126                "cb".to_string(),
1127            ))))
1128        };
1129        let listener = Listener {
1130            id: 9,
1131            target,
1132            event_name: "tick".to_string(),
1133            callback,
1134            enabled: true,
1135            valid: true,
1136        };
1137        let enabled = getfield_builtin(
1138            Value::Listener(listener.clone()),
1139            vec![Value::from("Enabled")],
1140        )
1141        .expect("enabled");
1142        assert_eq!(enabled, Value::Bool(true));
1143        let event_name = getfield_builtin(
1144            Value::Listener(listener.clone()),
1145            vec![Value::from("EventName")],
1146        )
1147        .expect("event name");
1148        assert_eq!(event_name, Value::String("tick".to_string()));
1149        let callback = getfield_builtin(Value::Listener(listener), vec![Value::from("Callback")])
1150            .expect("callback");
1151        assert!(matches!(callback, Value::FunctionHandle(_)));
1152    }
1153
1154    #[test]
1155    #[cfg(feature = "wgpu")]
1156    fn getfield_gpu_tensor_indexing() {
1157        let _ = wgpu_backend::register_wgpu_provider(wgpu_backend::WgpuProviderOptions::default());
1158        let provider = runmat_accelerate_api::provider().expect("wgpu provider");
1159
1160        let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
1161        let view = HostTensorView {
1162            data: &tensor.data,
1163            shape: &tensor.shape,
1164        };
1165        let handle = provider.upload(&view).expect("upload");
1166
1167        let mut st = StructValue::new();
1168        st.fields
1169            .insert("values".to_string(), Value::GpuTensor(handle.clone()));
1170
1171        let direct = getfield_builtin(Value::Struct(st.clone()), vec![Value::from("values")])
1172            .expect("direct gpu field");
1173        match direct {
1174            Value::GpuTensor(out) => assert_eq!(out.buffer_id, handle.buffer_id),
1175            other => panic!("expected gpu tensor, got {other:?}"),
1176        }
1177
1178        let idx_cell =
1179            CellArray::new(vec![Value::CharArray(CharArray::new_row("end"))], 1, 1).unwrap();
1180        let indexed = getfield_builtin(
1181            Value::Struct(st),
1182            vec![Value::from("values"), Value::Cell(idx_cell)],
1183        )
1184        .expect("gpu indexed field");
1185        match indexed {
1186            Value::Num(v) => assert_eq!(v, 3.0),
1187            other => panic!("expected numeric scalar, got {other:?}"),
1188        }
1189    }
1190}