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::indexing::perform_indexing;
4use crate::builtins::common::spec::{
5    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
6    ReductionNaN, ResidencyPolicy, ShapeRequirements,
7};
8use crate::builtins::common::tensor;
9use crate::builtins::introspection::dynamicprops;
10use crate::builtins::structs::type_resolvers::getfield_type;
11use crate::make_cell_with_shape;
12use crate::{
13    build_runtime_error, call_builtin_async, gather_if_needed_async, object_property_getter_name,
14    BuiltinResult, RuntimeError,
15};
16use runmat_builtins::{
17    Access, BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
18    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
19    CellArray, CharArray, ComplexTensor, HandleRef, Listener, LogicalArray, MException,
20    ObjectInstance, StructValue, Tensor, Value,
21};
22use runmat_macros::runtime_builtin;
23
24#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::structs::core::getfield")]
25pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
26    name: "getfield",
27    op_kind: GpuOpKind::Custom("getfield"),
28    supported_precisions: &[],
29    broadcast: BroadcastSemantics::None,
30    provider_hooks: &[],
31    constant_strategy: ConstantStrategy::InlineLiteral,
32    residency: ResidencyPolicy::InheritInputs,
33    nan_mode: ReductionNaN::Include,
34    two_pass_threshold: None,
35    workgroup_size: None,
36    accepts_nan_mode: false,
37    notes: "Pure metadata operation; acceleration providers do not participate.",
38};
39
40#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::structs::core::getfield")]
41pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
42    name: "getfield",
43    shape: ShapeRequirements::Any,
44    constant_strategy: ConstantStrategy::InlineLiteral,
45    elementwise: None,
46    reduction: None,
47    emits_nan: false,
48    notes: "Acts as a fusion barrier because it inspects metadata on the host.",
49};
50
51const BUILTIN_NAME: &str = "getfield";
52const GETFIELD_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
53    name: "value",
54    ty: BuiltinParamType::Any,
55    arity: BuiltinParamArity::Required,
56    default: None,
57    description: "Selected field/property value.",
58}];
59
60const GETFIELD_INPUTS_SCALAR: [BuiltinParamDescriptor; 2] = [
61    BuiltinParamDescriptor {
62        name: "S",
63        ty: BuiltinParamType::Any,
64        arity: BuiltinParamArity::Required,
65        default: None,
66        description: "Struct, struct array, object, or supported metadata container.",
67    },
68    BuiltinParamDescriptor {
69        name: "field",
70        ty: BuiltinParamType::PropertyName,
71        arity: BuiltinParamArity::Required,
72        default: None,
73        description: "Field/property name.",
74    },
75];
76
77const GETFIELD_INPUTS_NESTED: [BuiltinParamDescriptor; 2] = [
78    BuiltinParamDescriptor {
79        name: "S",
80        ty: BuiltinParamType::Any,
81        arity: BuiltinParamArity::Required,
82        default: None,
83        description: "Struct, struct array, object, or supported metadata container.",
84    },
85    BuiltinParamDescriptor {
86        name: "path",
87        ty: BuiltinParamType::Any,
88        arity: BuiltinParamArity::Variadic,
89        default: None,
90        description:
91            "Alternating field names and optional index-selector cells `{...}` for nested access.",
92    },
93];
94
95const GETFIELD_INPUTS_LEADING_INDEX: [BuiltinParamDescriptor; 3] = [
96    BuiltinParamDescriptor {
97        name: "S",
98        ty: BuiltinParamType::Any,
99        arity: BuiltinParamArity::Required,
100        default: None,
101        description: "Struct array or supported indexable container.",
102    },
103    BuiltinParamDescriptor {
104        name: "index_selector",
105        ty: BuiltinParamType::Any,
106        arity: BuiltinParamArity::Required,
107        default: None,
108        description: "Leading index selector in a cell array, e.g. `{2}` or `{end}`.",
109    },
110    BuiltinParamDescriptor {
111        name: "path",
112        ty: BuiltinParamType::Any,
113        arity: BuiltinParamArity::Variadic,
114        default: None,
115        description:
116            "Alternating field names and optional index-selector cells `{...}` for nested access.",
117    },
118];
119
120const GETFIELD_SIGNATURES: [BuiltinSignatureDescriptor; 3] = [
121    BuiltinSignatureDescriptor {
122        label: "value = getfield(S, field)",
123        inputs: &GETFIELD_INPUTS_SCALAR,
124        outputs: &GETFIELD_OUTPUT,
125    },
126    BuiltinSignatureDescriptor {
127        label: "value = getfield(S, field_or_index, ...)",
128        inputs: &GETFIELD_INPUTS_NESTED,
129        outputs: &GETFIELD_OUTPUT,
130    },
131    BuiltinSignatureDescriptor {
132        label: "value = getfield(S, {idx0}, field_or_index, ...)",
133        inputs: &GETFIELD_INPUTS_LEADING_INDEX,
134        outputs: &GETFIELD_OUTPUT,
135    },
136];
137
138const GETFIELD_ERROR_NOT_ENOUGH_INPUTS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
139    code: "RM.GETFIELD.NOT_ENOUGH_INPUTS",
140    identifier: Some("RunMat:getfield:NotEnoughInputs"),
141    when: "No field-name/path arguments are supplied.",
142    message: "getfield: expected at least one field name",
143};
144
145const GETFIELD_ERROR_FIELD_EXPECTED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
146    code: "RM.GETFIELD.FIELD_EXPECTED",
147    identifier: Some("RunMat:getfield:FieldExpected"),
148    when: "Field name is missing after indices or argument parsing.",
149    message: "getfield: expected field name arguments",
150};
151
152const GETFIELD_ERROR_INDEX_SELECTOR_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
153    code: "RM.GETFIELD.INDEX_SELECTOR_TYPE",
154    identifier: Some("RunMat:getfield:IndexSelectorType"),
155    when: "Index selector is not provided as a cell array.",
156    message: "getfield: indices must be provided in a cell array",
157};
158
159const GETFIELD_ERROR_INDEX_INVALID: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
160    code: "RM.GETFIELD.INDEX_INVALID",
161    identifier: Some("RunMat:getfield:InvalidIndex"),
162    when: "Index components are malformed, empty, unsupported, or not positive integers.",
163    message: "getfield: invalid index element",
164};
165
166const GETFIELD_ERROR_FIELD_NAME_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
167    code: "RM.GETFIELD.FIELD_NAME_TYPE",
168    identifier: Some("RunMat:getfield:FieldNameType"),
169    when: "Field name is not a string scalar or 1-by-N char vector.",
170    message: "getfield: expected field name",
171};
172
173const GETFIELD_ERROR_INDEX_SHAPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
174    code: "RM.GETFIELD.INDEX_SHAPE",
175    identifier: Some("RunMat:getfield:IndexShape"),
176    when: "Indexing rank/shape is unsupported for the targeted value.",
177    message: "getfield: unsupported index shape for target value",
178};
179
180const GETFIELD_ERROR_NON_STRUCT_REFERENCE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
181    code: "RM.GETFIELD.NON_STRUCT_REFERENCE",
182    identifier: Some("RunMat:getfield:NonStructReference"),
183    when: "Field lookup is attempted on a non-struct/non-object container.",
184    message: "Struct contents reference from a non-struct array object.",
185};
186
187const GETFIELD_ERROR_INDEX_OUT_OF_BOUNDS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
188    code: "RM.GETFIELD.INDEX_OUT_OF_BOUNDS",
189    identifier: Some("RunMat:getfield:IndexOutOfBounds"),
190    when: "Resolved index is outside the bounds of the targeted value.",
191    message: "Index exceeds the number of array elements.",
192};
193
194const GETFIELD_ERROR_MISSING_FIELD: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
195    code: "RM.GETFIELD.MISSING_FIELD",
196    identifier: Some("RunMat:getfield:MissingField"),
197    when: "Requested field does not exist on struct/exception/listener.",
198    message: "Reference to non-existent field",
199};
200
201const GETFIELD_ERROR_PROPERTY_PRIVATE_ACCESS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
202    code: "RM.GETFIELD.PROPERTY_PRIVATE_ACCESS",
203    identifier: Some("RunMat:PropertyPrivateAccess"),
204    when: "Property exists but is private/inaccessible from this context.",
205    message: "You cannot get this property from the current context.",
206};
207
208const GETFIELD_ERROR_OBJECT_PROPERTY: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
209    code: "RM.GETFIELD.OBJECT_PROPERTY",
210    identifier: Some("RunMat:getfield:ObjectProperty"),
211    when: "Object property access is invalid (static-through-instance, unknown, or non-public).",
212    message: "getfield: invalid object property access",
213};
214
215const GETFIELD_ERROR_INVALID_HANDLE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
216    code: "RM.GETFIELD.INVALID_HANDLE",
217    identifier: Some("RunMat:getfield:InvalidHandle"),
218    when: "Handle object is invalid/deleted.",
219    message: "Invalid or deleted handle object",
220};
221
222const GETFIELD_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
223    code: "RM.GETFIELD.INTERNAL",
224    identifier: Some("RunMat:getfield:InternalError"),
225    when: "Internal value conversion or output assembly failed.",
226    message: "getfield: internal error",
227};
228
229const GETFIELD_ERRORS: [BuiltinErrorDescriptor; 13] = [
230    GETFIELD_ERROR_NOT_ENOUGH_INPUTS,
231    GETFIELD_ERROR_FIELD_EXPECTED,
232    GETFIELD_ERROR_INDEX_SELECTOR_TYPE,
233    GETFIELD_ERROR_INDEX_INVALID,
234    GETFIELD_ERROR_FIELD_NAME_TYPE,
235    GETFIELD_ERROR_INDEX_SHAPE,
236    GETFIELD_ERROR_NON_STRUCT_REFERENCE,
237    GETFIELD_ERROR_INDEX_OUT_OF_BOUNDS,
238    GETFIELD_ERROR_MISSING_FIELD,
239    GETFIELD_ERROR_PROPERTY_PRIVATE_ACCESS,
240    GETFIELD_ERROR_OBJECT_PROPERTY,
241    GETFIELD_ERROR_INVALID_HANDLE,
242    GETFIELD_ERROR_INTERNAL,
243];
244
245pub const GETFIELD_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
246    signatures: &GETFIELD_SIGNATURES,
247    output_mode: BuiltinOutputMode::Fixed,
248    completion_policy: BuiltinCompletionPolicy::Public,
249    errors: &GETFIELD_ERRORS,
250};
251
252fn getfield_flow(message: impl Into<String>) -> RuntimeError {
253    getfield_error_with_message(message, &GETFIELD_ERROR_INTERNAL)
254}
255
256fn getfield_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
257    getfield_error_with_message(error.message, error)
258}
259
260fn getfield_error_with_message(
261    message: impl Into<String>,
262    error: &'static BuiltinErrorDescriptor,
263) -> RuntimeError {
264    let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
265    if let Some(identifier) = error.identifier {
266        builder = builder.with_identifier(identifier);
267    }
268    builder.build()
269}
270
271fn getfield_private_access(message: impl Into<String>) -> RuntimeError {
272    getfield_error_with_message(message, &GETFIELD_ERROR_PROPERTY_PRIVATE_ACCESS)
273}
274
275fn remap_getfield_flow(err: RuntimeError, prefix: Option<&str>) -> RuntimeError {
276    let mut message = err.message().to_string();
277    if let Some(prefix) = prefix {
278        if !message.starts_with(prefix) {
279            message = format!("{prefix}{message}");
280        }
281    }
282    let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
283    if let Some(identifier) = err.identifier() {
284        builder = builder.with_identifier(identifier);
285    }
286    builder.with_source(err).build()
287}
288
289fn is_undefined_function(err: &RuntimeError) -> bool {
290    err.identifier() == Some(crate::IDENT_UNDEFINED_FUNCTION)
291}
292
293#[runtime_builtin(
294    name = "getfield",
295    category = "structs/core",
296    summary = "Access struct or object fields.",
297    keywords = "getfield,struct,object,field access",
298    type_resolver(getfield_type),
299    descriptor(crate::builtins::structs::core::getfield::GETFIELD_DESCRIPTOR),
300    builtin_path = "crate::builtins::structs::core::getfield"
301)]
302async fn getfield_builtin(base: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
303    let parsed = parse_arguments(rest)?;
304
305    let mut current = base;
306    if let Some(index) = parsed.leading_index {
307        current = apply_indices(current, &index).await?;
308    }
309
310    for step in parsed.fields {
311        current = get_field_value(current, &step.name).await?;
312        if let Some(index) = step.index {
313            current = apply_indices(current, &index).await?;
314        }
315    }
316
317    Ok(current)
318}
319
320#[derive(Default)]
321struct ParsedArguments {
322    leading_index: Option<IndexSelector>,
323    fields: Vec<FieldStep>,
324}
325
326struct FieldStep {
327    name: String,
328    index: Option<IndexSelector>,
329}
330
331#[derive(Clone)]
332struct IndexSelector {
333    components: Vec<IndexComponent>,
334}
335
336#[derive(Clone)]
337enum IndexComponent {
338    Scalar(usize),
339    Vector(Vec<usize>, Vec<usize>),
340    End,
341}
342
343fn parse_arguments(mut rest: Vec<Value>) -> BuiltinResult<ParsedArguments> {
344    if rest.is_empty() {
345        return Err(getfield_error(&GETFIELD_ERROR_NOT_ENOUGH_INPUTS));
346    }
347
348    let mut parsed = ParsedArguments::default();
349    if let Some(first) = rest.first() {
350        if is_index_selector(first) {
351            let value = rest.remove(0);
352            parsed.leading_index = Some(parse_index_selector(value)?);
353        }
354    }
355
356    if rest.is_empty() {
357        return Err(getfield_error_with_message(
358            "getfield: expected field name after indices",
359            &GETFIELD_ERROR_FIELD_EXPECTED,
360        ));
361    }
362
363    let mut iter = rest.into_iter().peekable();
364    while let Some(arg) = iter.next() {
365        let field_name = parse_field_name(arg)?;
366        let mut step = FieldStep {
367            name: field_name,
368            index: None,
369        };
370        if let Some(next) = iter.peek() {
371            if is_index_selector(next) {
372                let selector = iter.next().unwrap();
373                step.index = Some(parse_index_selector(selector)?);
374            }
375        }
376        parsed.fields.push(step);
377    }
378
379    if parsed.fields.is_empty() {
380        return Err(getfield_error(&GETFIELD_ERROR_FIELD_EXPECTED));
381    }
382
383    Ok(parsed)
384}
385
386fn is_index_selector(value: &Value) -> bool {
387    matches!(value, Value::Cell(_))
388}
389
390fn parse_index_selector(value: Value) -> BuiltinResult<IndexSelector> {
391    let Value::Cell(cell) = value else {
392        return Err(getfield_error(&GETFIELD_ERROR_INDEX_SELECTOR_TYPE));
393    };
394
395    let mut components = Vec::with_capacity(cell.data.len());
396    for handle in &cell.data {
397        let entry = handle;
398        components.push(parse_index_component(entry)?);
399    }
400
401    Ok(IndexSelector { components })
402}
403
404fn parse_index_component(value: &Value) -> BuiltinResult<IndexComponent> {
405    match value {
406        Value::CharArray(ca) => {
407            let text: String = ca.data.iter().collect();
408            parse_index_text(text.trim())
409        }
410        Value::String(s) => parse_index_text(s.trim()),
411        Value::StringArray(sa) if sa.data.len() == 1 => parse_index_text(sa.data[0].trim()),
412        Value::Tensor(tensor) if tensor.data.len() > 1 => {
413            let indices = tensor
414                .data
415                .iter()
416                .map(|&value| parse_positive_integer(value))
417                .collect::<BuiltinResult<Vec<_>>>()?;
418            Ok(IndexComponent::Vector(indices, tensor.shape.clone()))
419        }
420        _ => {
421            let idx = parse_positive_scalar(value).map_err(|err| {
422                getfield_error_with_message(
423                    format!("getfield: invalid index element ({})", err.message()),
424                    &GETFIELD_ERROR_INDEX_INVALID,
425                )
426            })?;
427            Ok(IndexComponent::Scalar(idx))
428        }
429    }
430}
431
432fn parse_index_text(text: &str) -> BuiltinResult<IndexComponent> {
433    if text.eq_ignore_ascii_case("end") {
434        return Ok(IndexComponent::End);
435    }
436    if text == ":" {
437        return Err(getfield_error_with_message(
438            "getfield: ':' indexing is not currently supported",
439            &GETFIELD_ERROR_INDEX_INVALID,
440        ));
441    }
442    if text.is_empty() {
443        return Err(getfield_error_with_message(
444            "getfield: index elements must not be empty",
445            &GETFIELD_ERROR_INDEX_INVALID,
446        ));
447    }
448    if let Ok(value) = text.parse::<usize>() {
449        if value == 0 {
450            return Err(getfield_error_with_message(
451                "getfield: index must be >= 1",
452                &GETFIELD_ERROR_INDEX_INVALID,
453            ));
454        }
455        return Ok(IndexComponent::Scalar(value));
456    }
457    Err(getfield_error_with_message(
458        format!("getfield: invalid index element '{}'", text),
459        &GETFIELD_ERROR_INDEX_INVALID,
460    ))
461}
462
463fn parse_positive_scalar(value: &Value) -> BuiltinResult<usize> {
464    let number = match value {
465        Value::Int(i) => i.to_i64() as f64,
466        Value::Num(n) => *n,
467        Value::Tensor(t) if t.data.len() == 1 => t.data[0],
468        _ => {
469            let repr = format!("{value:?}");
470            return Err(getfield_error_with_message(
471                format!("expected positive integer index, got {repr}"),
472                &GETFIELD_ERROR_INDEX_INVALID,
473            ));
474        }
475    };
476
477    parse_positive_integer(number)
478}
479
480fn parse_positive_integer(number: f64) -> BuiltinResult<usize> {
481    if !number.is_finite() {
482        return Err(getfield_error_with_message(
483            "index must be a finite number",
484            &GETFIELD_ERROR_INDEX_INVALID,
485        ));
486    }
487    if number.fract() != 0.0 {
488        return Err(getfield_error_with_message(
489            "index must be an integer",
490            &GETFIELD_ERROR_INDEX_INVALID,
491        ));
492    }
493    if number <= 0.0 {
494        return Err(getfield_error_with_message(
495            "index must be >= 1",
496            &GETFIELD_ERROR_INDEX_INVALID,
497        ));
498    }
499    if number > usize::MAX as f64 {
500        return Err(getfield_error_with_message(
501            "index exceeds platform limits",
502            &GETFIELD_ERROR_INDEX_INVALID,
503        ));
504    }
505    Ok(number as usize)
506}
507
508fn parse_field_name(value: Value) -> BuiltinResult<String> {
509    match value {
510        Value::String(s) => Ok(s),
511        Value::StringArray(sa) => {
512            if sa.data.len() == 1 {
513                Ok(sa.data[0].clone())
514            } else {
515                Err(getfield_error_with_message(
516                    "getfield: field names must be scalar string arrays or character vectors",
517                    &GETFIELD_ERROR_FIELD_NAME_TYPE,
518                ))
519            }
520        }
521        Value::CharArray(ca) => {
522            if ca.rows == 1 {
523                Ok(ca.data.iter().collect())
524            } else {
525                Err(getfield_error_with_message(
526                    "getfield: field names must be 1-by-N character vectors",
527                    &GETFIELD_ERROR_FIELD_NAME_TYPE,
528                ))
529            }
530        }
531        other => Err(getfield_error_with_message(
532            format!("getfield: expected field name, got {other:?}"),
533            &GETFIELD_ERROR_FIELD_NAME_TYPE,
534        )),
535    }
536}
537
538async fn apply_indices(value: Value, selector: &IndexSelector) -> BuiltinResult<Value> {
539    if selector.components.is_empty() {
540        return Err(getfield_error_with_message(
541            "getfield: index cell must contain at least one element",
542            &GETFIELD_ERROR_INDEX_SELECTOR_TYPE,
543        ));
544    }
545
546    let value = match value {
547        Value::GpuTensor(handle) => gather_if_needed_async(&Value::GpuTensor(handle))
548            .await
549            .map_err(|flow| remap_getfield_flow(flow, Some("getfield: ")))?,
550        other => other,
551    };
552
553    if let Some(indexed) = apply_vector_index(&value, selector)? {
554        return Ok(indexed);
555    }
556
557    let resolved = resolve_indices(&value, selector)?;
558    let resolved_f64: Vec<f64> = resolved.iter().map(|&idx| idx as f64).collect();
559
560    match &value {
561        Value::LogicalArray(logical) => {
562            let tensor = tensor::logical_to_tensor(logical)
563                .map_err(|e| getfield_flow(format!("getfield: {e}")))?;
564            let scratch = Value::Tensor(tensor);
565            let indexed = perform_indexing(&scratch, &resolved_f64)
566                .await
567                .map_err(|err| remap_getfield_flow(err, Some("getfield: ")))?;
568            match indexed {
569                Value::Num(n) => Ok(Value::Bool(n != 0.0)),
570                Value::Tensor(t) => {
571                    let bits: Vec<u8> = t
572                        .data
573                        .iter()
574                        .map(|&v| if v != 0.0 { 1 } else { 0 })
575                        .collect();
576                    let logical = LogicalArray::new(bits, t.shape.clone())
577                        .map_err(|e| getfield_flow(format!("getfield: {e}")))?;
578                    Ok(Value::LogicalArray(logical))
579                }
580                other => Ok(other),
581            }
582        }
583        Value::CharArray(array) => index_char_array(array, &resolved),
584        Value::ComplexTensor(tensor) => index_complex_tensor(tensor, &resolved),
585        Value::Tensor(_)
586        | Value::StringArray(_)
587        | Value::Cell(_)
588        | Value::Num(_)
589        | Value::Int(_) => perform_indexing(&value, &resolved_f64)
590            .await
591            .map_err(|err| remap_getfield_flow(err, Some("getfield: "))),
592        Value::Bool(_) => {
593            if resolved.len() == 1 && resolved[0] == 1 {
594                Ok(value)
595            } else {
596                Err(getfield_error(&GETFIELD_ERROR_INDEX_OUT_OF_BOUNDS))
597            }
598        }
599        _ => Err(getfield_error(&GETFIELD_ERROR_NON_STRUCT_REFERENCE)),
600    }
601}
602
603fn apply_vector_index(value: &Value, selector: &IndexSelector) -> BuiltinResult<Option<Value>> {
604    let [IndexComponent::Vector(indices, shape)] = selector.components.as_slice() else {
605        return Ok(None);
606    };
607    match value {
608        Value::Tensor(tensor) => {
609            let mut data = Vec::with_capacity(indices.len());
610            for &index in indices {
611                if index < 1 || index > tensor.data.len() {
612                    return Err(getfield_error(&GETFIELD_ERROR_INDEX_OUT_OF_BOUNDS));
613                }
614                data.push(tensor.data[index - 1]);
615            }
616            let tensor = Tensor::new(data, shape.clone())
617                .map_err(|e| getfield_flow(format!("getfield: {e}")))?;
618            Ok(Some(Value::Tensor(tensor)))
619        }
620        _ => Ok(None),
621    }
622}
623
624fn resolve_indices(value: &Value, selector: &IndexSelector) -> BuiltinResult<Vec<usize>> {
625    let dims = selector.components.len();
626    let mut resolved = Vec::with_capacity(dims);
627    for (dim_idx, component) in selector.components.iter().enumerate() {
628        let index = match component {
629            IndexComponent::Scalar(idx) => *idx,
630            IndexComponent::Vector(_, _) => {
631                return Err(getfield_error_with_message(
632                    "getfield: vector indices are only supported for one-dimensional indexing",
633                    &GETFIELD_ERROR_INDEX_SHAPE,
634                ))
635            }
636            IndexComponent::End => dimension_length(value, dims, dim_idx)?,
637        };
638        resolved.push(index);
639    }
640    Ok(resolved)
641}
642
643fn dimension_length(value: &Value, dims: usize, dim_idx: usize) -> BuiltinResult<usize> {
644    match value {
645        Value::Tensor(tensor) => tensor_dimension_length(tensor, dims, dim_idx),
646        Value::Cell(cell) => cell_dimension_length(cell, dims, dim_idx),
647        Value::StringArray(sa) => string_array_dimension_length(sa, dims, dim_idx),
648        Value::LogicalArray(logical) => logical_array_dimension_length(logical, dims, dim_idx),
649        Value::CharArray(array) => char_array_dimension_length(array, dims, dim_idx),
650        Value::ComplexTensor(tensor) => complex_tensor_dimension_length(tensor, dims, dim_idx),
651        Value::Num(_) | Value::Int(_) | Value::Bool(_) => {
652            if dims == 1 {
653                Ok(1)
654            } else {
655                Err(getfield_error_with_message(
656                    "getfield: indexing with more than one dimension is not supported for scalars",
657                    &GETFIELD_ERROR_INDEX_SHAPE,
658                ))
659            }
660        }
661        _ => Err(getfield_error(&GETFIELD_ERROR_NON_STRUCT_REFERENCE)),
662    }
663}
664
665fn tensor_dimension_length(tensor: &Tensor, dims: usize, dim_idx: usize) -> BuiltinResult<usize> {
666    if dims == 1 {
667        let total = tensor.data.len();
668        if total == 0 {
669            return Err(getfield_error_with_message(
670                "Index exceeds the number of array elements (0).",
671                &GETFIELD_ERROR_INDEX_OUT_OF_BOUNDS,
672            ));
673        }
674        return Ok(total);
675    }
676    if dims > 2 {
677        return Err(getfield_error_with_message(
678            "getfield: indexing with more than two indices is not supported yet",
679            &GETFIELD_ERROR_INDEX_SHAPE,
680        ));
681    }
682    let len = if dim_idx == 0 {
683        tensor.rows()
684    } else {
685        tensor.cols()
686    };
687    if len == 0 {
688        return Err(getfield_error_with_message(
689            "Index exceeds the number of array elements (0).",
690            &GETFIELD_ERROR_INDEX_OUT_OF_BOUNDS,
691        ));
692    }
693    Ok(len)
694}
695
696fn cell_dimension_length(cell: &CellArray, dims: usize, dim_idx: usize) -> BuiltinResult<usize> {
697    if dims == 1 {
698        let total = cell.data.len();
699        if total == 0 {
700            return Err(getfield_error_with_message(
701                "Index exceeds the number of array elements (0).",
702                &GETFIELD_ERROR_INDEX_OUT_OF_BOUNDS,
703            ));
704        }
705        return Ok(total);
706    }
707    if dims > 2 {
708        return Err(getfield_error_with_message(
709            "getfield: indexing with more than two indices is not supported yet",
710            &GETFIELD_ERROR_INDEX_SHAPE,
711        ));
712    }
713    let len = if dim_idx == 0 { cell.rows } else { cell.cols };
714    if len == 0 {
715        return Err(getfield_error_with_message(
716            "Index exceeds the number of array elements (0).",
717            &GETFIELD_ERROR_INDEX_OUT_OF_BOUNDS,
718        ));
719    }
720    Ok(len)
721}
722
723fn string_array_dimension_length(
724    array: &runmat_builtins::StringArray,
725    dims: usize,
726    dim_idx: usize,
727) -> BuiltinResult<usize> {
728    if dims == 1 {
729        let total = array.data.len();
730        if total == 0 {
731            return Err(getfield_error_with_message(
732                "Index exceeds the number of array elements (0).",
733                &GETFIELD_ERROR_INDEX_OUT_OF_BOUNDS,
734            ));
735        }
736        return Ok(total);
737    }
738    if dims > 2 {
739        return Err(getfield_error_with_message(
740            "getfield: indexing with more than two indices is not supported yet",
741            &GETFIELD_ERROR_INDEX_SHAPE,
742        ));
743    }
744    let len = if dim_idx == 0 {
745        array.rows()
746    } else {
747        array.cols()
748    };
749    if len == 0 {
750        return Err(getfield_error_with_message(
751            "Index exceeds the number of array elements (0).",
752            &GETFIELD_ERROR_INDEX_OUT_OF_BOUNDS,
753        ));
754    }
755    Ok(len)
756}
757
758fn logical_array_dimension_length(
759    logical: &LogicalArray,
760    dims: usize,
761    dim_idx: usize,
762) -> BuiltinResult<usize> {
763    if dims == 1 {
764        let total = logical.data.len();
765        if total == 0 {
766            return Err(getfield_error_with_message(
767                "Index exceeds the number of array elements (0).",
768                &GETFIELD_ERROR_INDEX_OUT_OF_BOUNDS,
769            ));
770        }
771        return Ok(total);
772    }
773    if dims > 2 {
774        return Err(getfield_error_with_message(
775            "getfield: indexing with more than two indices is not supported yet",
776            &GETFIELD_ERROR_INDEX_SHAPE,
777        ));
778    }
779    let len = if dim_idx == 0 {
780        logical.shape.first().copied().unwrap_or(logical.data.len())
781    } else {
782        logical.shape.get(1).copied().unwrap_or(1)
783    };
784    if len == 0 {
785        return Err(getfield_error_with_message(
786            "Index exceeds the number of array elements (0).",
787            &GETFIELD_ERROR_INDEX_OUT_OF_BOUNDS,
788        ));
789    }
790    Ok(len)
791}
792
793fn char_array_dimension_length(
794    array: &CharArray,
795    dims: usize,
796    dim_idx: usize,
797) -> BuiltinResult<usize> {
798    if dims == 1 {
799        let total = array.rows * array.cols;
800        if total == 0 {
801            return Err(getfield_error_with_message(
802                "Index exceeds the number of array elements (0).",
803                &GETFIELD_ERROR_INDEX_OUT_OF_BOUNDS,
804            ));
805        }
806        return Ok(total);
807    }
808    if dims > 2 {
809        return Err(getfield_error_with_message(
810            "getfield: indexing with more than two indices is not supported yet",
811            &GETFIELD_ERROR_INDEX_SHAPE,
812        ));
813    }
814    let len = if dim_idx == 0 { array.rows } else { array.cols };
815    if len == 0 {
816        return Err(getfield_error_with_message(
817            "Index exceeds the number of array elements (0).",
818            &GETFIELD_ERROR_INDEX_OUT_OF_BOUNDS,
819        ));
820    }
821    Ok(len)
822}
823
824fn complex_tensor_dimension_length(
825    tensor: &ComplexTensor,
826    dims: usize,
827    dim_idx: usize,
828) -> BuiltinResult<usize> {
829    if dims == 1 {
830        let total = tensor.data.len();
831        if total == 0 {
832            return Err(getfield_error_with_message(
833                "Index exceeds the number of array elements (0).",
834                &GETFIELD_ERROR_INDEX_OUT_OF_BOUNDS,
835            ));
836        }
837        return Ok(total);
838    }
839    if dims > 2 {
840        return Err(getfield_error_with_message(
841            "getfield: indexing with more than two indices is not supported yet",
842            &GETFIELD_ERROR_INDEX_SHAPE,
843        ));
844    }
845    let len = if dim_idx == 0 {
846        tensor.rows
847    } else {
848        tensor.cols
849    };
850    if len == 0 {
851        return Err(getfield_error_with_message(
852            "Index exceeds the number of array elements (0).",
853            &GETFIELD_ERROR_INDEX_OUT_OF_BOUNDS,
854        ));
855    }
856    Ok(len)
857}
858
859fn index_char_array(array: &CharArray, indices: &[usize]) -> BuiltinResult<Value> {
860    if indices.is_empty() {
861        return Err(getfield_error_with_message(
862            "getfield: at least one index is required for char arrays",
863            &GETFIELD_ERROR_INDEX_INVALID,
864        ));
865    }
866    if indices.len() == 1 {
867        let total = array.rows * array.cols;
868        let idx = indices[0];
869        if idx == 0 || idx > total {
870            return Err(getfield_error(&GETFIELD_ERROR_INDEX_OUT_OF_BOUNDS));
871        }
872        let linear = idx - 1;
873        let rows = array.rows.max(1);
874        let col = linear / rows;
875        let row = linear % rows;
876        let pos = row * array.cols + col;
877        let ch = array
878            .data
879            .get(pos)
880            .copied()
881            .ok_or_else(|| getfield_error(&GETFIELD_ERROR_INDEX_OUT_OF_BOUNDS))?;
882        let out =
883            CharArray::new(vec![ch], 1, 1).map_err(|e| getfield_flow(format!("getfield: {e}")))?;
884        return Ok(Value::CharArray(out));
885    }
886    if indices.len() == 2 {
887        let row = indices[0];
888        let col = indices[1];
889        if row == 0 || row > array.rows || col == 0 || col > array.cols {
890            return Err(getfield_error(&GETFIELD_ERROR_INDEX_OUT_OF_BOUNDS));
891        }
892        let pos = (row - 1) * array.cols + (col - 1);
893        let ch = array
894            .data
895            .get(pos)
896            .copied()
897            .ok_or_else(|| getfield_error(&GETFIELD_ERROR_INDEX_OUT_OF_BOUNDS))?;
898        let out =
899            CharArray::new(vec![ch], 1, 1).map_err(|e| getfield_flow(format!("getfield: {e}")))?;
900        return Ok(Value::CharArray(out));
901    }
902    Err(getfield_error_with_message(
903        "getfield: indexing with more than two indices is not supported for char arrays",
904        &GETFIELD_ERROR_INDEX_SHAPE,
905    ))
906}
907
908fn index_complex_tensor(tensor: &ComplexTensor, indices: &[usize]) -> BuiltinResult<Value> {
909    if indices.is_empty() {
910        return Err(getfield_error_with_message(
911            "getfield: at least one index is required for complex tensors",
912            &GETFIELD_ERROR_INDEX_INVALID,
913        ));
914    }
915    if indices.len() == 1 {
916        let total = tensor.data.len();
917        let idx = indices[0];
918        if idx == 0 || idx > total {
919            return Err(getfield_error(&GETFIELD_ERROR_INDEX_OUT_OF_BOUNDS));
920        }
921        let (re, im) = tensor.data[idx - 1];
922        return Ok(Value::Complex(re, im));
923    }
924    if indices.len() == 2 {
925        let row = indices[0];
926        let col = indices[1];
927        if row == 0 || row > tensor.rows || col == 0 || col > tensor.cols {
928            return Err(getfield_error(&GETFIELD_ERROR_INDEX_OUT_OF_BOUNDS));
929        }
930        let pos = (row - 1) + (col - 1) * tensor.rows;
931        let (re, im) = tensor
932            .data
933            .get(pos)
934            .copied()
935            .ok_or_else(|| getfield_error(&GETFIELD_ERROR_INDEX_OUT_OF_BOUNDS))?;
936        return Ok(Value::Complex(re, im));
937    }
938    Err(getfield_error_with_message(
939        "getfield: indexing with more than two indices is not supported for complex tensors",
940        &GETFIELD_ERROR_INDEX_SHAPE,
941    ))
942}
943
944#[async_recursion::async_recursion(?Send)]
945async fn get_field_value(value: Value, name: &str) -> BuiltinResult<Value> {
946    match value {
947        Value::Struct(st) => get_struct_field(&st, name),
948        Value::Object(obj) => get_object_field(&obj, name).await,
949        Value::HandleObject(handle) => get_handle_field(&handle, name).await,
950        Value::Listener(listener) => get_listener_field(&listener, name),
951        Value::MException(ex) => get_exception_field(&ex, name),
952        Value::Cell(cell) if is_struct_array(&cell) => {
953            if cell.data.is_empty() {
954                return Err(getfield_error_with_message(
955                    "Struct contents reference from an empty struct array.",
956                    &GETFIELD_ERROR_NON_STRUCT_REFERENCE,
957                ));
958            }
959            // Default to first element when no index is specified
960            let first_entry = &cell.data[0];
961            match first_entry {
962                Value::Struct(st) => get_struct_field(st, name),
963                _ => Err(getfield_error(&GETFIELD_ERROR_NON_STRUCT_REFERENCE)),
964            }
965        }
966        _ => Err(getfield_error(&GETFIELD_ERROR_NON_STRUCT_REFERENCE)),
967    }
968}
969
970fn get_struct_field(struct_value: &StructValue, name: &str) -> BuiltinResult<Value> {
971    struct_value.fields.get(name).cloned().ok_or_else(|| {
972        getfield_error_with_message(
973            format!("Reference to non-existent field '{}'.", name),
974            &GETFIELD_ERROR_MISSING_FIELD,
975        )
976    })
977}
978
979async fn get_object_field(obj: &ObjectInstance, name: &str) -> BuiltinResult<Value> {
980    if let Some((prop, _owner)) = runmat_builtins::lookup_property(&obj.class_name, name) {
981        if prop.is_static {
982            return Err(getfield_error_with_message(
983                format!(
984                    "You cannot access the static property '{}' through an instance of class '{}'.",
985                    name, obj.class_name
986                ),
987                &GETFIELD_ERROR_OBJECT_PROPERTY,
988            ));
989        }
990        if prop.get_access == Access::Private {
991            return Err(getfield_private_access(format!(
992                "You cannot get the '{}' property of '{}' class.",
993                name, obj.class_name
994            )));
995        }
996        if prop.is_dependent {
997            let getter = object_property_getter_name(name);
998            match call_builtin_async(&getter, &[Value::Object(obj.clone())]).await {
999                Ok(value) => return Ok(value),
1000                Err(err) => {
1001                    if !is_undefined_function(&err) {
1002                        return Err(remap_getfield_flow(err, None));
1003                    }
1004                }
1005            }
1006            if let Some(val) = obj.properties.get(&format!("{name}_backing")) {
1007                return Ok(val.clone());
1008            }
1009        }
1010    }
1011
1012    if let Some(value) = dynamicprops::dynamic_property_read(obj, name)? {
1013        return Ok(value);
1014    }
1015
1016    if let Some(value) = obj.properties.get(name) {
1017        return Ok(value.clone());
1018    }
1019
1020    if let Some((prop, _owner)) = runmat_builtins::lookup_property(&obj.class_name, name) {
1021        if prop.get_access == Access::Private {
1022            return Err(getfield_private_access(format!(
1023                "You cannot get the '{}' property of '{}' class.",
1024                name, obj.class_name
1025            )));
1026        }
1027        return Err(getfield_error_with_message(
1028            format!(
1029                "No public property '{}' for class '{}'.",
1030                name, obj.class_name
1031            ),
1032            &GETFIELD_ERROR_OBJECT_PROPERTY,
1033        ));
1034    }
1035
1036    Err(getfield_error_with_message(
1037        format!("Undefined property '{}' for class {}", name, obj.class_name),
1038        &GETFIELD_ERROR_OBJECT_PROPERTY,
1039    ))
1040}
1041
1042#[async_recursion::async_recursion(?Send)]
1043async fn get_handle_field(handle: &HandleRef, name: &str) -> BuiltinResult<Value> {
1044    if !crate::is_handle_valid(handle) {
1045        return Err(getfield_error_with_message(
1046            format!("Invalid or deleted handle object '{}'.", handle.class_name),
1047            &GETFIELD_ERROR_INVALID_HANDLE,
1048        ));
1049    }
1050    let target = runmat_gc::gc_clone_value(&handle.target).map_err(|e| {
1051        getfield_error_with_message(
1052            format!("getfield: invalid handle target: {e}"),
1053            &GETFIELD_ERROR_INVALID_HANDLE,
1054        )
1055    })?;
1056    get_field_value(target, name).await
1057}
1058
1059fn get_listener_field(listener: &Listener, name: &str) -> BuiltinResult<Value> {
1060    match name {
1061        "Enabled" | "enabled" => Ok(Value::Bool(listener.enabled)),
1062        "Valid" | "valid" => Ok(Value::Bool(listener.valid)),
1063        "EventName" | "event_name" => Ok(Value::String(listener.event_name.clone())),
1064        "Callback" | "callback" => {
1065            if !listener.valid {
1066                return Err(getfield_error_with_message(
1067                    "getfield: listener is invalid or deleted",
1068                    &GETFIELD_ERROR_INVALID_HANDLE,
1069                ));
1070            }
1071            let value = runmat_gc::gc_clone_value(&listener.callback).map_err(|e| {
1072                getfield_error_with_message(
1073                    format!("getfield: invalid listener callback: {e}"),
1074                    &GETFIELD_ERROR_INVALID_HANDLE,
1075                )
1076            })?;
1077            Ok(value)
1078        }
1079        "Target" | "target" => {
1080            if !listener.valid {
1081                return Err(getfield_error_with_message(
1082                    "getfield: listener is invalid or deleted",
1083                    &GETFIELD_ERROR_INVALID_HANDLE,
1084                ));
1085            }
1086            let value = runmat_gc::gc_clone_value(&listener.target).map_err(|e| {
1087                getfield_error_with_message(
1088                    format!("getfield: invalid listener target: {e}"),
1089                    &GETFIELD_ERROR_INVALID_HANDLE,
1090                )
1091            })?;
1092            Ok(value)
1093        }
1094        "Id" | "id" => Ok(Value::Int(runmat_builtins::IntValue::U64(listener.id))),
1095        other => Err(getfield_error_with_message(
1096            format!("getfield: unknown field '{}' on listener object", other),
1097            &GETFIELD_ERROR_MISSING_FIELD,
1098        )),
1099    }
1100}
1101
1102fn get_exception_field(exception: &MException, name: &str) -> BuiltinResult<Value> {
1103    match name {
1104        "message" => Ok(Value::String(exception.message.clone())),
1105        "identifier" => Ok(Value::String(exception.identifier.clone())),
1106        "stack" => exception_stack_to_value(&exception.stack),
1107        other => Err(getfield_error_with_message(
1108            format!("Reference to non-existent field '{}'.", other),
1109            &GETFIELD_ERROR_MISSING_FIELD,
1110        )),
1111    }
1112}
1113
1114fn exception_stack_to_value(stack: &[String]) -> BuiltinResult<Value> {
1115    if stack.is_empty() {
1116        return make_cell_with_shape(Vec::new(), vec![0, 1])
1117            .map_err(|e| getfield_flow(format!("getfield: {e}")));
1118    }
1119    let mut values = Vec::with_capacity(stack.len());
1120    for frame in stack {
1121        values.push(Value::String(frame.clone()));
1122    }
1123    make_cell_with_shape(values, vec![stack.len(), 1])
1124        .map_err(|e| getfield_flow(format!("getfield: {e}")))
1125}
1126
1127fn is_struct_array(cell: &CellArray) -> bool {
1128    cell.data
1129        .iter()
1130        .all(|handle| matches!(handle, Value::Struct(_)))
1131}
1132
1133#[cfg(test)]
1134pub(crate) mod tests {
1135    use super::*;
1136    use runmat_builtins::{
1137        Access, CellArray, CharArray, ClassDef, ComplexTensor, HandleRef, IntValue, Listener,
1138        MException, ObjectInstance, PropertyDef, StructValue,
1139    };
1140
1141    #[cfg(feature = "wgpu")]
1142    use runmat_accelerate::backend::wgpu::provider as wgpu_backend;
1143    #[cfg(feature = "wgpu")]
1144    use runmat_accelerate_api::HostTensorView;
1145
1146    fn error_message(err: crate::RuntimeError) -> String {
1147        err.message().to_string()
1148    }
1149
1150    fn run_getfield(base: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
1151        futures::executor::block_on(getfield_builtin(base, rest))
1152    }
1153
1154    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1155    #[test]
1156    fn getfield_scalar_struct() {
1157        let mut st = StructValue::new();
1158        st.fields.insert("answer".to_string(), Value::Num(42.0));
1159        let value = run_getfield(Value::Struct(st), vec![Value::from("answer")]).expect("getfield");
1160        assert_eq!(value, Value::Num(42.0));
1161    }
1162
1163    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1164    #[test]
1165    fn getfield_nested_structs() {
1166        let mut inner = StructValue::new();
1167        inner.fields.insert("depth".to_string(), Value::Num(3.0));
1168        let mut outer = StructValue::new();
1169        outer
1170            .fields
1171            .insert("inner".to_string(), Value::Struct(inner));
1172        let result = run_getfield(
1173            Value::Struct(outer),
1174            vec![Value::from("inner"), Value::from("depth")],
1175        )
1176        .expect("nested getfield");
1177        assert_eq!(result, Value::Num(3.0));
1178    }
1179
1180    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1181    #[test]
1182    fn getfield_struct_array_element() {
1183        let mut first = StructValue::new();
1184        first.fields.insert("name".to_string(), Value::from("Ada"));
1185        let mut second = StructValue::new();
1186        second
1187            .fields
1188            .insert("name".to_string(), Value::from("Grace"));
1189        let array = CellArray::new_with_shape(
1190            vec![Value::Struct(first), Value::Struct(second)],
1191            vec![1, 2],
1192        )
1193        .unwrap();
1194        let index =
1195            CellArray::new_with_shape(vec![Value::Int(IntValue::I32(2))], vec![1, 1]).unwrap();
1196        let result = run_getfield(
1197            Value::Cell(array),
1198            vec![Value::Cell(index), Value::from("name")],
1199        )
1200        .expect("struct array element");
1201        assert_eq!(result, Value::from("Grace"));
1202    }
1203
1204    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1205    #[test]
1206    fn getfield_object_property() {
1207        let mut obj = ObjectInstance::new("TestClass".to_string());
1208        obj.properties.insert("value".to_string(), Value::Num(7.0));
1209        let result = run_getfield(Value::Object(obj), vec![Value::from("value")]).expect("object");
1210        assert_eq!(result, Value::Num(7.0));
1211    }
1212
1213    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1214    #[test]
1215    fn getfield_missing_field_errors() {
1216        let st = StructValue::new();
1217        let err = error_message(
1218            run_getfield(Value::Struct(st), vec![Value::from("missing")]).unwrap_err(),
1219        );
1220        assert!(err.contains("Reference to non-existent field 'missing'"));
1221    }
1222
1223    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1224    #[test]
1225    fn getfield_exception_fields() {
1226        let ex = MException::new("RunMat:Test".to_string(), "failure".to_string());
1227        let msg = run_getfield(Value::MException(ex.clone()), vec![Value::from("message")])
1228            .expect("message");
1229        assert_eq!(msg, Value::String("failure".to_string()));
1230        let ident = run_getfield(Value::MException(ex), vec![Value::from("identifier")])
1231            .expect("identifier");
1232        assert_eq!(ident, Value::String("RunMat:Test".to_string()));
1233    }
1234
1235    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1236    #[test]
1237    fn getfield_exception_stack_cell() {
1238        let mut ex = MException::new("RunMat:Test".to_string(), "failure".to_string());
1239        ex.stack.push("demo.m:5".to_string());
1240        ex.stack.push("main.m:1".to_string());
1241        let stack = run_getfield(Value::MException(ex), vec![Value::from("stack")]).expect("stack");
1242        let Value::Cell(cell) = stack else {
1243            panic!("expected cell array");
1244        };
1245        assert_eq!(cell.rows, 2);
1246        assert_eq!(cell.cols, 1);
1247        let first = cell.data[0].clone();
1248        assert_eq!(first, Value::String("demo.m:5".to_string()));
1249    }
1250
1251    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1252    #[test]
1253    fn indexing_missing_field_name_fails() {
1254        let mut outer = StructValue::new();
1255        outer.fields.insert("inner".to_string(), Value::Num(1.0));
1256        let index =
1257            CellArray::new_with_shape(vec![Value::Int(IntValue::I32(1))], vec![1, 1]).unwrap();
1258        let err = error_message(
1259            run_getfield(Value::Struct(outer), vec![Value::Cell(index)]).unwrap_err(),
1260        );
1261        assert!(err.contains("expected field name"));
1262    }
1263
1264    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1265    #[test]
1266    fn getfield_supports_end_index() {
1267        let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
1268        let mut st = StructValue::new();
1269        st.fields
1270            .insert("values".to_string(), Value::Tensor(tensor));
1271        let idx_cell =
1272            CellArray::new(vec![Value::CharArray(CharArray::new_row("end"))], 1, 1).unwrap();
1273        let result = run_getfield(
1274            Value::Struct(st),
1275            vec![Value::from("values"), Value::Cell(idx_cell)],
1276        )
1277        .expect("end index");
1278        assert_eq!(result, Value::Num(3.0));
1279    }
1280
1281    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1282    #[test]
1283    fn getfield_struct_array_defaults_to_first() {
1284        let mut first = StructValue::new();
1285        first.fields.insert("name".to_string(), Value::from("Ada"));
1286        let mut second = StructValue::new();
1287        second
1288            .fields
1289            .insert("name".to_string(), Value::from("Grace"));
1290        let array = CellArray::new_with_shape(
1291            vec![Value::Struct(first), Value::Struct(second)],
1292            vec![1, 2],
1293        )
1294        .unwrap();
1295        let result =
1296            run_getfield(Value::Cell(array), vec![Value::from("name")]).expect("default index");
1297        assert_eq!(result, Value::from("Ada"));
1298    }
1299
1300    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1301    #[test]
1302    fn getfield_char_array_single_element() {
1303        let chars = CharArray::new_row("Ada");
1304        let mut st = StructValue::new();
1305        st.fields
1306            .insert("name".to_string(), Value::CharArray(chars));
1307        let index =
1308            CellArray::new_with_shape(vec![Value::Int(IntValue::I32(2))], vec![1, 1]).unwrap();
1309        let result = run_getfield(
1310            Value::Struct(st),
1311            vec![Value::from("name"), Value::Cell(index)],
1312        )
1313        .expect("char indexing");
1314        match result {
1315            Value::CharArray(ca) => {
1316                assert_eq!(ca.rows, 1);
1317                assert_eq!(ca.cols, 1);
1318                assert_eq!(ca.data, vec!['d']);
1319            }
1320            other => panic!("expected 1x1 CharArray, got {other:?}"),
1321        }
1322    }
1323
1324    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1325    #[test]
1326    fn getfield_complex_tensor_index() {
1327        let tensor =
1328            ComplexTensor::new(vec![(1.0, 2.0), (3.0, 4.0)], vec![2, 1]).expect("complex tensor");
1329        let mut st = StructValue::new();
1330        st.fields
1331            .insert("vals".to_string(), Value::ComplexTensor(tensor));
1332        let index =
1333            CellArray::new_with_shape(vec![Value::Int(IntValue::I32(2))], vec![1, 1]).unwrap();
1334        let result = run_getfield(
1335            Value::Struct(st),
1336            vec![Value::from("vals"), Value::Cell(index)],
1337        )
1338        .expect("complex index");
1339        assert_eq!(result, Value::Complex(3.0, 4.0));
1340    }
1341
1342    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1343    #[test]
1344    fn getfield_dependent_property_invokes_getter() {
1345        let class_name = "runmat.unittest.GetfieldDependent";
1346        let mut def = ClassDef {
1347            name: class_name.to_string(),
1348            parent: None,
1349            properties: std::collections::HashMap::new(),
1350            methods: std::collections::HashMap::new(),
1351        };
1352        def.properties.insert(
1353            "p".to_string(),
1354            PropertyDef {
1355                name: "p".to_string(),
1356                is_static: false,
1357                is_constant: false,
1358                is_dependent: true,
1359                get_access: Access::Public,
1360                set_access: Access::Public,
1361                default_value: None,
1362            },
1363        );
1364        runmat_builtins::register_class(def);
1365
1366        let mut obj = ObjectInstance::new(class_name.to_string());
1367        obj.properties
1368            .insert("p_backing".to_string(), Value::Num(42.0));
1369
1370        let result = run_getfield(Value::Object(obj), vec![Value::from("p")]).expect("dependent");
1371        assert_eq!(result, Value::Num(42.0));
1372    }
1373
1374    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1375    #[test]
1376    fn getfield_inherited_dependent_property_uses_parent_metadata() {
1377        let parent_name = "runmat.unittest.GetfieldDependentParent";
1378        let child_name = "runmat.unittest.GetfieldDependentChild";
1379
1380        let mut parent = ClassDef {
1381            name: parent_name.to_string(),
1382            parent: None,
1383            properties: std::collections::HashMap::new(),
1384            methods: std::collections::HashMap::new(),
1385        };
1386        parent.properties.insert(
1387            "p".to_string(),
1388            PropertyDef {
1389                name: "p".to_string(),
1390                is_static: false,
1391                is_constant: false,
1392                is_dependent: true,
1393                get_access: Access::Public,
1394                set_access: Access::Public,
1395                default_value: None,
1396            },
1397        );
1398        runmat_builtins::register_class(parent);
1399
1400        runmat_builtins::register_class(ClassDef {
1401            name: child_name.to_string(),
1402            parent: Some(parent_name.to_string()),
1403            properties: std::collections::HashMap::new(),
1404            methods: std::collections::HashMap::new(),
1405        });
1406
1407        let mut obj = ObjectInstance::new(child_name.to_string());
1408        obj.properties
1409            .insert("p_backing".to_string(), Value::Num(17.0));
1410
1411        let result =
1412            run_getfield(Value::Object(obj), vec![Value::from("p")]).expect("inherited dependent");
1413        assert_eq!(result, Value::Num(17.0));
1414    }
1415
1416    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1417    #[test]
1418    fn getfield_invalid_handle_errors() {
1419        let target = runmat_gc::gc_allocate(Value::Num(1.0)).expect("gc allocate target");
1420        let handle = HandleRef {
1421            class_name: "Demo".to_string(),
1422            target,
1423            valid: false,
1424        };
1425        let err = error_message(
1426            run_getfield(Value::HandleObject(handle), vec![Value::from("x")]).unwrap_err(),
1427        );
1428        assert!(err.contains("Invalid or deleted handle object 'Demo'"));
1429    }
1430
1431    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1432    #[test]
1433    fn getfield_listener_fields_resolved() {
1434        let target = runmat_gc::gc_allocate(Value::Num(7.0)).expect("gc allocate target");
1435        let callback = runmat_gc::gc_allocate(Value::FunctionHandle("cb".to_string()))
1436            .expect("gc allocate callback");
1437        let listener = Listener {
1438            id: 9,
1439            target,
1440            target_class_name: "EventTarget".to_string(),
1441            event_name: "tick".to_string(),
1442            callback,
1443            enabled: true,
1444            valid: true,
1445        };
1446        let enabled = run_getfield(
1447            Value::Listener(listener.clone()),
1448            vec![Value::from("Enabled")],
1449        )
1450        .expect("enabled");
1451        assert_eq!(enabled, Value::Bool(true));
1452        let event_name = run_getfield(
1453            Value::Listener(listener.clone()),
1454            vec![Value::from("EventName")],
1455        )
1456        .expect("event name");
1457        assert_eq!(event_name, Value::String("tick".to_string()));
1458        let callback = run_getfield(Value::Listener(listener), vec![Value::from("Callback")])
1459            .expect("callback");
1460        assert!(matches!(callback, Value::FunctionHandle(_)));
1461    }
1462
1463    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1464    #[test]
1465    fn getfield_invalid_listener_rejects_rooted_fields() {
1466        let target = runmat_gc::gc_allocate(Value::Num(7.0)).expect("gc allocate target");
1467        let callback = runmat_gc::gc_allocate(Value::FunctionHandle("cb".to_string()))
1468            .expect("gc allocate callback");
1469        let listener = Listener {
1470            id: 10,
1471            target,
1472            target_class_name: "EventTarget".to_string(),
1473            event_name: "tick".to_string(),
1474            callback,
1475            enabled: false,
1476            valid: false,
1477        };
1478
1479        let err = error_message(
1480            run_getfield(
1481                Value::Listener(listener.clone()),
1482                vec![Value::from("Callback")],
1483            )
1484            .unwrap_err(),
1485        );
1486        assert!(err.contains("listener is invalid or deleted"));
1487
1488        let err = error_message(
1489            run_getfield(Value::Listener(listener), vec![Value::from("Target")]).unwrap_err(),
1490        );
1491        assert!(err.contains("listener is invalid or deleted"));
1492    }
1493
1494    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1495    #[test]
1496    #[cfg(feature = "wgpu")]
1497    fn getfield_gpu_tensor_indexing() {
1498        let _ = wgpu_backend::register_wgpu_provider(wgpu_backend::WgpuProviderOptions::default());
1499        let provider = runmat_accelerate_api::provider().expect("wgpu provider");
1500
1501        let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
1502        let view = HostTensorView {
1503            data: &tensor.data,
1504            shape: &tensor.shape,
1505        };
1506        let handle = provider.upload(&view).expect("upload");
1507
1508        let mut st = StructValue::new();
1509        st.fields
1510            .insert("values".to_string(), Value::GpuTensor(handle.clone()));
1511
1512        let direct = run_getfield(Value::Struct(st.clone()), vec![Value::from("values")])
1513            .expect("direct gpu field");
1514        match direct {
1515            Value::GpuTensor(out) => assert_eq!(out.buffer_id, handle.buffer_id),
1516            other => panic!("expected gpu tensor, got {other:?}"),
1517        }
1518
1519        let idx_cell =
1520            CellArray::new(vec![Value::CharArray(CharArray::new_row("end"))], 1, 1).unwrap();
1521        let indexed = run_getfield(
1522            Value::Struct(st),
1523            vec![Value::from("values"), Value::Cell(idx_cell)],
1524        )
1525        .expect("gpu indexed field");
1526        match indexed {
1527            Value::Num(v) => assert_eq!(v, 3.0),
1528            other => panic!("expected numeric scalar, got {other:?}"),
1529        }
1530    }
1531
1532    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1533    #[test]
1534    fn getfield_undefined_detection_requires_identifier() {
1535        let with_identifier = build_runtime_error("missing")
1536            .with_identifier(crate::IDENT_UNDEFINED_FUNCTION)
1537            .build();
1538        assert!(is_undefined_function(&with_identifier));
1539
1540        let message_only =
1541            build_runtime_error(format!("{} message only", crate::IDENT_UNDEFINED_FUNCTION))
1542                .build();
1543        assert!(
1544            !is_undefined_function(&message_only),
1545            "message-only undefined markers should not trigger getter fallback"
1546        );
1547    }
1548}