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