Skip to main content

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