1use crate::builtins::common::spec::{
9 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
10 ReductionNaN, ResidencyPolicy, ShapeRequirements,
11};
12use crate::call_builtin;
13#[cfg(feature = "doc_export")]
14use crate::register_builtin_doc_text;
15use crate::{gather_if_needed, register_builtin_fusion_spec, register_builtin_gpu_spec};
16use runmat_builtins::{
17 Access, CellArray, CharArray, ComplexTensor, HandleRef, LogicalArray, ObjectInstance,
18 StructValue, Tensor, Value,
19};
20use runmat_gc_api::GcPtr;
21use runmat_macros::runtime_builtin;
22use std::convert::TryFrom;
23
24#[cfg(feature = "doc_export")]
25pub const DOC_MD: &str = r#"---
26title: "setfield"
27category: "structs/core"
28keywords: ["setfield", "struct", "assignment", "struct array", "object property"]
29summary: "Assign into struct fields, struct arrays, or MATLAB-style object properties."
30references: []
31gpu_support:
32 elementwise: false
33 reduction: false
34 precisions: []
35 broadcasting: "none"
36 notes: "Assignments run on the host. GPU tensors or handles embedded in structs are gathered to host memory before mutation."
37fusion:
38 elementwise: false
39 reduction: false
40 max_inputs: 2
41 constants: "inline"
42requires_feature: null
43tested:
44 unit: "builtins::structs::core::setfield::tests"
45 integration: "runmat_ignition::tests::functions::member_get_set_and_method_call_skeleton"
46---
47
48# What does the `setfield` function do in MATLAB / RunMat?
49`S = setfield(S, field, value)` returns a copy of the struct (or object) with `field`
50assigned to `value`. Additional field names and index cells let you update nested
51structures, struct arrays, and array elements contained within fields.
52
53## How does the `setfield` function behave in MATLAB / RunMat?
54- Field names must be character vectors or string scalars. Provide as many field
55 names as needed; each additional name drills deeper into nested structs, so
56 `setfield(S,"outer","inner",value)` mirrors `S.outer.inner = value`.
57- Missing struct fields are created automatically. If intermediary structs do not
58 exist, RunMat allocates them so that the assignment completes successfully.
59- Struct arrays require a leading cell array of one-based indices, e.g.
60 `setfield(S,{2},"field",value)` or `setfield(S,{1,3},"field",value)`, and accept
61 the keyword `end`.
62- You can index into a field's contents before traversing deeper by placing a cell
63 array of indices immediately after the field name:
64 `setfield(S,"values",{1,2},"leaf",x)` matches `S.values{1,2}.leaf = x`.
65- MATLAB-style objects honour property metadata: private setters raise access
66 errors, static properties cannot be written through instances, and dependent
67 properties forward to `set.<name>` methods when available.
68- The function returns the updated struct or object. For value types the result is a
69 new copy; handle objects still point at the same instance, and the handle is
70 returned for chaining.
71
72## `setfield` Function GPU Execution Behaviour
73`setfield` executes entirely on the host. When fields contain GPU-resident tensors,
74RunMat gathers those tensors to host memory before mutating them and stores the
75resulting host tensor back into the struct or object. No GPU kernels are launched
76for these assignments.
77
78## Examples of using the `setfield` function in MATLAB / RunMat
79
80### Assigning a new field in a scalar struct
81```matlab
82s = struct();
83s = setfield(s, "answer", 42);
84disp(s.answer);
85```
86
87Expected output:
88```matlab
89 42
90```
91
92### Creating nested structs automatically
93```matlab
94cfg = struct();
95cfg = setfield(cfg, "solver", "name", "cg");
96cfg = setfield(cfg, "solver", "tolerance", 1e-6);
97disp(cfg.solver.tolerance);
98```
99
100Expected output:
101```matlab
102 1.0000e-06
103```
104
105### Updating an element of a struct array
106```matlab
107people = struct("name", {"Ada", "Grace"}, "id", {101, 102});
108people = setfield(people, {2}, "id", 999);
109disp(people(2).id);
110```
111
112Expected output:
113```matlab
114 999
115```
116
117### Assigning through a field that contains a cell array
118```matlab
119data = struct("samples", {{struct("value", 1), struct("value", 2)}} );
120data = setfield(data, "samples", {2}, "value", 10);
121disp(data.samples{2}.value);
122```
123
124Expected output:
125```matlab
126 10
127```
128
129### Setting an object property that honours access attributes
130Save the following class definition as `Point.m`:
131```matlab
132classdef Point
133 properties
134 x double = 0;
135 end
136end
137```
138
139Then update the property from the command window:
140```matlab
141p = Point;
142p = setfield(p, "x", 3);
143disp(p.x);
144```
145
146Expected output:
147```matlab
148 3
149```
150
151## GPU residency in RunMat (Do I need `gpuArray`?)
152You do not have to move data explicitly when assigning into structs. If a field
153contains a GPU tensor, `setfield` gathers it to host memory so the mutation can be
154performed safely. Subsequent operations decide whether to migrate it back to the GPU.
155
156## FAQ
157
158### Does `setfield` modify the input in-place?
159No. Like MATLAB, it returns a new struct (or object) with the requested update. In
160Rust this entails cloning the source value and mutating the clone.
161
162### Can I create nested structs in a single call?
163Yes. Missing intermediate structs are created automatically when you provide multiple
164field names, e.g. `setfield(S,"outer","inner",value)` builds `outer` when needed.
165
166### How do I update a specific element of a struct array?
167Supply an index cell before the first field name: `setfield(S,{row,col},"field",value)`
168is the same as `S(row,col).field = value`.
169
170### Does `setfield` work with handle objects?
171Yes. Valid handle objects forward the assignment to the underlying instance. Deleted
172or invalid handles raise the standard MATLAB-style error.
173
174### Can I index into field contents before continuing?
175Yes. Place a cell array of indices immediately after the field name. Each set of
176indices uses MATLAB's one-based semantics and supports the keyword `end`.
177
178### Why are GPU tensors gathered to the host?
179Assignments require host-side mutation. Providers can re-upload the updated tensor on
180subsequent GPU-aware operations; `setfield` itself never launches kernels.
181
182## See Also
183[getfield](./getfield), [fieldnames](./fieldnames), [struct](./struct), [gpuArray](../../acceleration/gpu/gpuArray), [gather](../../acceleration/gpu/gather)
184"#;
185
186pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
187 name: "setfield",
188 op_kind: GpuOpKind::Custom("setfield"),
189 supported_precisions: &[],
190 broadcast: BroadcastSemantics::None,
191 provider_hooks: &[],
192 constant_strategy: ConstantStrategy::InlineLiteral,
193 residency: ResidencyPolicy::InheritInputs,
194 nan_mode: ReductionNaN::Include,
195 two_pass_threshold: None,
196 workgroup_size: None,
197 accepts_nan_mode: false,
198 notes: "Host-only metadata mutation; GPU tensors are gathered before assignment.",
199};
200
201register_builtin_gpu_spec!(GPU_SPEC);
202
203pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
204 name: "setfield",
205 shape: ShapeRequirements::Any,
206 constant_strategy: ConstantStrategy::InlineLiteral,
207 elementwise: None,
208 reduction: None,
209 emits_nan: false,
210 notes: "Assignments terminate fusion and gather device data back to the host.",
211};
212
213register_builtin_fusion_spec!(FUSION_SPEC);
214
215#[cfg(feature = "doc_export")]
216register_builtin_doc_text!("setfield", DOC_MD);
217
218#[runtime_builtin(
219 name = "setfield",
220 category = "structs/core",
221 summary = "Assign into struct fields, struct arrays, or MATLAB-style object properties.",
222 keywords = "setfield,struct,assignment,object property"
223)]
224fn setfield_builtin(base: Value, rest: Vec<Value>) -> Result<Value, String> {
225 let parsed = parse_arguments(rest)?;
226 let ParsedArguments {
227 leading_index,
228 steps,
229 value,
230 } = parsed;
231 assign_value(base, leading_index, steps, value)
232}
233
234struct ParsedArguments {
235 leading_index: Option<IndexSelector>,
236 steps: Vec<FieldStep>,
237 value: Value,
238}
239
240struct FieldStep {
241 name: String,
242 index: Option<IndexSelector>,
243}
244
245#[derive(Clone)]
246struct IndexSelector {
247 components: Vec<IndexComponent>,
248}
249
250#[derive(Clone)]
251enum IndexComponent {
252 Scalar(usize),
253 End,
254}
255
256fn parse_arguments(mut rest: Vec<Value>) -> Result<ParsedArguments, String> {
257 if rest.len() < 2 {
258 return Err("setfield: expected at least one field name and a value".to_string());
259 }
260
261 let value = rest
262 .pop()
263 .expect("rest contains at least two elements after early return");
264
265 let mut parsed = ParsedArguments {
266 leading_index: None,
267 steps: Vec::new(),
268 value,
269 };
270
271 if let Some(first) = rest.first() {
272 if is_index_selector(first) {
273 let selector = rest.remove(0);
274 parsed.leading_index = Some(parse_index_selector(selector)?);
275 }
276 }
277
278 if rest.is_empty() {
279 return Err("setfield: expected field name arguments".to_string());
280 }
281
282 let mut iter = rest.into_iter().peekable();
283 while let Some(arg) = iter.next() {
284 let name = parse_field_name(arg)?;
285 let mut step = FieldStep { name, index: None };
286 if let Some(next) = iter.peek() {
287 if is_index_selector(next) {
288 let selector = iter.next().unwrap();
289 step.index = Some(parse_index_selector(selector)?);
290 }
291 }
292 parsed.steps.push(step);
293 }
294
295 if parsed.steps.is_empty() {
296 return Err("setfield: expected field name arguments".to_string());
297 }
298
299 Ok(parsed)
300}
301
302fn assign_value(
303 base: Value,
304 leading_index: Option<IndexSelector>,
305 steps: Vec<FieldStep>,
306 rhs: Value,
307) -> Result<Value, String> {
308 if steps.is_empty() {
309 return Err("setfield: expected field name arguments".to_string());
310 }
311 if let Some(selector) = leading_index {
312 assign_with_leading_index(base, &selector, &steps, rhs)
313 } else {
314 assign_without_leading_index(base, &steps, rhs)
315 }
316}
317
318fn assign_with_leading_index(
319 base: Value,
320 selector: &IndexSelector,
321 steps: &[FieldStep],
322 rhs: Value,
323) -> Result<Value, String> {
324 match base {
325 Value::Cell(cell) => assign_into_struct_array(cell, selector, steps, rhs),
326 other => Err(format!(
327 "setfield: leading indices require a struct array, got {other:?}"
328 )),
329 }
330}
331
332fn assign_without_leading_index(
333 base: Value,
334 steps: &[FieldStep],
335 rhs: Value,
336) -> Result<Value, String> {
337 match base {
338 Value::Struct(struct_value) => assign_into_struct(struct_value, steps, rhs),
339 Value::Object(object) => assign_into_object(object, steps, rhs),
340 Value::Cell(cell) if is_struct_array(&cell) => {
341 if cell.data.is_empty() {
342 Err("setfield: struct array is empty; supply indices in a cell array".to_string())
343 } else {
344 let selector = IndexSelector {
345 components: vec![IndexComponent::Scalar(1)],
346 };
347 assign_into_struct_array(cell, &selector, steps, rhs)
348 }
349 }
350 Value::HandleObject(handle) => assign_into_handle(handle, steps, rhs),
351 Value::Listener(_) => {
352 Err("setfield: listeners do not support direct field assignment".to_string())
353 }
354 other => Err(format!(
355 "setfield unsupported on this value for field '{}': {other:?}",
356 steps.first().map(|s| s.name.as_str()).unwrap_or_default()
357 )),
358 }
359}
360
361fn assign_into_struct_array(
362 mut cell: CellArray,
363 selector: &IndexSelector,
364 steps: &[FieldStep],
365 rhs: Value,
366) -> Result<Value, String> {
367 if selector.components.is_empty() {
368 return Err("setfield: index cell must contain at least one element".to_string());
369 }
370
371 let resolved = resolve_indices(&Value::Cell(cell.clone()), selector)?;
372
373 let position = match resolved.len() {
374 1 => {
375 let idx = resolved[0];
376 if idx == 0 || idx > cell.data.len() {
377 return Err("Index exceeds the number of array elements.".to_string());
378 }
379 idx - 1
380 }
381 2 => {
382 let row = resolved[0];
383 let col = resolved[1];
384 if row == 0 || row > cell.rows || col == 0 || col > cell.cols {
385 return Err("Index exceeds the number of array elements.".to_string());
386 }
387 (row - 1) * cell.cols + (col - 1)
388 }
389 _ => {
390 return Err(
391 "setfield: indexing with more than two indices is not supported yet".to_string(),
392 );
393 }
394 };
395
396 let handle = cell
397 .data
398 .get(position)
399 .ok_or_else(|| "Index exceeds the number of array elements.".to_string())?
400 .clone();
401
402 let current = unsafe { &*handle.as_raw() }.clone();
403 let updated = assign_into_value(current, steps, rhs)?;
404 cell.data[position] = allocate_cell_handle(updated)?;
405 Ok(Value::Cell(cell))
406}
407
408fn assign_into_value(value: Value, steps: &[FieldStep], rhs: Value) -> Result<Value, String> {
409 if steps.is_empty() {
410 return Ok(rhs);
411 }
412 match value {
413 Value::Struct(struct_value) => assign_into_struct(struct_value, steps, rhs),
414 Value::Object(object) => assign_into_object(object, steps, rhs),
415 Value::Cell(cell) => assign_into_cell(cell, steps, rhs),
416 Value::HandleObject(handle) => assign_into_handle(handle, steps, rhs),
417 Value::Listener(_) => {
418 Err("setfield: listeners do not support nested field assignment".to_string())
419 }
420 other => Err(format!(
421 "Struct contents assignment to a {other:?} object is not supported."
422 )),
423 }
424}
425
426fn assign_into_struct(
427 mut struct_value: StructValue,
428 steps: &[FieldStep],
429 rhs: Value,
430) -> Result<Value, String> {
431 let (first, rest) = steps
432 .split_first()
433 .expect("steps is non-empty when assign_into_struct is called");
434
435 if rest.is_empty() {
436 if let Some(selector) = &first.index {
437 let current = struct_value
438 .fields
439 .get(&first.name)
440 .cloned()
441 .ok_or_else(|| format!("Reference to non-existent field '{}'.", first.name))?;
442 let updated = assign_with_selector(current, selector, &[], rhs)?;
443 struct_value.fields.insert(first.name.clone(), updated);
444 } else {
445 struct_value.fields.insert(first.name.clone(), rhs);
446 }
447 return Ok(Value::Struct(struct_value));
448 }
449
450 if let Some(selector) = &first.index {
451 let current = struct_value
452 .fields
453 .get(&first.name)
454 .cloned()
455 .ok_or_else(|| format!("Reference to non-existent field '{}'.", first.name))?;
456 let updated = assign_with_selector(current, selector, rest, rhs)?;
457 struct_value.fields.insert(first.name.clone(), updated);
458 return Ok(Value::Struct(struct_value));
459 }
460
461 let current = struct_value
462 .fields
463 .get(&first.name)
464 .cloned()
465 .unwrap_or_else(|| Value::Struct(StructValue::new()));
466 let updated = assign_into_value(current, rest, rhs)?;
467 struct_value.fields.insert(first.name.clone(), updated);
468 Ok(Value::Struct(struct_value))
469}
470
471fn assign_into_object(
472 mut object: ObjectInstance,
473 steps: &[FieldStep],
474 rhs: Value,
475) -> Result<Value, String> {
476 let (first, rest) = steps
477 .split_first()
478 .expect("steps is non-empty when assign_into_object is called");
479
480 if first.index.is_some() {
481 return Err(
482 "setfield: indexing into object properties is not currently supported".to_string(),
483 );
484 }
485
486 if rest.is_empty() {
487 write_object_property(&mut object, &first.name, rhs)?;
488 return Ok(Value::Object(object));
489 }
490
491 let current = read_object_property(&object, &first.name)?;
492 let updated = assign_into_value(current, rest, rhs)?;
493 write_object_property(&mut object, &first.name, updated)?;
494 Ok(Value::Object(object))
495}
496
497fn assign_into_cell(cell: CellArray, steps: &[FieldStep], rhs: Value) -> Result<Value, String> {
498 let (first, rest) = steps
499 .split_first()
500 .expect("steps is non-empty when assign_into_cell is called");
501
502 let selector = first.index.as_ref().ok_or_else(|| {
503 "setfield: cell array assignments require indices in a cell array".to_string()
504 })?;
505 if rest.is_empty() {
506 assign_with_selector(Value::Cell(cell), selector, &[], rhs)
507 } else {
508 assign_with_selector(Value::Cell(cell), selector, rest, rhs)
509 }
510}
511
512fn assign_with_selector(
513 value: Value,
514 selector: &IndexSelector,
515 rest: &[FieldStep],
516 rhs: Value,
517) -> Result<Value, String> {
518 let host_value = gather_if_needed(&value).map_err(|e| format!("setfield: {e}"))?;
519 match host_value {
520 Value::Cell(mut cell) => {
521 let resolved = resolve_indices(&Value::Cell(cell.clone()), selector)?;
522 let position = match resolved.len() {
523 1 => {
524 let idx = resolved[0];
525 if idx == 0 || idx > cell.data.len() {
526 return Err("Index exceeds the number of array elements.".to_string());
527 }
528 idx - 1
529 }
530 2 => {
531 let row = resolved[0];
532 let col = resolved[1];
533 if row == 0 || row > cell.rows || col == 0 || col > cell.cols {
534 return Err("Index exceeds the number of array elements.".to_string());
535 }
536 (row - 1) * cell.cols + (col - 1)
537 }
538 _ => {
539 return Err(
540 "setfield: indexing with more than two indices is not supported yet"
541 .to_string(),
542 );
543 }
544 };
545
546 let handle = cell
547 .data
548 .get(position)
549 .ok_or_else(|| "Index exceeds the number of array elements.".to_string())?
550 .clone();
551 let existing = unsafe { &*handle.as_raw() }.clone();
552 let new_value = if rest.is_empty() {
553 rhs
554 } else {
555 assign_into_value(existing, rest, rhs)?
556 };
557 cell.data[position] = allocate_cell_handle(new_value)?;
558 Ok(Value::Cell(cell))
559 }
560 Value::Tensor(mut tensor) => {
561 if !rest.is_empty() {
562 return Err(
563 "setfield: cannot traverse deeper fields after indexing into a numeric tensor"
564 .to_string(),
565 );
566 }
567 assign_tensor_element(&mut tensor, selector, rhs)?;
568 Ok(Value::Tensor(tensor))
569 }
570 Value::LogicalArray(mut logical) => {
571 if !rest.is_empty() {
572 return Err(
573 "setfield: cannot traverse deeper fields after indexing into a logical array"
574 .to_string(),
575 );
576 }
577 assign_logical_element(&mut logical, selector, rhs)?;
578 Ok(Value::LogicalArray(logical))
579 }
580 Value::StringArray(mut sa) => {
581 if !rest.is_empty() {
582 return Err(
583 "setfield: cannot traverse deeper fields after indexing into a string array"
584 .to_string(),
585 );
586 }
587 assign_string_array_element(&mut sa, selector, rhs)?;
588 Ok(Value::StringArray(sa))
589 }
590 Value::CharArray(mut ca) => {
591 if !rest.is_empty() {
592 return Err(
593 "setfield: cannot traverse deeper fields after indexing into a char array"
594 .to_string(),
595 );
596 }
597 assign_char_array_element(&mut ca, selector, rhs)?;
598 Ok(Value::CharArray(ca))
599 }
600 Value::ComplexTensor(mut tensor) => {
601 if !rest.is_empty() {
602 return Err(
603 "setfield: cannot traverse deeper fields after indexing into a complex tensor"
604 .to_string(),
605 );
606 }
607 assign_complex_tensor_element(&mut tensor, selector, rhs)?;
608 Ok(Value::ComplexTensor(tensor))
609 }
610 other => Err(format!(
611 "Struct contents assignment to a {other:?} object is not supported."
612 )),
613 }
614}
615
616fn assign_tensor_element(
617 tensor: &mut Tensor,
618 selector: &IndexSelector,
619 rhs: Value,
620) -> Result<(), String> {
621 let resolved = resolve_indices(&Value::Tensor(tensor.clone()), selector)?;
622 let value = value_to_scalar(rhs)?;
623 match resolved.len() {
624 1 => {
625 let idx = resolved[0];
626 if idx == 0 || idx > tensor.data.len() {
627 return Err("Index exceeds the number of array elements.".to_string());
628 }
629 tensor.data[idx - 1] = value;
630 Ok(())
631 }
632 2 => {
633 let row = resolved[0];
634 let col = resolved[1];
635 if row == 0 || row > tensor.rows() || col == 0 || col > tensor.cols() {
636 return Err("Index exceeds the number of array elements.".to_string());
637 }
638 let pos = (row - 1) + (col - 1) * tensor.rows();
639 tensor
640 .data
641 .get_mut(pos)
642 .map(|slot| *slot = value)
643 .ok_or_else(|| "Index exceeds the number of array elements.".to_string())
644 }
645 _ => Err("setfield: indexing with more than two indices is not supported yet".to_string()),
646 }
647}
648
649fn assign_logical_element(
650 logical: &mut LogicalArray,
651 selector: &IndexSelector,
652 rhs: Value,
653) -> Result<(), String> {
654 let resolved = resolve_indices(&Value::LogicalArray(logical.clone()), selector)?;
655 let value = value_to_bool(rhs)?;
656 match resolved.len() {
657 1 => {
658 let idx = resolved[0];
659 if idx == 0 || idx > logical.data.len() {
660 return Err("Index exceeds the number of array elements.".to_string());
661 }
662 logical.data[idx - 1] = if value { 1 } else { 0 };
663 Ok(())
664 }
665 2 => {
666 if logical.shape.len() < 2 {
667 return Err("Index exceeds the number of array elements.".to_string());
668 }
669 let row = resolved[0];
670 let col = resolved[1];
671 let rows = logical.shape[0];
672 let cols = logical.shape[1];
673 if row == 0 || row > rows || col == 0 || col > cols {
674 return Err("Index exceeds the number of array elements.".to_string());
675 }
676 let pos = (row - 1) + (col - 1) * rows;
677 if pos >= logical.data.len() {
678 return Err("Index exceeds the number of array elements.".to_string());
679 }
680 logical.data[pos] = if value { 1 } else { 0 };
681 Ok(())
682 }
683 _ => Err("setfield: indexing with more than two indices is not supported yet".to_string()),
684 }
685}
686
687fn assign_string_array_element(
688 array: &mut runmat_builtins::StringArray,
689 selector: &IndexSelector,
690 rhs: Value,
691) -> Result<(), String> {
692 let resolved = resolve_indices(&Value::StringArray(array.clone()), selector)?;
693 let text = String::try_from(&rhs)
694 .map_err(|_| "setfield: string assignments require text-compatible values".to_string())?;
695 match resolved.len() {
696 1 => {
697 let idx = resolved[0];
698 if idx == 0 || idx > array.data.len() {
699 return Err("Index exceeds the number of array elements.".to_string());
700 }
701 array.data[idx - 1] = text;
702 Ok(())
703 }
704 2 => {
705 let row = resolved[0];
706 let col = resolved[1];
707 if row == 0 || row > array.rows || col == 0 || col > array.cols {
708 return Err("Index exceeds the number of array elements.".to_string());
709 }
710 let pos = (row - 1) + (col - 1) * array.rows;
711 if pos >= array.data.len() {
712 return Err("Index exceeds the number of array elements.".to_string());
713 }
714 array.data[pos] = text;
715 Ok(())
716 }
717 _ => Err("setfield: indexing with more than two indices is not supported yet".to_string()),
718 }
719}
720
721fn assign_char_array_element(
722 array: &mut CharArray,
723 selector: &IndexSelector,
724 rhs: Value,
725) -> Result<(), String> {
726 let resolved = resolve_indices(&Value::CharArray(array.clone()), selector)?;
727 let text = String::try_from(&rhs)
728 .map_err(|_| "setfield: char assignments require text-compatible values".to_string())?;
729 if text.chars().count() != 1 {
730 return Err("setfield: char array assignments require single characters".to_string());
731 }
732 let ch = text.chars().next().unwrap();
733 match resolved.len() {
734 1 => {
735 let idx = resolved[0];
736 if idx == 0 || idx > array.data.len() {
737 return Err("Index exceeds the number of array elements.".to_string());
738 }
739 array.data[idx - 1] = ch;
740 Ok(())
741 }
742 2 => {
743 let row = resolved[0];
744 let col = resolved[1];
745 if row == 0 || row > array.rows || col == 0 || col > array.cols {
746 return Err("Index exceeds the number of array elements.".to_string());
747 }
748 let pos = (row - 1) * array.cols + (col - 1);
749 if pos >= array.data.len() {
750 return Err("Index exceeds the number of array elements.".to_string());
751 }
752 array.data[pos] = ch;
753 Ok(())
754 }
755 _ => Err("setfield: indexing with more than two indices is not supported yet".to_string()),
756 }
757}
758
759fn assign_complex_tensor_element(
760 tensor: &mut ComplexTensor,
761 selector: &IndexSelector,
762 rhs: Value,
763) -> Result<(), String> {
764 let resolved = resolve_indices(&Value::ComplexTensor(tensor.clone()), selector)?;
765 let (re, im) = match rhs {
766 Value::Complex(r, i) => (r, i),
767 Value::Num(n) => (n, 0.0),
768 Value::Int(i) => (i.to_f64(), 0.0),
769 other => {
770 return Err(format!(
771 "setfield: cannot assign {other:?} into a complex tensor element"
772 ));
773 }
774 };
775 match resolved.len() {
776 1 => {
777 let idx = resolved[0];
778 if idx == 0 || idx > tensor.data.len() {
779 return Err("Index exceeds the number of array elements.".to_string());
780 }
781 tensor.data[idx - 1] = (re, im);
782 Ok(())
783 }
784 2 => {
785 let row = resolved[0];
786 let col = resolved[1];
787 if row == 0 || row > tensor.rows || col == 0 || col > tensor.cols {
788 return Err("Index exceeds the number of array elements.".to_string());
789 }
790 let pos = (row - 1) + (col - 1) * tensor.rows;
791 if pos >= tensor.data.len() {
792 return Err("Index exceeds the number of array elements.".to_string());
793 }
794 tensor.data[pos] = (re, im);
795 Ok(())
796 }
797 _ => Err("setfield: indexing with more than two indices is not supported yet".to_string()),
798 }
799}
800
801fn read_object_property(obj: &ObjectInstance, name: &str) -> Result<Value, String> {
802 if let Some((prop, _owner)) = runmat_builtins::lookup_property(&obj.class_name, name) {
803 if prop.is_static {
804 return Err(format!(
805 "You cannot access the static property '{}' through an instance of class '{}'.",
806 name, obj.class_name
807 ));
808 }
809 if prop.get_access == Access::Private {
810 return Err(format!(
811 "You cannot get the '{}' property of '{}' class.",
812 name, obj.class_name
813 ));
814 }
815 if prop.is_dependent {
816 let getter = format!("get.{name}");
817 match call_builtin(&getter, &[Value::Object(obj.clone())]) {
818 Ok(value) => return Ok(value),
819 Err(err) => {
820 if !err.contains("MATLAB:UndefinedFunction") {
821 return Err(err);
822 }
823 }
824 }
825 if let Some(value) = obj.properties.get(&format!("{name}_backing")) {
826 return Ok(value.clone());
827 }
828 }
829 }
830
831 if let Some(value) = obj.properties.get(name) {
832 return Ok(value.clone());
833 }
834
835 if let Some((prop, _owner)) = runmat_builtins::lookup_property(&obj.class_name, name) {
836 if prop.get_access == Access::Private {
837 return Err(format!(
838 "You cannot get the '{}' property of '{}' class.",
839 name, obj.class_name
840 ));
841 }
842 return Err(format!(
843 "No public property '{}' for class '{}'.",
844 name, obj.class_name
845 ));
846 }
847
848 Err(format!(
849 "Undefined property '{}' for class {}",
850 name, obj.class_name
851 ))
852}
853
854fn write_object_property(obj: &mut ObjectInstance, name: &str, rhs: Value) -> Result<(), String> {
855 if let Some((prop, _owner)) = runmat_builtins::lookup_property(&obj.class_name, name) {
856 if prop.is_static {
857 return Err(format!(
858 "Property '{}' is static; use classref('{}').{}",
859 name, obj.class_name, name
860 ));
861 }
862 if prop.set_access == Access::Private {
863 return Err(format!("Property '{name}' is private"));
864 }
865 if prop.is_dependent {
866 let setter = format!("set.{name}");
867 if let Ok(value) = call_builtin(&setter, &[Value::Object(obj.clone()), rhs.clone()]) {
868 if let Value::Object(updated) = value {
869 *obj = updated;
870 return Ok(());
871 }
872 return Err(format!(
873 "Dependent property setter for '{}' must return the updated object",
874 name
875 ));
876 }
877 obj.properties.insert(format!("{name}_backing"), rhs);
878 return Ok(());
879 }
880 }
881
882 obj.properties.insert(name.to_string(), rhs);
883 Ok(())
884}
885
886fn assign_into_handle(handle: HandleRef, steps: &[FieldStep], rhs: Value) -> Result<Value, String> {
887 if steps.is_empty() {
888 return Err(
889 "setfield: expected at least one field name when assigning into a handle".to_string(),
890 );
891 }
892 if !handle.valid {
893 return Err(format!(
894 "Invalid or deleted handle object '{}'.",
895 handle.class_name
896 ));
897 }
898 let current = unsafe { &*handle.target.as_raw() }.clone();
899 let updated = assign_into_value(current, steps, rhs)?;
900 let raw = unsafe { handle.target.as_raw_mut() };
901 if raw.is_null() {
902 return Err("setfield: handle target is null".to_string());
903 }
904 unsafe {
905 *raw = updated;
906 }
907 Ok(Value::HandleObject(handle))
908}
909
910fn is_index_selector(value: &Value) -> bool {
911 matches!(value, Value::Cell(_))
912}
913
914fn parse_index_selector(value: Value) -> Result<IndexSelector, String> {
915 let Value::Cell(cell) = value else {
916 return Err("setfield: indices must be provided in a cell array".to_string());
917 };
918 let mut components = Vec::with_capacity(cell.data.len());
919 for handle in &cell.data {
920 let entry = unsafe { &*handle.as_raw() };
921 components.push(parse_index_component(entry)?);
922 }
923 Ok(IndexSelector { components })
924}
925
926fn parse_index_component(value: &Value) -> Result<IndexComponent, String> {
927 match value {
928 Value::CharArray(ca) => {
929 let text: String = ca.data.iter().collect();
930 parse_index_text(text.trim())
931 }
932 Value::String(s) => parse_index_text(s.trim()),
933 Value::StringArray(sa) if sa.data.len() == 1 => parse_index_text(sa.data[0].trim()),
934 _ => {
935 let idx = parse_positive_scalar(value)
936 .map_err(|e| format!("setfield: invalid index element ({e})"))?;
937 Ok(IndexComponent::Scalar(idx))
938 }
939 }
940}
941
942fn parse_index_text(text: &str) -> Result<IndexComponent, String> {
943 if text.eq_ignore_ascii_case("end") {
944 return Ok(IndexComponent::End);
945 }
946 if text == ":" {
947 return Err("setfield: ':' indexing is not currently supported".to_string());
948 }
949 if text.is_empty() {
950 return Err("setfield: index elements must not be empty".to_string());
951 }
952 if let Ok(value) = text.parse::<usize>() {
953 if value == 0 {
954 return Err("setfield: index must be >= 1".to_string());
955 }
956 return Ok(IndexComponent::Scalar(value));
957 }
958 Err(format!("setfield: invalid index element '{}'", text))
959}
960
961fn parse_positive_scalar(value: &Value) -> Result<usize, String> {
962 let number = match value {
963 Value::Int(i) => i.to_i64() as f64,
964 Value::Num(n) => *n,
965 Value::Tensor(t) if t.data.len() == 1 => t.data[0],
966 _ => {
967 let repr = format!("{value:?}");
968 return Err(format!("expected positive integer index, got {repr}"));
969 }
970 };
971
972 if !number.is_finite() {
973 return Err("index must be a finite number".to_string());
974 }
975 if number.fract() != 0.0 {
976 return Err("index must be an integer".to_string());
977 }
978 if number <= 0.0 {
979 return Err("index must be >= 1".to_string());
980 }
981 if number > usize::MAX as f64 {
982 return Err("index exceeds platform limits".to_string());
983 }
984 Ok(number as usize)
985}
986
987fn parse_field_name(value: Value) -> Result<String, String> {
988 match value {
989 Value::String(s) => Ok(s),
990 Value::StringArray(sa) => {
991 if sa.data.len() == 1 {
992 Ok(sa.data[0].clone())
993 } else {
994 Err(
995 "setfield: field names must be scalar string arrays or character vectors"
996 .to_string(),
997 )
998 }
999 }
1000 Value::CharArray(ca) => {
1001 if ca.rows == 1 {
1002 Ok(ca.data.iter().collect())
1003 } else {
1004 Err("setfield: field names must be 1-by-N character vectors".to_string())
1005 }
1006 }
1007 other => Err(format!("setfield: expected field name, got {other:?}")),
1008 }
1009}
1010
1011fn resolve_indices(value: &Value, selector: &IndexSelector) -> Result<Vec<usize>, String> {
1012 let dims = selector.components.len();
1013 let mut resolved = Vec::with_capacity(dims);
1014 for (dim_idx, component) in selector.components.iter().enumerate() {
1015 let index = match component {
1016 IndexComponent::Scalar(idx) => *idx,
1017 IndexComponent::End => dimension_length(value, dims, dim_idx)?,
1018 };
1019 resolved.push(index);
1020 }
1021 Ok(resolved)
1022}
1023
1024fn dimension_length(value: &Value, dims: usize, dim_idx: usize) -> Result<usize, String> {
1025 match value {
1026 Value::Tensor(tensor) => tensor_dimension_length(tensor, dims, dim_idx),
1027 Value::Cell(cell) => cell_dimension_length(cell, dims, dim_idx),
1028 Value::StringArray(array) => string_array_dimension_length(array, dims, dim_idx),
1029 Value::LogicalArray(logical) => logical_array_dimension_length(logical, dims, dim_idx),
1030 Value::CharArray(array) => char_array_dimension_length(array, dims, dim_idx),
1031 Value::ComplexTensor(tensor) => complex_tensor_dimension_length(tensor, dims, dim_idx),
1032 Value::Num(_) | Value::Int(_) | Value::Bool(_) => {
1033 if dims == 1 {
1034 Ok(1)
1035 } else {
1036 Err(
1037 "setfield: indexing with more than one dimension is not supported for scalars"
1038 .to_string(),
1039 )
1040 }
1041 }
1042 other => Err(format!(
1043 "Struct contents assignment to a {other:?} object is not supported."
1044 )),
1045 }
1046}
1047
1048fn tensor_dimension_length(tensor: &Tensor, dims: usize, dim_idx: usize) -> Result<usize, String> {
1049 if dims == 1 {
1050 let total = tensor.data.len();
1051 if total == 0 {
1052 return Err("Index exceeds the number of array elements (0).".to_string());
1053 }
1054 return Ok(total);
1055 }
1056 if dims > 2 {
1057 return Err(
1058 "setfield: indexing with more than two indices is not supported yet".to_string(),
1059 );
1060 }
1061 let len = if dim_idx == 0 {
1062 tensor.rows()
1063 } else {
1064 tensor.cols()
1065 };
1066 if len == 0 {
1067 return Err("Index exceeds the number of array elements (0).".to_string());
1068 }
1069 Ok(len)
1070}
1071
1072fn cell_dimension_length(cell: &CellArray, dims: usize, dim_idx: usize) -> Result<usize, String> {
1073 if dims == 1 {
1074 let total = cell.data.len();
1075 if total == 0 {
1076 return Err("Index exceeds the number of array elements (0).".to_string());
1077 }
1078 return Ok(total);
1079 }
1080 if dims > 2 {
1081 return Err(
1082 "setfield: indexing with more than two indices is not supported yet".to_string(),
1083 );
1084 }
1085 let len = if dim_idx == 0 { cell.rows } else { cell.cols };
1086 if len == 0 {
1087 return Err("Index exceeds the number of array elements (0).".to_string());
1088 }
1089 Ok(len)
1090}
1091
1092fn string_array_dimension_length(
1093 array: &runmat_builtins::StringArray,
1094 dims: usize,
1095 dim_idx: usize,
1096) -> Result<usize, String> {
1097 if dims == 1 {
1098 let total = array.data.len();
1099 if total == 0 {
1100 return Err("Index exceeds the number of array elements (0).".to_string());
1101 }
1102 return Ok(total);
1103 }
1104 if dims > 2 {
1105 return Err(
1106 "setfield: indexing with more than two indices is not supported yet".to_string(),
1107 );
1108 }
1109 let len = if dim_idx == 0 { array.rows } else { array.cols };
1110 if len == 0 {
1111 return Err("Index exceeds the number of array elements (0).".to_string());
1112 }
1113 Ok(len)
1114}
1115
1116fn logical_array_dimension_length(
1117 array: &LogicalArray,
1118 dims: usize,
1119 dim_idx: usize,
1120) -> Result<usize, String> {
1121 if dims == 1 {
1122 let total = array.data.len();
1123 if total == 0 {
1124 return Err("Index exceeds the number of array elements (0).".to_string());
1125 }
1126 return Ok(total);
1127 }
1128 if dims > 2 {
1129 return Err(
1130 "setfield: indexing with more than two indices is not supported yet".to_string(),
1131 );
1132 }
1133 if array.shape.len() < dims {
1134 return Err("Index exceeds the number of array elements (0).".to_string());
1135 }
1136 let len = array.shape[dim_idx];
1137 if len == 0 {
1138 return Err("Index exceeds the number of array elements (0).".to_string());
1139 }
1140 Ok(len)
1141}
1142
1143fn char_array_dimension_length(
1144 array: &CharArray,
1145 dims: usize,
1146 dim_idx: usize,
1147) -> Result<usize, String> {
1148 if dims == 1 {
1149 let total = array.data.len();
1150 if total == 0 {
1151 return Err("Index exceeds the number of array elements (0).".to_string());
1152 }
1153 return Ok(total);
1154 }
1155 if dims > 2 {
1156 return Err(
1157 "setfield: indexing with more than two indices is not supported yet".to_string(),
1158 );
1159 }
1160 let len = if dim_idx == 0 { array.rows } else { array.cols };
1161 if len == 0 {
1162 return Err("Index exceeds the number of array elements (0).".to_string());
1163 }
1164 Ok(len)
1165}
1166
1167fn complex_tensor_dimension_length(
1168 tensor: &ComplexTensor,
1169 dims: usize,
1170 dim_idx: usize,
1171) -> Result<usize, String> {
1172 if dims == 1 {
1173 let total = tensor.data.len();
1174 if total == 0 {
1175 return Err("Index exceeds the number of array elements (0).".to_string());
1176 }
1177 return Ok(total);
1178 }
1179 if dims > 2 {
1180 return Err(
1181 "setfield: indexing with more than two indices is not supported yet".to_string(),
1182 );
1183 }
1184 let len = if dim_idx == 0 {
1185 tensor.rows
1186 } else {
1187 tensor.cols
1188 };
1189 if len == 0 {
1190 return Err("Index exceeds the number of array elements (0).".to_string());
1191 }
1192 Ok(len)
1193}
1194
1195fn value_to_scalar(value: Value) -> Result<f64, String> {
1196 match value {
1197 Value::Num(n) => Ok(n),
1198 Value::Int(i) => Ok(i.to_f64()),
1199 Value::Bool(b) => Ok(if b { 1.0 } else { 0.0 }),
1200 Value::Tensor(t) if t.data.len() == 1 => Ok(t.data[0]),
1201 other => Err(format!(
1202 "setfield: cannot assign {other:?} into a numeric tensor element"
1203 )),
1204 }
1205}
1206
1207fn value_to_bool(value: Value) -> Result<bool, String> {
1208 match value {
1209 Value::Bool(b) => Ok(b),
1210 Value::Num(n) => Ok(n != 0.0),
1211 Value::Int(i) => Ok(i.to_i64() != 0),
1212 Value::Tensor(t) if t.data.len() == 1 => Ok(t.data[0] != 0.0),
1213 other => Err(format!(
1214 "setfield: cannot assign {other:?} into a logical array element"
1215 )),
1216 }
1217}
1218
1219fn allocate_cell_handle(value: Value) -> Result<GcPtr<Value>, String> {
1220 runmat_gc::gc_allocate(value)
1221 .map_err(|e| format!("setfield: failed to allocate cell element in GC: {e}"))
1222}
1223
1224fn is_struct_array(cell: &CellArray) -> bool {
1225 cell.data
1226 .iter()
1227 .all(|handle| matches!(unsafe { &*handle.as_raw() }, Value::Struct(_)))
1228}
1229
1230#[cfg(test)]
1231mod tests {
1232 use super::*;
1233 use runmat_builtins::{
1234 Access, CellArray, ClassDef, HandleRef, IntValue, ObjectInstance, PropertyDef, StructValue,
1235 };
1236 use runmat_gc::gc_allocate;
1237
1238 #[cfg(feature = "doc_export")]
1239 use crate::builtins::common::test_support;
1240
1241 #[test]
1242 fn setfield_creates_scalar_field() {
1243 let struct_value = StructValue::new();
1244 let updated = setfield_builtin(
1245 Value::Struct(struct_value),
1246 vec![Value::from("answer"), Value::Num(42.0)],
1247 )
1248 .expect("setfield");
1249 match updated {
1250 Value::Struct(st) => {
1251 assert_eq!(
1252 st.fields.get("answer"),
1253 Some(&Value::Num(42.0)),
1254 "field should be inserted"
1255 );
1256 }
1257 other => panic!("expected struct result, got {other:?}"),
1258 }
1259 }
1260
1261 #[test]
1262 fn setfield_creates_nested_structs() {
1263 let struct_value = StructValue::new();
1264 let updated = setfield_builtin(
1265 Value::Struct(struct_value),
1266 vec![
1267 Value::from("solver"),
1268 Value::from("name"),
1269 Value::from("cg"),
1270 ],
1271 )
1272 .expect("setfield");
1273 match updated {
1274 Value::Struct(st) => {
1275 let solver = st.fields.get("solver").expect("solver field");
1276 match solver {
1277 Value::Struct(inner) => {
1278 assert_eq!(
1279 inner.fields.get("name"),
1280 Some(&Value::from("cg")),
1281 "inner field should exist"
1282 );
1283 }
1284 other => panic!("expected inner struct, got {other:?}"),
1285 }
1286 }
1287 other => panic!("expected struct result, got {other:?}"),
1288 }
1289 }
1290
1291 #[test]
1292 fn setfield_updates_struct_array_element() {
1293 let mut a = StructValue::new();
1294 a.fields
1295 .insert("id".to_string(), Value::Int(IntValue::I32(1)));
1296 let mut b = StructValue::new();
1297 b.fields
1298 .insert("id".to_string(), Value::Int(IntValue::I32(2)));
1299 let array = CellArray::new_with_shape(vec![Value::Struct(a), Value::Struct(b)], vec![1, 2])
1300 .unwrap();
1301 let indices =
1302 CellArray::new_with_shape(vec![Value::Int(IntValue::I32(2))], vec![1, 1]).unwrap();
1303 let updated = setfield_builtin(
1304 Value::Cell(array),
1305 vec![
1306 Value::Cell(indices),
1307 Value::from("id"),
1308 Value::Int(IntValue::I32(42)),
1309 ],
1310 )
1311 .expect("setfield");
1312 match updated {
1313 Value::Cell(cell) => {
1314 let second = unsafe { &*cell.data[1].as_raw() }.clone();
1315 match second {
1316 Value::Struct(st) => {
1317 assert_eq!(st.fields.get("id"), Some(&Value::Int(IntValue::I32(42))));
1318 }
1319 other => panic!("expected struct element, got {other:?}"),
1320 }
1321 }
1322 other => panic!("expected cell array, got {other:?}"),
1323 }
1324 }
1325
1326 #[test]
1327 fn setfield_assigns_into_cell_then_struct() {
1328 let mut inner1 = StructValue::new();
1329 inner1.fields.insert("value".to_string(), Value::Num(1.0));
1330 let mut inner2 = StructValue::new();
1331 inner2.fields.insert("value".to_string(), Value::Num(2.0));
1332 let cell = CellArray::new_with_shape(
1333 vec![Value::Struct(inner1), Value::Struct(inner2)],
1334 vec![1, 2],
1335 )
1336 .unwrap();
1337 let mut root = StructValue::new();
1338 root.fields.insert("samples".to_string(), Value::Cell(cell));
1339
1340 let index_cell =
1341 CellArray::new_with_shape(vec![Value::Int(IntValue::I32(2))], vec![1, 1]).unwrap();
1342 let updated = setfield_builtin(
1343 Value::Struct(root),
1344 vec![
1345 Value::from("samples"),
1346 Value::Cell(index_cell),
1347 Value::from("value"),
1348 Value::Num(10.0),
1349 ],
1350 )
1351 .expect("setfield");
1352
1353 match updated {
1354 Value::Struct(st) => {
1355 let samples = st.fields.get("samples").expect("samples field");
1356 match samples {
1357 Value::Cell(cell) => {
1358 let value = unsafe { &*cell.data[1].as_raw() }.clone();
1359 match value {
1360 Value::Struct(inner) => {
1361 assert_eq!(inner.fields.get("value"), Some(&Value::Num(10.0)));
1362 }
1363 other => panic!("expected struct, got {other:?}"),
1364 }
1365 }
1366 other => panic!("expected cell array, got {other:?}"),
1367 }
1368 }
1369 other => panic!("expected struct, got {other:?}"),
1370 }
1371 }
1372
1373 #[test]
1374 fn setfield_struct_array_with_end_index() {
1375 let mut first = StructValue::new();
1376 first
1377 .fields
1378 .insert("id".to_string(), Value::Int(IntValue::I32(1)));
1379 let mut second = StructValue::new();
1380 second
1381 .fields
1382 .insert("id".to_string(), Value::Int(IntValue::I32(2)));
1383 let array = CellArray::new_with_shape(
1384 vec![Value::Struct(first), Value::Struct(second)],
1385 vec![1, 2],
1386 )
1387 .unwrap();
1388 let index_cell = CellArray::new_with_shape(vec![Value::from("end")], vec![1, 1]).unwrap();
1389 let updated = setfield_builtin(
1390 Value::Cell(array),
1391 vec![
1392 Value::Cell(index_cell),
1393 Value::from("id"),
1394 Value::Int(IntValue::I32(99)),
1395 ],
1396 )
1397 .expect("setfield");
1398 match updated {
1399 Value::Cell(cell) => {
1400 let second = unsafe { &*cell.data[1].as_raw() }.clone();
1401 match second {
1402 Value::Struct(st) => {
1403 assert_eq!(st.fields.get("id"), Some(&Value::Int(IntValue::I32(99))));
1404 }
1405 other => panic!("expected struct element, got {other:?}"),
1406 }
1407 }
1408 other => panic!("expected cell array result, got {other:?}"),
1409 }
1410 }
1411
1412 #[test]
1413 fn setfield_assigns_object_property() {
1414 let mut class_def = ClassDef {
1415 name: "Simple".to_string(),
1416 parent: None,
1417 properties: Default::default(),
1418 methods: Default::default(),
1419 };
1420 class_def.properties.insert(
1421 "x".to_string(),
1422 PropertyDef {
1423 name: "x".to_string(),
1424 is_static: false,
1425 is_dependent: false,
1426 get_access: Access::Public,
1427 set_access: Access::Public,
1428 default_value: None,
1429 },
1430 );
1431 runmat_builtins::register_class(class_def);
1432
1433 let mut obj = ObjectInstance::new("Simple".to_string());
1434 obj.properties.insert("x".to_string(), Value::Num(0.0));
1435
1436 let updated = setfield_builtin(Value::Object(obj), vec![Value::from("x"), Value::Num(5.0)])
1437 .expect("setfield");
1438
1439 match updated {
1440 Value::Object(o) => {
1441 assert_eq!(o.properties.get("x"), Some(&Value::Num(5.0)));
1442 }
1443 other => panic!("expected object result, got {other:?}"),
1444 }
1445 }
1446
1447 #[test]
1448 fn setfield_errors_when_indexing_missing_field() {
1449 let struct_value = StructValue::new();
1450 let index_cell =
1451 CellArray::new_with_shape(vec![Value::Int(IntValue::I32(1))], vec![1, 1]).unwrap();
1452 let err = setfield_builtin(
1453 Value::Struct(struct_value),
1454 vec![
1455 Value::from("missing"),
1456 Value::Cell(index_cell),
1457 Value::Num(1.0),
1458 ],
1459 )
1460 .expect_err("setfield should fail when field is missing");
1461 assert!(
1462 err.contains("Reference to non-existent field 'missing'."),
1463 "unexpected error message: {err}"
1464 );
1465 }
1466
1467 #[test]
1468 fn setfield_errors_on_static_property_assignment() {
1469 let mut class_def = ClassDef {
1470 name: "StaticSetfield".to_string(),
1471 parent: None,
1472 properties: Default::default(),
1473 methods: Default::default(),
1474 };
1475 class_def.properties.insert(
1476 "version".to_string(),
1477 PropertyDef {
1478 name: "version".to_string(),
1479 is_static: true,
1480 is_dependent: false,
1481 get_access: Access::Public,
1482 set_access: Access::Public,
1483 default_value: None,
1484 },
1485 );
1486 runmat_builtins::register_class(class_def);
1487
1488 let obj = ObjectInstance::new("StaticSetfield".to_string());
1489 let err = setfield_builtin(
1490 Value::Object(obj),
1491 vec![Value::from("version"), Value::Num(2.0)],
1492 )
1493 .expect_err("setfield should reject static property writes");
1494 assert!(
1495 err.contains("Property 'version' is static"),
1496 "unexpected error message: {err}"
1497 );
1498 }
1499
1500 #[test]
1501 fn setfield_updates_handle_target() {
1502 let mut inner = StructValue::new();
1503 inner.fields.insert("x".to_string(), Value::Num(0.0));
1504 let gc_ptr = gc_allocate(Value::Struct(inner)).expect("gc allocation");
1505 let handle_ptr = gc_ptr.clone();
1506 let handle = HandleRef {
1507 class_name: "PointHandle".to_string(),
1508 target: handle_ptr,
1509 valid: true,
1510 };
1511
1512 let updated = setfield_builtin(
1513 Value::HandleObject(handle.clone()),
1514 vec![Value::from("x"), Value::Num(7.0)],
1515 )
1516 .expect("setfield handle update");
1517
1518 match updated {
1519 Value::HandleObject(h) => assert!(h.valid),
1520 other => panic!("expected handle, got {other:?}"),
1521 }
1522
1523 let pointee = unsafe { &*gc_ptr.as_raw() };
1524 match pointee {
1525 Value::Struct(st) => {
1526 assert_eq!(st.fields.get("x"), Some(&Value::Num(7.0)));
1527 }
1528 other => panic!("expected struct pointee, got {other:?}"),
1529 }
1530 }
1531
1532 #[test]
1533 #[cfg(feature = "wgpu")]
1534 fn setfield_gpu_tensor_indexing_gathers_to_host() {
1535 use runmat_accelerate::backend::wgpu::provider::{
1536 register_wgpu_provider, WgpuProviderOptions,
1537 };
1538 use runmat_accelerate_api::HostTensorView;
1539
1540 if runmat_accelerate_api::provider().is_none()
1541 && register_wgpu_provider(WgpuProviderOptions::default()).is_err()
1542 {
1543 runmat_accelerate::simple_provider::register_inprocess_provider();
1544 }
1545
1546 let provider = runmat_accelerate_api::provider().expect("accel provider");
1547 let data = [1.0, 2.0, 3.0, 4.0];
1548 let shape = [2usize, 2usize];
1549 let view = HostTensorView {
1550 data: &data,
1551 shape: &shape,
1552 };
1553 let handle = provider.upload(&view).expect("upload");
1554
1555 let mut root = StructValue::new();
1556 root.fields
1557 .insert("values".to_string(), Value::GpuTensor(handle));
1558
1559 let index_cell = CellArray::new_with_shape(
1560 vec![Value::Int(IntValue::I32(2)), Value::Int(IntValue::I32(2))],
1561 vec![1, 2],
1562 )
1563 .unwrap();
1564
1565 let updated = setfield_builtin(
1566 Value::Struct(root),
1567 vec![
1568 Value::from("values"),
1569 Value::Cell(index_cell),
1570 Value::Num(99.0),
1571 ],
1572 )
1573 .expect("setfield gpu value");
1574
1575 match updated {
1576 Value::Struct(st) => {
1577 let values = st.fields.get("values").expect("values field");
1578 match values {
1579 Value::Tensor(tensor) => {
1580 assert_eq!(tensor.shape, vec![2, 2]);
1581 assert_eq!(tensor.data[3], 99.0);
1582 }
1583 other => panic!("expected tensor after gather, got {other:?}"),
1584 }
1585 }
1586 other => panic!("expected struct result, got {other:?}"),
1587 }
1588 }
1589
1590 #[test]
1591 #[cfg(feature = "doc_export")]
1592 fn doc_examples_present() {
1593 let blocks = test_support::doc_examples(DOC_MD);
1594 assert!(!blocks.is_empty());
1595 }
1596}