1use 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}