Skip to main content

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