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