runmat_runtime/builtins/structs/core/
setfield.rs

1//! MATLAB-compatible `setfield` builtin with struct array and object support.
2//!
3//! Mirrors MATLAB's `setfield` semantics, including nested field creation, struct
4//! array indexing via cell arguments, and property assignment on MATLAB-style
5//! objects. The builtin performs all updates on host data; GPU-resident values are
6//! gathered automatically before mutation. Updated tensors remain on the host.
7
8use 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}