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::common::tensor;
13use crate::builtins::introspection::dynamicprops;
14use crate::builtins::structs::type_resolvers::setfield_type;
15use crate::{
16    build_runtime_error, call_builtin_async, gather_if_needed_async, object_property_getter_name,
17    object_property_setter_name, BuiltinResult, RuntimeError,
18};
19use runmat_builtins::{
20    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
21    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
22};
23use runmat_builtins::{
24    BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor, BuiltinIntegerComputationDomain,
25    BuiltinIntegerInputAvailability, BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule,
26    BuiltinIntegerOverflowRule, BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule,
27};
28use runmat_macros::runtime_builtin;
29use runmat_types::MemberAccess;
30use runmat_value::{
31    CellArray, CharArray, ComplexTensor, HandleRef, LogicalArray, NumericScalar, ObjectInstance,
32    StructValue, Tensor, Value,
33};
34use std::convert::TryFrom;
35
36#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::structs::core::setfield")]
37pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
38    name: "setfield",
39    op_kind: GpuOpKind::Custom("setfield"),
40    supported_precisions: &[],
41    broadcast: BroadcastSemantics::None,
42    provider_hooks: &[],
43    constant_strategy: ConstantStrategy::InlineLiteral,
44    residency: ResidencyPolicy::InheritInputs,
45    nan_mode: ReductionNaN::Include,
46    two_pass_threshold: None,
47    workgroup_size: None,
48    accepts_nan_mode: false,
49    notes: "Host-only metadata mutation; GPU tensors are gathered before assignment.",
50};
51
52#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::structs::core::setfield")]
53pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
54    name: "setfield",
55    shape: ShapeRequirements::Any,
56    constant_strategy: ConstantStrategy::InlineLiteral,
57    elementwise: None,
58    reduction: None,
59    emits_nan: false,
60    notes: "Assignments terminate fusion and gather device data back to the host.",
61};
62
63const BUILTIN_NAME: &str = "setfield";
64const SETFIELD_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
65    name: "S",
66    ty: BuiltinParamType::Any,
67    arity: BuiltinParamArity::Required,
68    default: None,
69    description: "Updated struct/object/array value.",
70}];
71
72const SETFIELD_INPUTS_SCALAR: [BuiltinParamDescriptor; 3] = [
73    BuiltinParamDescriptor {
74        name: "S",
75        ty: BuiltinParamType::Any,
76        arity: BuiltinParamArity::Required,
77        default: None,
78        description: "Input struct/object/struct-array target.",
79    },
80    BuiltinParamDescriptor {
81        name: "field",
82        ty: BuiltinParamType::PropertyName,
83        arity: BuiltinParamArity::Required,
84        default: None,
85        description: "Field/property name to assign.",
86    },
87    BuiltinParamDescriptor {
88        name: "value",
89        ty: BuiltinParamType::Any,
90        arity: BuiltinParamArity::Required,
91        default: None,
92        description: "Assigned value.",
93    },
94];
95
96const SETFIELD_INPUTS_NESTED: [BuiltinParamDescriptor; 3] = [
97    BuiltinParamDescriptor {
98        name: "S",
99        ty: BuiltinParamType::Any,
100        arity: BuiltinParamArity::Required,
101        default: None,
102        description: "Input struct/object/struct-array target.",
103    },
104    BuiltinParamDescriptor {
105        name: "path",
106        ty: BuiltinParamType::Any,
107        arity: BuiltinParamArity::Variadic,
108        default: None,
109        description:
110            "Alternating field names and optional index-selector cells `{...}` for nested assignment.",
111    },
112    BuiltinParamDescriptor {
113        name: "value",
114        ty: BuiltinParamType::Any,
115        arity: BuiltinParamArity::Required,
116        default: None,
117        description: "Assigned value.",
118    },
119];
120
121const SETFIELD_INPUTS_LEADING_INDEX: [BuiltinParamDescriptor; 4] = [
122    BuiltinParamDescriptor {
123        name: "S",
124        ty: BuiltinParamType::Any,
125        arity: BuiltinParamArity::Required,
126        default: None,
127        description: "Input struct-array target.",
128    },
129    BuiltinParamDescriptor {
130        name: "index_selector",
131        ty: BuiltinParamType::Any,
132        arity: BuiltinParamArity::Required,
133        default: None,
134        description: "Leading index selector in a cell array, e.g. `{2}` or `{end}`.",
135    },
136    BuiltinParamDescriptor {
137        name: "path",
138        ty: BuiltinParamType::Any,
139        arity: BuiltinParamArity::Variadic,
140        default: None,
141        description:
142            "Alternating field names and optional index-selector cells `{...}` for nested assignment.",
143    },
144    BuiltinParamDescriptor {
145        name: "value",
146        ty: BuiltinParamType::Any,
147        arity: BuiltinParamArity::Required,
148        default: None,
149        description: "Assigned value.",
150    },
151];
152
153const SETFIELD_SIGNATURES: [BuiltinSignatureDescriptor; 3] = [
154    BuiltinSignatureDescriptor {
155        label: "S = setfield(S, field, value)",
156        inputs: &SETFIELD_INPUTS_SCALAR,
157        outputs: &SETFIELD_OUTPUT,
158    },
159    BuiltinSignatureDescriptor {
160        label: "S = setfield(S, field_or_index, ..., value)",
161        inputs: &SETFIELD_INPUTS_NESTED,
162        outputs: &SETFIELD_OUTPUT,
163    },
164    BuiltinSignatureDescriptor {
165        label: "S = setfield(S, {idx0}, field_or_index, ..., value)",
166        inputs: &SETFIELD_INPUTS_LEADING_INDEX,
167        outputs: &SETFIELD_OUTPUT,
168    },
169];
170
171const SETFIELD_INTEGER_VALUE_INPUT: [BuiltinIntegerInputCapability; 1] =
172    [BuiltinIntegerInputCapability {
173        name: "value",
174        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
175        availability: BuiltinIntegerInputAvailability::Documented,
176        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
177        notes: "An integer assigned as a new or replacement field value remains an ordinary exact MATLAB value with its native class and shape.",
178    }];
179const SETFIELD_INTEGER_INDEX_INPUT: [BuiltinIntegerInputCapability; 1] =
180    [BuiltinIntegerInputCapability {
181        name: "idx cells",
182        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
183        availability: BuiltinIntegerInputAvailability::Documented,
184        scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
185        notes: "Numeric index values inside selector cells are decoded exactly and range checked before structural indexing.",
186    }];
187pub const SETFIELD_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 2] = [
188    BuiltinIntegerCapabilityDescriptor { form: "S = setfield(S, field_or_path, integer_value)", inputs: &SETFIELD_INTEGER_VALUE_INPUT, computation_domain: BuiltinIntegerComputationDomain::Structural, output_class: BuiltinIntegerOutputClassRule::PreserveInput, overflow: BuiltinIntegerOverflowRule::NotApplicable, backend: BuiltinIntegerBackendRule::GatherFallback, overload: BuiltinIntegerOverloadKind::Multiple, notes: "Direct structure and object field replacement preserves authoritative integer payloads; assignment into an existing typed numeric container follows that container's documented conversion rule." },
189    BuiltinIntegerCapabilityDescriptor { form: "S = setfield(S, {integer_idx}, field_or_path, value)", inputs: &SETFIELD_INTEGER_INDEX_INPUT, computation_domain: BuiltinIntegerComputationDomain::Structural, output_class: BuiltinIntegerOutputClassRule::FunctionSpecific, overflow: BuiltinIntegerOverflowRule::Error, backend: BuiltinIntegerBackendRule::GatherFallback, overload: BuiltinIntegerOverloadKind::Multiple, notes: "Index selectors are decoded exactly in every supported integer class; host mutation gathers automatically resident values when required." },
190];
191
192const SETFIELD_ERROR_NOT_ENOUGH_INPUTS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
193    code: "RM.SETFIELD.NOT_ENOUGH_INPUTS",
194    identifier: Some("RunMat:setfield:NotEnoughInputs"),
195    when: "Input does not provide at least one path component plus assigned value.",
196    message: "setfield: expected at least one field name and a value",
197};
198
199const SETFIELD_ERROR_FIELD_EXPECTED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
200    code: "RM.SETFIELD.FIELD_EXPECTED",
201    identifier: Some("RunMat:setfield:FieldExpected"),
202    when: "Field/path arguments are missing after parsing selectors.",
203    message: "setfield: expected field name arguments",
204};
205
206const SETFIELD_ERROR_INDEX_SELECTOR_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
207    code: "RM.SETFIELD.INDEX_SELECTOR_TYPE",
208    identifier: Some("RunMat:setfield:IndexSelectorType"),
209    when: "Index selector is not provided as a cell array.",
210    message: "setfield: indices must be provided in a cell array",
211};
212
213const SETFIELD_ERROR_INDEX_INVALID: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
214    code: "RM.SETFIELD.INDEX_INVALID",
215    identifier: Some("RunMat:setfield:InvalidIndex"),
216    when: "Index component is malformed, empty, unsupported, or not a positive integer.",
217    message: "setfield: invalid index element",
218};
219
220const SETFIELD_ERROR_FIELD_NAME_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
221    code: "RM.SETFIELD.FIELD_NAME_TYPE",
222    identifier: Some("RunMat:setfield:FieldNameType"),
223    when: "Field name is not a scalar string or 1-by-N char vector.",
224    message: "setfield: expected field name",
225};
226
227const SETFIELD_ERROR_INDEX_SHAPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
228    code: "RM.SETFIELD.INDEX_SHAPE",
229    identifier: Some("RunMat:setfield:IndexShape"),
230    when: "Indexing rank/shape is unsupported for the targeted value.",
231    message: "setfield: unsupported index shape for target value",
232};
233
234const SETFIELD_ERROR_NON_STRUCT_ASSIGNMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
235    code: "RM.SETFIELD.NON_STRUCT_ASSIGNMENT",
236    identifier: Some("RunMat:setfield:NonStructAssignment"),
237    when: "Assignment target does not support struct-like field updates.",
238    message: "Struct contents assignment to a non-struct object is not supported.",
239};
240
241const SETFIELD_ERROR_INDEX_OUT_OF_BOUNDS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
242    code: "RM.SETFIELD.INDEX_OUT_OF_BOUNDS",
243    identifier: Some("RunMat:setfield:IndexOutOfBounds"),
244    when: "Resolved index is outside bounds for target value.",
245    message: "Index exceeds the number of array elements.",
246};
247
248const SETFIELD_ERROR_MISSING_FIELD: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
249    code: "RM.SETFIELD.MISSING_FIELD",
250    identifier: Some("RunMat:setfield:MissingField"),
251    when: "Indexed assignment path references a missing field.",
252    message: "Reference to non-existent field",
253};
254
255const SETFIELD_ERROR_PROPERTY_PRIVATE_ACCESS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
256    code: "RM.SETFIELD.PROPERTY_PRIVATE_ACCESS",
257    identifier: Some("RunMat:PropertyPrivateAccess"),
258    when: "Property exists but get/set access is private.",
259    message: "setfield: private property access denied",
260};
261
262const SETFIELD_ERROR_PROPERTY_STATIC_ACCESS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
263    code: "RM.SETFIELD.PROPERTY_STATIC_ACCESS",
264    identifier: Some("RunMat:PropertyStaticAccess"),
265    when: "Property exists but is static and cannot be assigned through an instance.",
266    message: "setfield: static property access denied",
267};
268
269const SETFIELD_ERROR_OBJECT_PROPERTY: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
270    code: "RM.SETFIELD.OBJECT_PROPERTY",
271    identifier: Some("RunMat:setfield:ObjectProperty"),
272    when: "Object property operation is invalid (static, non-public, or malformed setter result).",
273    message: "setfield: invalid object property operation",
274};
275
276const SETFIELD_ERROR_INVALID_HANDLE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
277    code: "RM.SETFIELD.INVALID_HANDLE",
278    identifier: Some("RunMat:setfield:InvalidHandle"),
279    when: "Handle target is invalid/deleted/null.",
280    message: "setfield: invalid or deleted handle object",
281};
282
283const SETFIELD_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
284    code: "RM.SETFIELD.INTERNAL",
285    identifier: Some("RunMat:setfield:InternalError"),
286    when: "Internal conversion/allocation failed while assigning values.",
287    message: "setfield: internal error",
288};
289
290const SETFIELD_ERRORS: [BuiltinErrorDescriptor; 14] = [
291    SETFIELD_ERROR_NOT_ENOUGH_INPUTS,
292    SETFIELD_ERROR_FIELD_EXPECTED,
293    SETFIELD_ERROR_INDEX_SELECTOR_TYPE,
294    SETFIELD_ERROR_INDEX_INVALID,
295    SETFIELD_ERROR_FIELD_NAME_TYPE,
296    SETFIELD_ERROR_INDEX_SHAPE,
297    SETFIELD_ERROR_NON_STRUCT_ASSIGNMENT,
298    SETFIELD_ERROR_INDEX_OUT_OF_BOUNDS,
299    SETFIELD_ERROR_MISSING_FIELD,
300    SETFIELD_ERROR_PROPERTY_PRIVATE_ACCESS,
301    SETFIELD_ERROR_PROPERTY_STATIC_ACCESS,
302    SETFIELD_ERROR_OBJECT_PROPERTY,
303    SETFIELD_ERROR_INVALID_HANDLE,
304    SETFIELD_ERROR_INTERNAL,
305];
306
307pub const SETFIELD_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
308    signatures: &SETFIELD_SIGNATURES,
309    output_mode: BuiltinOutputMode::Fixed,
310    completion_policy: BuiltinCompletionPolicy::Public,
311    errors: &SETFIELD_ERRORS,
312};
313
314fn setfield_flow(message: impl Into<String>) -> RuntimeError {
315    setfield_error_with_message(
316        format!("{}: {}", SETFIELD_ERROR_INTERNAL.message, message.into()),
317        &SETFIELD_ERROR_INTERNAL,
318    )
319}
320
321fn setfield_error_with_message(
322    message: impl Into<String>,
323    error: &'static BuiltinErrorDescriptor,
324) -> RuntimeError {
325    let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
326    if let Some(identifier) = error.identifier {
327        builder = builder.with_identifier(identifier);
328    }
329    builder.build()
330}
331
332fn setfield_private_access(message: impl Into<String>) -> RuntimeError {
333    setfield_error_with_message(message, &SETFIELD_ERROR_PROPERTY_PRIVATE_ACCESS)
334}
335
336fn setfield_static_access(message: impl Into<String>) -> RuntimeError {
337    setfield_error_with_message(message, &SETFIELD_ERROR_PROPERTY_STATIC_ACCESS)
338}
339
340fn remap_setfield_flow(err: RuntimeError, prefix: Option<&str>) -> RuntimeError {
341    let mut message = err.message().to_string();
342    if let Some(prefix) = prefix {
343        if !message.starts_with(prefix) {
344            message = format!("{prefix}{message}");
345        }
346    }
347    let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
348    if let Some(identifier) = err.identifier() {
349        builder = builder.with_identifier(identifier);
350    }
351    builder.with_source(err).build()
352}
353
354fn is_undefined_function(err: &RuntimeError) -> bool {
355    err.identifier() == Some(crate::IDENT_UNDEFINED_FUNCTION)
356}
357
358#[runtime_builtin(
359    name = "setfield",
360    category = "structs/core",
361    summary = "Assign values into struct fields, nested fields, or struct-array elements.",
362    keywords = "setfield,struct,assignment,object property",
363    type_resolver(setfield_type),
364    descriptor(crate::builtins::structs::core::setfield::SETFIELD_DESCRIPTOR),
365    integer_capabilities(crate::builtins::structs::core::setfield::SETFIELD_INTEGER_CAPABILITIES),
366    builtin_path = "crate::builtins::structs::core::setfield"
367)]
368async fn setfield_builtin(base: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
369    let parsed = parse_arguments(rest)?;
370    let ParsedArguments {
371        leading_index,
372        steps,
373        value,
374    } = parsed;
375    assign_value(base, leading_index, steps, value).await
376}
377
378struct ParsedArguments {
379    leading_index: Option<IndexSelector>,
380    steps: Vec<FieldStep>,
381    value: Value,
382}
383
384struct FieldStep {
385    name: String,
386    index: Option<IndexSelector>,
387}
388
389#[derive(Clone)]
390struct IndexSelector {
391    components: Vec<IndexComponent>,
392}
393
394#[derive(Clone)]
395enum IndexComponent {
396    Scalar(usize),
397    End,
398}
399
400fn parse_arguments(mut rest: Vec<Value>) -> BuiltinResult<ParsedArguments> {
401    if rest.len() < 2 {
402        return Err(setfield_flow(SETFIELD_ERROR_NOT_ENOUGH_INPUTS.message));
403    }
404
405    let value = rest
406        .pop()
407        .expect("rest contains at least two elements after early return");
408
409    let mut parsed = ParsedArguments {
410        leading_index: None,
411        steps: Vec::new(),
412        value,
413    };
414
415    if let Some(first) = rest.first() {
416        if is_index_selector(first) {
417            let selector = rest.remove(0);
418            parsed.leading_index = Some(parse_index_selector(selector)?);
419        }
420    }
421
422    if rest.is_empty() {
423        return Err(setfield_flow(SETFIELD_ERROR_FIELD_EXPECTED.message));
424    }
425
426    let mut iter = rest.into_iter().peekable();
427    while let Some(arg) = iter.next() {
428        let name = parse_field_name(arg)?;
429        let mut step = FieldStep { name, index: None };
430        if let Some(next) = iter.peek() {
431            if is_index_selector(next) {
432                let selector = iter.next().unwrap();
433                step.index = Some(parse_index_selector(selector)?);
434            }
435        }
436        parsed.steps.push(step);
437    }
438
439    if parsed.steps.is_empty() {
440        return Err(setfield_flow(SETFIELD_ERROR_FIELD_EXPECTED.message));
441    }
442
443    Ok(parsed)
444}
445
446async fn assign_value(
447    base: Value,
448    leading_index: Option<IndexSelector>,
449    steps: Vec<FieldStep>,
450    rhs: Value,
451) -> BuiltinResult<Value> {
452    if steps.is_empty() {
453        return Err(setfield_flow(SETFIELD_ERROR_FIELD_EXPECTED.message));
454    }
455    if let Some(selector) = leading_index {
456        assign_with_leading_index(base, &selector, &steps, rhs).await
457    } else {
458        assign_without_leading_index(base, &steps, rhs).await
459    }
460}
461
462async fn assign_with_leading_index(
463    base: Value,
464    selector: &IndexSelector,
465    steps: &[FieldStep],
466    rhs: Value,
467) -> BuiltinResult<Value> {
468    match base {
469        Value::Cell(cell) => assign_into_struct_array(cell, selector, steps, rhs).await,
470        other => Err(setfield_flow(format!(
471            "setfield: leading indices require a struct array, got {other:?}"
472        ))),
473    }
474}
475
476async fn assign_without_leading_index(
477    base: Value,
478    steps: &[FieldStep],
479    rhs: Value,
480) -> BuiltinResult<Value> {
481    match base {
482        Value::Struct(struct_value) => assign_into_struct(struct_value, steps, rhs).await,
483        Value::Object(object) => assign_into_object(object, steps, rhs).await,
484        Value::Cell(cell) if is_struct_array(&cell) => {
485            if cell.data.is_empty() {
486                Err(setfield_flow(
487                    "setfield: struct array is empty; supply indices in a cell array",
488                ))
489            } else {
490                let selector = IndexSelector {
491                    components: vec![IndexComponent::Scalar(1)],
492                };
493                assign_into_struct_array(cell, &selector, steps, rhs).await
494            }
495        }
496        Value::HandleObject(handle) => assign_into_handle(handle, steps, rhs).await,
497        Value::Listener(_) => Err(setfield_flow(
498            "setfield: listeners do not support direct field assignment",
499        )),
500        other => Err(setfield_flow(format!(
501            "setfield unsupported on this value for field '{}': {other:?}",
502            steps.first().map(|s| s.name.as_str()).unwrap_or_default()
503        ))),
504    }
505}
506
507async fn assign_into_struct_array(
508    mut cell: CellArray,
509    selector: &IndexSelector,
510    steps: &[FieldStep],
511    rhs: Value,
512) -> BuiltinResult<Value> {
513    if selector.components.is_empty() {
514        return Err(setfield_flow(
515            "setfield: index cell must contain at least one element",
516        ));
517    }
518
519    let resolved = resolve_indices(&Value::Cell(cell.clone()), selector)?;
520
521    let position = match resolved.len() {
522        1 => {
523            let idx = resolved[0];
524            if idx == 0 || idx > cell.data.len() {
525                return Err(setfield_flow(SETFIELD_ERROR_INDEX_OUT_OF_BOUNDS.message));
526            }
527            idx - 1
528        }
529        2 => {
530            let row = resolved[0];
531            let col = resolved[1];
532            if row == 0 || row > cell.rows || col == 0 || col > cell.cols {
533                return Err(setfield_flow(SETFIELD_ERROR_INDEX_OUT_OF_BOUNDS.message));
534            }
535            (row - 1) * cell.cols + (col - 1)
536        }
537        _ => {
538            return Err(setfield_flow(
539                "setfield: indexing with more than two indices is not supported yet",
540            ));
541        }
542    };
543
544    let handle = cell
545        .data
546        .get(position)
547        .ok_or_else(|| setfield_flow(SETFIELD_ERROR_INDEX_OUT_OF_BOUNDS.message))?
548        .clone();
549
550    let current = handle.clone();
551    let updated = assign_into_value(current, steps, rhs).await?;
552    cell.data[position] = updated;
553    Ok(Value::Cell(cell))
554}
555
556#[async_recursion::async_recursion(?Send)]
557async fn assign_into_value(value: Value, steps: &[FieldStep], rhs: Value) -> BuiltinResult<Value> {
558    if steps.is_empty() {
559        return Ok(rhs);
560    }
561    match value {
562        Value::Struct(struct_value) => assign_into_struct(struct_value, steps, rhs).await,
563        Value::Object(object) => assign_into_object(object, steps, rhs).await,
564        Value::Cell(cell) => assign_into_cell(cell, steps, rhs).await,
565        Value::HandleObject(handle) => assign_into_handle(handle, steps, rhs).await,
566        Value::Listener(_) => Err(setfield_flow(
567            "setfield: listeners do not support nested field assignment",
568        )),
569        other => Err(setfield_flow(format!(
570            "Struct contents assignment to a {other:?} object is not supported."
571        ))),
572    }
573}
574
575#[async_recursion::async_recursion(?Send)]
576async fn assign_into_struct(
577    mut struct_value: StructValue,
578    steps: &[FieldStep],
579    rhs: Value,
580) -> BuiltinResult<Value> {
581    let (first, rest) = steps
582        .split_first()
583        .expect("steps is non-empty when assign_into_struct is called");
584
585    if rest.is_empty() {
586        if let Some(selector) = &first.index {
587            let current = struct_value
588                .fields
589                .get(&first.name)
590                .cloned()
591                .ok_or_else(|| format!("Reference to non-existent field '{}'.", first.name))?;
592            let updated = assign_with_selector(current, selector, &[], rhs).await?;
593            struct_value.fields.insert(first.name.clone(), updated);
594        } else {
595            struct_value.fields.insert(first.name.clone(), rhs);
596        }
597        return Ok(Value::Struct(struct_value));
598    }
599
600    if let Some(selector) = &first.index {
601        let current = struct_value
602            .fields
603            .get(&first.name)
604            .cloned()
605            .ok_or_else(|| format!("Reference to non-existent field '{}'.", first.name))?;
606        let updated = assign_with_selector(current, selector, rest, rhs).await?;
607        struct_value.fields.insert(first.name.clone(), updated);
608        return Ok(Value::Struct(struct_value));
609    }
610
611    let current = struct_value
612        .fields
613        .get(&first.name)
614        .cloned()
615        .unwrap_or_else(|| Value::Struct(StructValue::new()));
616    let updated = assign_into_value(current, rest, rhs).await?;
617    struct_value.fields.insert(first.name.clone(), updated);
618    Ok(Value::Struct(struct_value))
619}
620
621async fn assign_into_object(
622    mut object: ObjectInstance,
623    steps: &[FieldStep],
624    rhs: Value,
625) -> BuiltinResult<Value> {
626    let (first, rest) = steps
627        .split_first()
628        .expect("steps is non-empty when assign_into_object is called");
629
630    if first.index.is_some() {
631        return Err(setfield_flow(
632            "setfield: indexing into object properties is not currently supported",
633        ));
634    }
635
636    if rest.is_empty() {
637        write_object_property(&mut object, &first.name, rhs).await?;
638        return Ok(Value::Object(object));
639    }
640
641    let current = read_object_property(&object, &first.name).await?;
642    let updated = assign_into_value(current, rest, rhs).await?;
643    write_object_property(&mut object, &first.name, updated).await?;
644    Ok(Value::Object(object))
645}
646
647async fn assign_into_cell(
648    cell: CellArray,
649    steps: &[FieldStep],
650    rhs: Value,
651) -> BuiltinResult<Value> {
652    let (first, rest) = steps
653        .split_first()
654        .expect("steps is non-empty when assign_into_cell is called");
655
656    let selector = first.index.as_ref().ok_or_else(|| {
657        setfield_flow("setfield: cell array assignments require indices in a cell array")
658    })?;
659    if rest.is_empty() {
660        assign_with_selector(Value::Cell(cell), selector, &[], rhs).await
661    } else {
662        assign_with_selector(Value::Cell(cell), selector, rest, rhs).await
663    }
664}
665
666#[async_recursion::async_recursion(?Send)]
667async fn assign_with_selector(
668    value: Value,
669    selector: &IndexSelector,
670    rest: &[FieldStep],
671    rhs: Value,
672) -> BuiltinResult<Value> {
673    let host_value = gather_if_needed_async(&value)
674        .await
675        .map_err(|flow| remap_setfield_flow(flow, Some("setfield: ")))?;
676    match host_value {
677        Value::Cell(mut cell) => {
678            let resolved = resolve_indices(&Value::Cell(cell.clone()), selector)?;
679            let position = match resolved.len() {
680                1 => {
681                    let idx = resolved[0];
682                    if idx == 0 || idx > cell.data.len() {
683                        return Err(setfield_flow(SETFIELD_ERROR_INDEX_OUT_OF_BOUNDS.message));
684                    }
685                    idx - 1
686                }
687                2 => {
688                    let row = resolved[0];
689                    let col = resolved[1];
690                    if row == 0 || row > cell.rows || col == 0 || col > cell.cols {
691                        return Err(setfield_flow(SETFIELD_ERROR_INDEX_OUT_OF_BOUNDS.message));
692                    }
693                    (row - 1) * cell.cols + (col - 1)
694                }
695                _ => {
696                    return Err(setfield_flow(
697                        "setfield: indexing with more than two indices is not supported yet",
698                    ));
699                }
700            };
701
702            let handle = cell
703                .data
704                .get(position)
705                .ok_or_else(|| setfield_flow(SETFIELD_ERROR_INDEX_OUT_OF_BOUNDS.message))?
706                .clone();
707            let existing = handle.clone();
708            let new_value = if rest.is_empty() {
709                rhs
710            } else {
711                assign_into_value(existing, rest, rhs).await?
712            };
713            cell.data[position] = new_value;
714            Ok(Value::Cell(cell))
715        }
716        Value::Tensor(mut tensor) => {
717            if !rest.is_empty() {
718                return Err(setfield_flow(
719                    "setfield: cannot traverse deeper fields after indexing into a numeric tensor",
720                ));
721            }
722            assign_tensor_element(&mut tensor, selector, rhs)?;
723            Ok(Value::Tensor(tensor))
724        }
725        Value::LogicalArray(mut logical) => {
726            if !rest.is_empty() {
727                return Err(setfield_flow(
728                    "setfield: cannot traverse deeper fields after indexing into a logical array",
729                ));
730            }
731            assign_logical_element(&mut logical, selector, rhs)?;
732            Ok(Value::LogicalArray(logical))
733        }
734        Value::StringArray(mut sa) => {
735            if !rest.is_empty() {
736                return Err(setfield_flow(
737                    "setfield: cannot traverse deeper fields after indexing into a string array",
738                ));
739            }
740            assign_string_array_element(&mut sa, selector, rhs)?;
741            Ok(Value::StringArray(sa))
742        }
743        Value::CharArray(mut ca) => {
744            if !rest.is_empty() {
745                return Err(setfield_flow(
746                    "setfield: cannot traverse deeper fields after indexing into a char array",
747                ));
748            }
749            assign_char_array_element(&mut ca, selector, rhs)?;
750            Ok(Value::CharArray(ca))
751        }
752        Value::ComplexTensor(mut tensor) => {
753            if !rest.is_empty() {
754                return Err(setfield_flow(
755                    "setfield: cannot traverse deeper fields after indexing into a complex tensor",
756                ));
757            }
758            assign_complex_tensor_element(&mut tensor, selector, rhs)?;
759            Ok(Value::ComplexTensor(tensor))
760        }
761        other => Err(setfield_flow(format!(
762            "Struct contents assignment to a {other:?} object is not supported."
763        ))),
764    }
765}
766
767fn assign_tensor_element(
768    tensor: &mut Tensor,
769    selector: &IndexSelector,
770    rhs: Value,
771) -> BuiltinResult<()> {
772    let resolved = resolve_indices(&Value::Tensor(tensor.clone()), selector)?;
773    let value = value_to_numeric_scalar(rhs)?;
774    match resolved.len() {
775        1 => {
776            let idx = resolved[0];
777            if idx == 0 || idx > tensor.len() {
778                return Err(setfield_flow(SETFIELD_ERROR_INDEX_OUT_OF_BOUNDS.message));
779            }
780            tensor
781                .set_numeric_assignment_at(idx - 1, value)
782                .map_err(setfield_flow)
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(setfield_flow(SETFIELD_ERROR_INDEX_OUT_OF_BOUNDS.message));
789            }
790            let pos = (row - 1) + (col - 1) * tensor.rows();
791            tensor
792                .set_numeric_assignment_at(pos, value)
793                .map_err(setfield_flow)
794        }
795        _ => Err(setfield_flow(
796            "setfield: indexing with more than two indices is not supported yet",
797        )),
798    }
799}
800
801fn assign_logical_element(
802    logical: &mut LogicalArray,
803    selector: &IndexSelector,
804    rhs: Value,
805) -> BuiltinResult<()> {
806    let resolved = resolve_indices(&Value::LogicalArray(logical.clone()), selector)?;
807    let value = value_to_bool(rhs)?;
808    match resolved.len() {
809        1 => {
810            let idx = resolved[0];
811            if idx == 0 || idx > logical.data.len() {
812                return Err(setfield_flow(SETFIELD_ERROR_INDEX_OUT_OF_BOUNDS.message));
813            }
814            logical.data[idx - 1] = if value { 1 } else { 0 };
815            Ok(())
816        }
817        2 => {
818            if logical.shape.len() < 2 {
819                return Err(setfield_flow(SETFIELD_ERROR_INDEX_OUT_OF_BOUNDS.message));
820            }
821            let row = resolved[0];
822            let col = resolved[1];
823            let rows = logical.shape[0];
824            let cols = logical.shape[1];
825            if row == 0 || row > rows || col == 0 || col > cols {
826                return Err(setfield_flow(SETFIELD_ERROR_INDEX_OUT_OF_BOUNDS.message));
827            }
828            let pos = (row - 1) + (col - 1) * rows;
829            if pos >= logical.data.len() {
830                return Err(setfield_flow(SETFIELD_ERROR_INDEX_OUT_OF_BOUNDS.message));
831            }
832            logical.data[pos] = if value { 1 } else { 0 };
833            Ok(())
834        }
835        _ => Err(setfield_flow(
836            "setfield: indexing with more than two indices is not supported yet",
837        )),
838    }
839}
840
841fn assign_string_array_element(
842    array: &mut runmat_value::StringArray,
843    selector: &IndexSelector,
844    rhs: Value,
845) -> BuiltinResult<()> {
846    let resolved = resolve_indices(&Value::StringArray(array.clone()), selector)?;
847    let text = String::try_from(&rhs).map_err(|_| {
848        setfield_flow("setfield: string assignments require text-compatible values")
849    })?;
850    match resolved.len() {
851        1 => {
852            let idx = resolved[0];
853            if idx == 0 || idx > array.data.len() {
854                return Err(setfield_flow(SETFIELD_ERROR_INDEX_OUT_OF_BOUNDS.message));
855            }
856            array.data[idx - 1] = text;
857            Ok(())
858        }
859        2 => {
860            let row = resolved[0];
861            let col = resolved[1];
862            if row == 0 || row > array.rows || col == 0 || col > array.cols {
863                return Err(setfield_flow(SETFIELD_ERROR_INDEX_OUT_OF_BOUNDS.message));
864            }
865            let pos = (row - 1) + (col - 1) * array.rows;
866            if pos >= array.data.len() {
867                return Err(setfield_flow(SETFIELD_ERROR_INDEX_OUT_OF_BOUNDS.message));
868            }
869            array.data[pos] = text;
870            Ok(())
871        }
872        _ => Err(setfield_flow(
873            "setfield: indexing with more than two indices is not supported yet",
874        )),
875    }
876}
877
878fn assign_char_array_element(
879    array: &mut CharArray,
880    selector: &IndexSelector,
881    rhs: Value,
882) -> BuiltinResult<()> {
883    let resolved = resolve_indices(&Value::CharArray(array.clone()), selector)?;
884    let text = String::try_from(&rhs)
885        .map_err(|_| setfield_flow("setfield: char assignments require text-compatible values"))?;
886    if text.chars().count() != 1 {
887        return Err(setfield_flow(
888            "setfield: char array assignments require single characters",
889        ));
890    }
891    let ch = text.chars().next().unwrap();
892    match resolved.len() {
893        1 => {
894            let idx = resolved[0];
895            if idx == 0 || idx > array.data.len() {
896                return Err(setfield_flow(SETFIELD_ERROR_INDEX_OUT_OF_BOUNDS.message));
897            }
898            array.data[idx - 1] = ch;
899            Ok(())
900        }
901        2 => {
902            let row = resolved[0];
903            let col = resolved[1];
904            if row == 0 || row > array.rows || col == 0 || col > array.cols {
905                return Err(setfield_flow(SETFIELD_ERROR_INDEX_OUT_OF_BOUNDS.message));
906            }
907            let pos = (row - 1) * array.cols + (col - 1);
908            if pos >= array.data.len() {
909                return Err(setfield_flow(SETFIELD_ERROR_INDEX_OUT_OF_BOUNDS.message));
910            }
911            array.data[pos] = ch;
912            Ok(())
913        }
914        _ => Err(setfield_flow(
915            "setfield: indexing with more than two indices is not supported yet",
916        )),
917    }
918}
919
920fn assign_complex_tensor_element(
921    tensor: &mut ComplexTensor,
922    selector: &IndexSelector,
923    rhs: Value,
924) -> BuiltinResult<()> {
925    let resolved = resolve_indices(&Value::ComplexTensor(tensor.clone()), selector)?;
926    let (re, im) = match rhs {
927        Value::Complex(r, i) => (r, i),
928        Value::Num(n) => (n, 0.0),
929        Value::Int(i) => (i.to_f64(), 0.0),
930        other => {
931            return Err(setfield_flow(format!(
932                "setfield: cannot assign {other:?} into a complex tensor element"
933            )));
934        }
935    };
936    match resolved.len() {
937        1 => {
938            let idx = resolved[0];
939            if idx == 0 || idx > tensor.len() {
940                return Err(setfield_flow(SETFIELD_ERROR_INDEX_OUT_OF_BOUNDS.message));
941            }
942            tensor
943                .set_f64_assignment_at(idx - 1, re, im)
944                .map_err(setfield_flow)?;
945            Ok(())
946        }
947        2 => {
948            let row = resolved[0];
949            let col = resolved[1];
950            if row == 0 || row > tensor.rows || col == 0 || col > tensor.cols {
951                return Err(setfield_flow(SETFIELD_ERROR_INDEX_OUT_OF_BOUNDS.message));
952            }
953            let pos = (row - 1) + (col - 1) * tensor.rows;
954            if pos >= tensor.len() {
955                return Err(setfield_flow(SETFIELD_ERROR_INDEX_OUT_OF_BOUNDS.message));
956            }
957            tensor
958                .set_f64_assignment_at(pos, re, im)
959                .map_err(setfield_flow)?;
960            Ok(())
961        }
962        _ => Err(setfield_flow(
963            "setfield: indexing with more than two indices is not supported yet",
964        )),
965    }
966}
967
968async fn read_object_property(obj: &ObjectInstance, name: &str) -> BuiltinResult<Value> {
969    if let Some((prop, _owner)) = crate::class_registry::lookup_property(&obj.class_name, name) {
970        if prop.is_static {
971            return Err(setfield_flow(format!(
972                "You cannot access the static property '{}' through an instance of class '{}'.",
973                name, obj.class_name
974            )));
975        }
976        if prop.get_access == MemberAccess::Private {
977            return Err(setfield_private_access(format!(
978                "You cannot get the '{}' property of '{}' class.",
979                name, obj.class_name
980            )));
981        }
982        if prop.is_dependent {
983            let getter = object_property_getter_name(name);
984            match call_builtin_async(&getter, &[Value::Object(obj.clone())]).await {
985                Ok(value) => return Ok(value),
986                Err(err) => {
987                    if !is_undefined_function(&err) {
988                        return Err(remap_setfield_flow(err, None));
989                    }
990                }
991            }
992            if let Some(value) = obj.properties.get(&format!("{name}_backing")) {
993                return Ok(value.clone());
994            }
995        }
996    }
997
998    if let Some(value) = obj.properties.get(name) {
999        return Ok(value.clone());
1000    }
1001
1002    if let Some((prop, _owner)) = crate::class_registry::lookup_property(&obj.class_name, name) {
1003        if prop.get_access == MemberAccess::Private {
1004            return Err(setfield_private_access(format!(
1005                "You cannot get the '{}' property of '{}' class.",
1006                name, obj.class_name
1007            )));
1008        }
1009        return Err(setfield_flow(format!(
1010            "No public property '{}' for class '{}'.",
1011            name, obj.class_name
1012        )));
1013    }
1014
1015    Err(setfield_flow(format!(
1016        "Undefined property '{}' for class {}",
1017        name, obj.class_name
1018    )))
1019}
1020
1021async fn write_object_property(
1022    obj: &mut ObjectInstance,
1023    name: &str,
1024    rhs: Value,
1025) -> BuiltinResult<()> {
1026    if dynamicprops::metadata_assignment(obj, name, rhs.clone())? {
1027        return Ok(());
1028    }
1029
1030    if let Some((prop, _owner)) = crate::class_registry::lookup_property(&obj.class_name, name) {
1031        if prop.is_static {
1032            return Err(setfield_static_access(format!(
1033                "Property '{}' is static; use classref('{}').{}",
1034                name, obj.class_name, name
1035            )));
1036        }
1037        if prop.set_access == MemberAccess::Private {
1038            return Err(setfield_private_access(format!(
1039                "Property '{name}' is private"
1040            )));
1041        }
1042        if prop.is_dependent {
1043            let setter = object_property_setter_name(name);
1044            match call_builtin_async(&setter, &[Value::Object(obj.clone()), rhs.clone()]).await {
1045                Ok(value) => {
1046                    if let Value::Object(updated) = value {
1047                        *obj = updated;
1048                        return Ok(());
1049                    }
1050                    return Err(setfield_flow(format!(
1051                        "Dependent property setter for '{}' must return the updated object",
1052                        name
1053                    )));
1054                }
1055                Err(err) => {
1056                    if !is_undefined_function(&err) {
1057                        return Err(remap_setfield_flow(err, None));
1058                    }
1059                }
1060            }
1061            obj.properties.insert(format!("{name}_backing"), rhs);
1062            return Ok(());
1063        }
1064    }
1065
1066    if dynamicprops::dynamic_property_assign(obj, name, rhs.clone())? {
1067        return Ok(());
1068    }
1069
1070    obj.properties.insert(name.to_string(), rhs);
1071    Ok(())
1072}
1073
1074async fn assign_into_handle(
1075    handle: HandleRef,
1076    steps: &[FieldStep],
1077    rhs: Value,
1078) -> BuiltinResult<Value> {
1079    if steps.is_empty() {
1080        return Err(setfield_flow(
1081            "setfield: expected at least one field name when assigning into a handle",
1082        ));
1083    }
1084    if !crate::is_handle_valid(&handle) {
1085        return Err(setfield_flow(format!(
1086            "Invalid or deleted handle object '{}'.",
1087            handle.class_name
1088        )));
1089    }
1090    let current = runmat_gc::gc_clone_value(&handle.target)
1091        .map_err(|e| setfield_flow(format!("setfield: invalid handle target: {e}")))?;
1092    let updated = assign_into_value(current.clone(), steps, rhs).await?;
1093    runmat_gc::gc_with_value_mut(&handle.target, |target| -> BuiltinResult<()> {
1094        let target_valid = match target {
1095            Value::Object(obj) => !matches!(
1096                obj.properties.get(crate::HANDLE_VALID_FLAG_PROPERTY),
1097                Some(Value::Bool(false))
1098            ),
1099            _ => {
1100                return Err(setfield_flow(format!(
1101                    "Invalid or deleted handle object '{}'.",
1102                    handle.class_name
1103                )));
1104            }
1105        };
1106        if !target_valid {
1107            return Err(setfield_flow(format!(
1108                "Invalid or deleted handle object '{}'.",
1109                handle.class_name
1110            )));
1111        }
1112        if *target != current {
1113            return Err(setfield_flow(
1114                "setfield: handle target changed during asynchronous assignment",
1115            ));
1116        }
1117        runmat_gc::gc_record_handle_write(&handle.target, &updated);
1118        *target = updated;
1119        Ok(())
1120    })
1121    .map_err(|e| setfield_flow(format!("setfield: invalid handle target: {e}")))??;
1122    Ok(Value::HandleObject(handle))
1123}
1124
1125fn is_index_selector(value: &Value) -> bool {
1126    matches!(value, Value::Cell(_))
1127}
1128
1129fn parse_index_selector(value: Value) -> BuiltinResult<IndexSelector> {
1130    let Value::Cell(cell) = value else {
1131        return Err(setfield_flow(SETFIELD_ERROR_INDEX_SELECTOR_TYPE.message));
1132    };
1133    let mut components = Vec::with_capacity(cell.data.len());
1134    for handle in &cell.data {
1135        let entry = handle;
1136        components.push(parse_index_component(entry)?);
1137    }
1138    Ok(IndexSelector { components })
1139}
1140
1141fn parse_index_component(value: &Value) -> BuiltinResult<IndexComponent> {
1142    match value {
1143        Value::CharArray(ca) => {
1144            let text: String = ca.data.iter().collect();
1145            parse_index_text(text.trim())
1146        }
1147        Value::String(s) => parse_index_text(s.trim()),
1148        Value::StringArray(sa) if sa.data.len() == 1 => parse_index_text(sa.data[0].trim()),
1149        _ => {
1150            let idx = parse_positive_scalar(value).map_err(|err| {
1151                setfield_flow(format!(
1152                    "setfield: invalid index element ({})",
1153                    err.message()
1154                ))
1155            })?;
1156            Ok(IndexComponent::Scalar(idx))
1157        }
1158    }
1159}
1160
1161fn parse_index_text(text: &str) -> BuiltinResult<IndexComponent> {
1162    if text.eq_ignore_ascii_case("end") {
1163        return Ok(IndexComponent::End);
1164    }
1165    if text == ":" {
1166        return Err(setfield_flow(
1167            "setfield: ':' indexing is not currently supported",
1168        ));
1169    }
1170    if text.is_empty() {
1171        return Err(setfield_flow("setfield: index elements must not be empty"));
1172    }
1173    if let Ok(value) = text.parse::<usize>() {
1174        if value == 0 {
1175            return Err(setfield_flow("setfield: index must be >= 1"));
1176        }
1177        return Ok(IndexComponent::Scalar(value));
1178    }
1179    Err(setfield_flow(format!(
1180        "setfield: invalid index element '{}'",
1181        text
1182    )))
1183}
1184
1185fn parse_positive_scalar(value: &Value) -> BuiltinResult<usize> {
1186    if let Value::Int(i) = value {
1187        return i
1188            .try_to_usize()
1189            .filter(|index| *index >= 1)
1190            .ok_or_else(|| setfield_flow("index must be >= 1"));
1191    }
1192    if let Value::Tensor(t) = value {
1193        if tensor::is_scalar_tensor(t) {
1194            if let Some(storage) = t.integer_storage() {
1195                return storage
1196                    .value_at(0)
1197                    .and_then(|value| value.try_to_usize())
1198                    .filter(|index| *index >= 1)
1199                    .ok_or_else(|| setfield_flow("index must be >= 1"));
1200            }
1201        }
1202    }
1203    let number = match value {
1204        Value::Num(n) => *n,
1205        Value::Tensor(t) if tensor::is_scalar_tensor(t) => tensor::tensor_value_f64(t, 0),
1206        _ => {
1207            let repr = format!("{value:?}");
1208            return Err(setfield_flow(format!(
1209                "expected positive integer index, got {repr}"
1210            )));
1211        }
1212    };
1213
1214    if !number.is_finite() {
1215        return Err(setfield_flow("index must be a finite number"));
1216    }
1217    if number.fract() != 0.0 {
1218        return Err(setfield_flow("index must be an integer"));
1219    }
1220    if number <= 0.0 {
1221        return Err(setfield_flow("index must be >= 1"));
1222    }
1223    if number > usize::MAX as f64 || (usize::BITS == 64 && number == usize::MAX as f64) {
1224        return Err(setfield_flow("index exceeds platform limits"));
1225    }
1226    Ok(number as usize)
1227}
1228
1229fn parse_field_name(value: Value) -> BuiltinResult<String> {
1230    match value {
1231        Value::String(s) => Ok(s),
1232        Value::StringArray(sa) => {
1233            if sa.data.len() == 1 {
1234                Ok(sa.data[0].clone())
1235            } else {
1236                Err(setfield_flow(
1237                    "setfield: field names must be scalar string arrays or character vectors",
1238                ))
1239            }
1240        }
1241        Value::CharArray(ca) => {
1242            if ca.rows == 1 {
1243                Ok(ca.data.iter().collect())
1244            } else {
1245                Err(setfield_flow(
1246                    "setfield: field names must be 1-by-N character vectors",
1247                ))
1248            }
1249        }
1250        other => Err(setfield_flow(format!(
1251            "setfield: expected field name, got {other:?}"
1252        ))),
1253    }
1254}
1255
1256fn resolve_indices(value: &Value, selector: &IndexSelector) -> BuiltinResult<Vec<usize>> {
1257    let dims = selector.components.len();
1258    let mut resolved = Vec::with_capacity(dims);
1259    for (dim_idx, component) in selector.components.iter().enumerate() {
1260        let index = match component {
1261            IndexComponent::Scalar(idx) => *idx,
1262            IndexComponent::End => dimension_length(value, dims, dim_idx)?,
1263        };
1264        resolved.push(index);
1265    }
1266    Ok(resolved)
1267}
1268
1269fn dimension_length(value: &Value, dims: usize, dim_idx: usize) -> BuiltinResult<usize> {
1270    match value {
1271        Value::Tensor(tensor) => tensor_dimension_length(tensor, dims, dim_idx),
1272        Value::Cell(cell) => cell_dimension_length(cell, dims, dim_idx),
1273        Value::StringArray(array) => string_array_dimension_length(array, dims, dim_idx),
1274        Value::LogicalArray(logical) => logical_array_dimension_length(logical, dims, dim_idx),
1275        Value::CharArray(array) => char_array_dimension_length(array, dims, dim_idx),
1276        Value::ComplexTensor(tensor) => complex_tensor_dimension_length(tensor, dims, dim_idx),
1277        Value::Num(_) | Value::Int(_) | Value::Bool(_) => {
1278            if dims == 1 {
1279                Ok(1)
1280            } else {
1281                Err(setfield_flow(
1282                    "setfield: indexing with more than one dimension is not supported for scalars",
1283                ))
1284            }
1285        }
1286        other => Err(setfield_flow(format!(
1287            "Struct contents assignment to a {other:?} object is not supported."
1288        ))),
1289    }
1290}
1291
1292fn tensor_dimension_length(tensor: &Tensor, dims: usize, dim_idx: usize) -> BuiltinResult<usize> {
1293    if dims == 1 {
1294        let total = tensor.len();
1295        if total == 0 {
1296            return Err(setfield_flow(
1297                "Index exceeds the number of array elements (0).",
1298            ));
1299        }
1300        return Ok(total);
1301    }
1302    if dims > 2 {
1303        return Err(setfield_flow(
1304            "setfield: indexing with more than two indices is not supported yet",
1305        ));
1306    }
1307    let len = if dim_idx == 0 {
1308        tensor.rows()
1309    } else {
1310        tensor.cols()
1311    };
1312    if len == 0 {
1313        return Err(setfield_flow(
1314            "Index exceeds the number of array elements (0).",
1315        ));
1316    }
1317    Ok(len)
1318}
1319
1320fn cell_dimension_length(cell: &CellArray, dims: usize, dim_idx: usize) -> BuiltinResult<usize> {
1321    if dims == 1 {
1322        let total = cell.data.len();
1323        if total == 0 {
1324            return Err(setfield_flow(
1325                "Index exceeds the number of array elements (0).",
1326            ));
1327        }
1328        return Ok(total);
1329    }
1330    if dims > 2 {
1331        return Err(setfield_flow(
1332            "setfield: indexing with more than two indices is not supported yet",
1333        ));
1334    }
1335    let len = if dim_idx == 0 { cell.rows } else { cell.cols };
1336    if len == 0 {
1337        return Err(setfield_flow(
1338            "Index exceeds the number of array elements (0).",
1339        ));
1340    }
1341    Ok(len)
1342}
1343
1344fn string_array_dimension_length(
1345    array: &runmat_value::StringArray,
1346    dims: usize,
1347    dim_idx: usize,
1348) -> BuiltinResult<usize> {
1349    if dims == 1 {
1350        let total = array.data.len();
1351        if total == 0 {
1352            return Err(setfield_flow(
1353                "Index exceeds the number of array elements (0).",
1354            ));
1355        }
1356        return Ok(total);
1357    }
1358    if dims > 2 {
1359        return Err(setfield_flow(
1360            "setfield: indexing with more than two indices is not supported yet",
1361        ));
1362    }
1363    let len = if dim_idx == 0 { array.rows } else { array.cols };
1364    if len == 0 {
1365        return Err(setfield_flow(
1366            "Index exceeds the number of array elements (0).",
1367        ));
1368    }
1369    Ok(len)
1370}
1371
1372fn logical_array_dimension_length(
1373    array: &LogicalArray,
1374    dims: usize,
1375    dim_idx: usize,
1376) -> BuiltinResult<usize> {
1377    if dims == 1 {
1378        let total = array.data.len();
1379        if total == 0 {
1380            return Err(setfield_flow(
1381                "Index exceeds the number of array elements (0).",
1382            ));
1383        }
1384        return Ok(total);
1385    }
1386    if dims > 2 {
1387        return Err(setfield_flow(
1388            "setfield: indexing with more than two indices is not supported yet",
1389        ));
1390    }
1391    if array.shape.len() < dims {
1392        return Err(setfield_flow(
1393            "Index exceeds the number of array elements (0).",
1394        ));
1395    }
1396    let len = array.shape[dim_idx];
1397    if len == 0 {
1398        return Err(setfield_flow(
1399            "Index exceeds the number of array elements (0).",
1400        ));
1401    }
1402    Ok(len)
1403}
1404
1405fn char_array_dimension_length(
1406    array: &CharArray,
1407    dims: usize,
1408    dim_idx: usize,
1409) -> BuiltinResult<usize> {
1410    if dims == 1 {
1411        let total = array.data.len();
1412        if total == 0 {
1413            return Err(setfield_flow(
1414                "Index exceeds the number of array elements (0).",
1415            ));
1416        }
1417        return Ok(total);
1418    }
1419    if dims > 2 {
1420        return Err(setfield_flow(
1421            "setfield: indexing with more than two indices is not supported yet",
1422        ));
1423    }
1424    let len = if dim_idx == 0 { array.rows } else { array.cols };
1425    if len == 0 {
1426        return Err(setfield_flow(
1427            "Index exceeds the number of array elements (0).",
1428        ));
1429    }
1430    Ok(len)
1431}
1432
1433fn complex_tensor_dimension_length(
1434    tensor: &ComplexTensor,
1435    dims: usize,
1436    dim_idx: usize,
1437) -> BuiltinResult<usize> {
1438    if dims == 1 {
1439        let total = tensor.materialize_f64().len();
1440        if total == 0 {
1441            return Err(setfield_flow(
1442                "Index exceeds the number of array elements (0).",
1443            ));
1444        }
1445        return Ok(total);
1446    }
1447    if dims > 2 {
1448        return Err(setfield_flow(
1449            "setfield: indexing with more than two indices is not supported yet",
1450        ));
1451    }
1452    let len = if dim_idx == 0 {
1453        tensor.rows
1454    } else {
1455        tensor.cols
1456    };
1457    if len == 0 {
1458        return Err(setfield_flow(
1459            "Index exceeds the number of array elements (0).",
1460        ));
1461    }
1462    Ok(len)
1463}
1464
1465fn value_to_numeric_scalar(value: Value) -> BuiltinResult<NumericScalar> {
1466    match value {
1467        Value::Num(n) => Ok(NumericScalar::F64(n)),
1468        Value::Int(i) => Ok(NumericScalar::from(i)),
1469        Value::Bool(b) => Ok(NumericScalar::F64(if b { 1.0 } else { 0.0 })),
1470        Value::Tensor(t) if tensor::is_scalar_tensor(&t) => t
1471            .numeric_value_at(0)
1472            .ok_or_else(|| setfield_flow("setfield: invalid numeric tensor storage")),
1473        other => Err(setfield_flow(format!(
1474            "setfield: cannot assign {other:?} into a numeric tensor element"
1475        ))),
1476    }
1477}
1478
1479fn value_to_bool(value: Value) -> BuiltinResult<bool> {
1480    match value {
1481        Value::Bool(b) => Ok(b),
1482        Value::Num(n) => Ok(n != 0.0),
1483        Value::Int(i) => Ok(!i.is_zero()),
1484        Value::Tensor(t) if tensor::is_scalar_tensor(&t) => {
1485            if let Some(storage) = t.integer_storage() {
1486                Ok(!storage
1487                    .value_at(0)
1488                    .ok_or_else(|| setfield_flow("setfield: invalid integer tensor storage"))?
1489                    .is_zero())
1490            } else {
1491                Ok(tensor::tensor_value_f64(&t, 0) != 0.0)
1492            }
1493        }
1494        other => Err(setfield_flow(format!(
1495            "setfield: cannot assign {other:?} into a logical array element"
1496        ))),
1497    }
1498}
1499
1500fn is_struct_array(cell: &CellArray) -> bool {
1501    cell.data
1502        .iter()
1503        .all(|handle| matches!(handle, Value::Struct(_)))
1504}
1505
1506#[cfg(test)]
1507pub(crate) mod tests {
1508    use super::*;
1509    use runmat_gc::gc_allocate;
1510    use runmat_value::{
1511        CellArray, HandleRef, IntValue, IntegerStorage, LogicalArray, ObjectInstance, StructValue,
1512    };
1513
1514    fn error_message(err: crate::RuntimeError) -> String {
1515        err.message().to_string()
1516    }
1517
1518    fn run_setfield(base: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
1519        futures::executor::block_on(setfield_builtin(base, rest))
1520    }
1521
1522    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1523    #[test]
1524    fn setfield_creates_scalar_field() {
1525        let struct_value = StructValue::new();
1526        let updated = run_setfield(
1527            Value::Struct(struct_value),
1528            vec![Value::from("answer"), Value::Num(42.0)],
1529        )
1530        .expect("setfield");
1531        match updated {
1532            Value::Struct(st) => {
1533                assert_eq!(
1534                    st.fields.get("answer"),
1535                    Some(&Value::Num(42.0)),
1536                    "field should be inserted"
1537                );
1538            }
1539            other => panic!("expected struct result, got {other:?}"),
1540        }
1541    }
1542
1543    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1544    #[test]
1545    fn setfield_creates_nested_structs() {
1546        let struct_value = StructValue::new();
1547        let updated = run_setfield(
1548            Value::Struct(struct_value),
1549            vec![
1550                Value::from("solver"),
1551                Value::from("name"),
1552                Value::from("cg"),
1553            ],
1554        )
1555        .expect("setfield");
1556        match updated {
1557            Value::Struct(st) => {
1558                let solver = st.fields.get("solver").expect("solver field");
1559                match solver {
1560                    Value::Struct(inner) => {
1561                        assert_eq!(
1562                            inner.fields.get("name"),
1563                            Some(&Value::from("cg")),
1564                            "inner field should exist"
1565                        );
1566                    }
1567                    other => panic!("expected inner struct, got {other:?}"),
1568                }
1569            }
1570            other => panic!("expected struct result, got {other:?}"),
1571        }
1572    }
1573
1574    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1575    #[test]
1576    fn setfield_updates_struct_array_element() {
1577        let mut a = StructValue::new();
1578        a.fields
1579            .insert("id".to_string(), Value::Int(IntValue::I32(1)));
1580        let mut b = StructValue::new();
1581        b.fields
1582            .insert("id".to_string(), Value::Int(IntValue::I32(2)));
1583        let array = CellArray::new_with_shape(vec![Value::Struct(a), Value::Struct(b)], vec![1, 2])
1584            .unwrap();
1585        let indices =
1586            CellArray::new_with_shape(vec![Value::Int(IntValue::I32(2))], vec![1, 1]).unwrap();
1587        let updated = run_setfield(
1588            Value::Cell(array),
1589            vec![
1590                Value::Cell(indices),
1591                Value::from("id"),
1592                Value::Int(IntValue::I32(42)),
1593            ],
1594        )
1595        .expect("setfield");
1596        match updated {
1597            Value::Cell(cell) => {
1598                let second = &cell.data[1].clone();
1599                match second {
1600                    Value::Struct(st) => {
1601                        assert_eq!(st.fields.get("id"), Some(&Value::Int(IntValue::I32(42))));
1602                    }
1603                    other => panic!("expected struct element, got {other:?}"),
1604                }
1605            }
1606            other => panic!("expected cell array, got {other:?}"),
1607        }
1608    }
1609
1610    #[test]
1611    fn setfield_index_selector_reads_typed_integer_storage_exactly() {
1612        let mut a = StructValue::new();
1613        a.fields
1614            .insert("id".to_string(), Value::Int(IntValue::I32(1)));
1615        let mut b = StructValue::new();
1616        b.fields
1617            .insert("id".to_string(), Value::Int(IntValue::I32(2)));
1618        let array = CellArray::new_with_shape(vec![Value::Struct(a), Value::Struct(b)], vec![1, 2])
1619            .unwrap();
1620        let index_tensor =
1621            Tensor::new_integer(IntegerStorage::U64(vec![2]), vec![1, 1]).expect("index tensor");
1622        let indices = CellArray::new_with_shape(vec![Value::Tensor(index_tensor)], vec![1, 1])
1623            .expect("index cell");
1624
1625        let updated = run_setfield(
1626            Value::Cell(array),
1627            vec![
1628                Value::Cell(indices),
1629                Value::from("id"),
1630                Value::Int(IntValue::I32(42)),
1631            ],
1632        )
1633        .expect("setfield");
1634        match updated {
1635            Value::Cell(cell) => match &cell.data[1] {
1636                Value::Struct(st) => {
1637                    assert_eq!(st.fields.get("id"), Some(&Value::Int(IntValue::I32(42))));
1638                }
1639                other => panic!("expected struct element, got {other:?}"),
1640            },
1641            other => panic!("expected cell array, got {other:?}"),
1642        }
1643
1644        assert!(parse_positive_scalar(&Value::Num(usize::MAX as f64)).is_err());
1645        assert!(parse_positive_scalar(&Value::Num(usize::MAX as f64 + 1.0)).is_err());
1646    }
1647
1648    #[test]
1649    fn setfield_scalar_assignment_reads_typed_integer_storage_exactly() {
1650        let mut root = StructValue::new();
1651        root.fields.insert(
1652            "values".to_string(),
1653            Value::Tensor(Tensor::new(vec![10.0, 20.0], vec![1, 2]).unwrap()),
1654        );
1655        root.fields.insert(
1656            "mask".to_string(),
1657            Value::LogicalArray(LogicalArray::new(vec![0, 0], vec![1, 2]).unwrap()),
1658        );
1659
1660        let index_tensor =
1661            Tensor::new_integer(IntegerStorage::U64(vec![2]), vec![1, 1]).expect("index tensor");
1662        let index =
1663            CellArray::new_with_shape(vec![Value::Tensor(index_tensor)], vec![1, 1]).unwrap();
1664        let rhs =
1665            Tensor::new_integer(IntegerStorage::U64(vec![77]), vec![1, 1]).expect("rhs tensor");
1666        let updated = run_setfield(
1667            Value::Struct(root),
1668            vec![
1669                Value::from("values"),
1670                Value::Cell(index),
1671                Value::Tensor(rhs),
1672            ],
1673        )
1674        .expect("numeric setfield");
1675
1676        let logical_index =
1677            Tensor::new_integer(IntegerStorage::U64(vec![2]), vec![1, 1]).expect("logical index");
1678        let logical_index =
1679            CellArray::new_with_shape(vec![Value::Tensor(logical_index)], vec![1, 1]).unwrap();
1680        let logical_rhs =
1681            Tensor::new_integer(IntegerStorage::U64(vec![1]), vec![1, 1]).expect("logical rhs");
1682        let updated = run_setfield(
1683            updated,
1684            vec![
1685                Value::from("mask"),
1686                Value::Cell(logical_index),
1687                Value::Tensor(logical_rhs),
1688            ],
1689        )
1690        .expect("logical setfield");
1691
1692        match updated {
1693            Value::Struct(st) => {
1694                match st.fields.get("values").expect("values") {
1695                    Value::Tensor(tensor) => {
1696                        assert_eq!(tensor.materialize_f64(), vec![10.0, 77.0])
1697                    }
1698                    other => panic!("expected tensor field, got {other:?}"),
1699                }
1700                match st.fields.get("mask").expect("mask") {
1701                    Value::LogicalArray(array) => assert_eq!(array.data, vec![0, 1]),
1702                    other => panic!("expected logical field, got {other:?}"),
1703                }
1704            }
1705            other => panic!("expected struct, got {other:?}"),
1706        }
1707    }
1708
1709    #[test]
1710    fn setfield_preserves_exact_integer_destination_storage() {
1711        let large = 9_007_199_254_740_993_u64;
1712        let mut root = StructValue::new();
1713        root.fields.insert(
1714            "wide".to_string(),
1715            Value::Tensor(
1716                Tensor::new_integer(IntegerStorage::U64(vec![large, large + 1]), vec![1, 2])
1717                    .unwrap(),
1718            ),
1719        );
1720        let index =
1721            CellArray::new_with_shape(vec![Value::Int(IntValue::U8(2))], vec![1, 1]).unwrap();
1722        let updated = run_setfield(
1723            Value::Struct(root),
1724            vec![
1725                Value::from("wide"),
1726                Value::Cell(index),
1727                Value::Int(IntValue::U64(u64::MAX)),
1728            ],
1729        )
1730        .expect("exact integer setfield");
1731
1732        let Value::Struct(root) = updated else {
1733            panic!("expected struct");
1734        };
1735        let Value::Tensor(tensor) = root.fields.get("wide").expect("wide field") else {
1736            panic!("expected tensor");
1737        };
1738        assert_eq!(tensor.numeric_value_at(0), Some(NumericScalar::U64(large)));
1739        assert_eq!(
1740            tensor.numeric_value_at(1),
1741            Some(NumericScalar::U64(u64::MAX))
1742        );
1743    }
1744
1745    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1746    #[test]
1747    fn setfield_assigns_into_cell_then_struct() {
1748        let mut inner1 = StructValue::new();
1749        inner1.fields.insert("value".to_string(), Value::Num(1.0));
1750        let mut inner2 = StructValue::new();
1751        inner2.fields.insert("value".to_string(), Value::Num(2.0));
1752        let cell = CellArray::new_with_shape(
1753            vec![Value::Struct(inner1), Value::Struct(inner2)],
1754            vec![1, 2],
1755        )
1756        .unwrap();
1757        let mut root = StructValue::new();
1758        root.fields.insert("samples".to_string(), Value::Cell(cell));
1759
1760        let index_cell =
1761            CellArray::new_with_shape(vec![Value::Int(IntValue::I32(2))], vec![1, 1]).unwrap();
1762        let updated = run_setfield(
1763            Value::Struct(root),
1764            vec![
1765                Value::from("samples"),
1766                Value::Cell(index_cell),
1767                Value::from("value"),
1768                Value::Num(10.0),
1769            ],
1770        )
1771        .expect("setfield");
1772
1773        match updated {
1774            Value::Struct(st) => {
1775                let samples = st.fields.get("samples").expect("samples field");
1776                match samples {
1777                    Value::Cell(cell) => {
1778                        let value = &cell.data[1].clone();
1779                        match value {
1780                            Value::Struct(inner) => {
1781                                assert_eq!(inner.fields.get("value"), Some(&Value::Num(10.0)));
1782                            }
1783                            other => panic!("expected struct, got {other:?}"),
1784                        }
1785                    }
1786                    other => panic!("expected cell array, got {other:?}"),
1787                }
1788            }
1789            other => panic!("expected struct, got {other:?}"),
1790        }
1791    }
1792
1793    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1794    #[test]
1795    fn setfield_struct_array_with_end_index() {
1796        let mut first = StructValue::new();
1797        first
1798            .fields
1799            .insert("id".to_string(), Value::Int(IntValue::I32(1)));
1800        let mut second = StructValue::new();
1801        second
1802            .fields
1803            .insert("id".to_string(), Value::Int(IntValue::I32(2)));
1804        let array = CellArray::new_with_shape(
1805            vec![Value::Struct(first), Value::Struct(second)],
1806            vec![1, 2],
1807        )
1808        .unwrap();
1809        let index_cell = CellArray::new_with_shape(vec![Value::from("end")], vec![1, 1]).unwrap();
1810        let updated = run_setfield(
1811            Value::Cell(array),
1812            vec![
1813                Value::Cell(index_cell),
1814                Value::from("id"),
1815                Value::Int(IntValue::I32(99)),
1816            ],
1817        )
1818        .expect("setfield");
1819        match updated {
1820            Value::Cell(cell) => {
1821                let second = &cell.data[1].clone();
1822                match second {
1823                    Value::Struct(st) => {
1824                        assert_eq!(st.fields.get("id"), Some(&Value::Int(IntValue::I32(99))));
1825                    }
1826                    other => panic!("expected struct element, got {other:?}"),
1827                }
1828            }
1829            other => panic!("expected cell array result, got {other:?}"),
1830        }
1831    }
1832
1833    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1834    #[test]
1835    fn setfield_assigns_object_property() {
1836        let mut class_def = crate::class_registry::RuntimeClass {
1837            name: "Simple".to_string(),
1838            parent: None,
1839            properties: Default::default(),
1840            methods: Default::default(),
1841        };
1842        class_def.properties.insert(
1843            "x".to_string(),
1844            crate::class_registry::RuntimeProperty {
1845                name: "x".to_string(),
1846                is_static: false,
1847                is_constant: false,
1848                is_dependent: false,
1849                get_access: MemberAccess::Public,
1850                set_access: MemberAccess::Public,
1851                default_value: None,
1852            },
1853        );
1854        crate::class_registry::register_class(class_def);
1855
1856        let mut obj = ObjectInstance::new("Simple".to_string());
1857        obj.properties.insert("x".to_string(), Value::Num(0.0));
1858
1859        let updated = run_setfield(Value::Object(obj), vec![Value::from("x"), Value::Num(5.0)])
1860            .expect("setfield");
1861
1862        match updated {
1863            Value::Object(o) => {
1864                assert_eq!(o.properties.get("x"), Some(&Value::Num(5.0)));
1865            }
1866            other => panic!("expected object result, got {other:?}"),
1867        }
1868    }
1869
1870    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1871    #[test]
1872    fn setfield_errors_when_indexing_missing_field() {
1873        let struct_value = StructValue::new();
1874        let index_cell =
1875            CellArray::new_with_shape(vec![Value::Int(IntValue::I32(1))], vec![1, 1]).unwrap();
1876        let err = error_message(
1877            run_setfield(
1878                Value::Struct(struct_value),
1879                vec![
1880                    Value::from("missing"),
1881                    Value::Cell(index_cell),
1882                    Value::Num(1.0),
1883                ],
1884            )
1885            .expect_err("setfield should fail when field is missing"),
1886        );
1887        assert!(
1888            err.contains("Reference to non-existent field 'missing'."),
1889            "unexpected error message: {err}"
1890        );
1891    }
1892
1893    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1894    #[test]
1895    fn setfield_errors_on_static_property_assignment() {
1896        let mut class_def = crate::class_registry::RuntimeClass {
1897            name: "StaticSetfield".to_string(),
1898            parent: None,
1899            properties: Default::default(),
1900            methods: Default::default(),
1901        };
1902        class_def.properties.insert(
1903            "version".to_string(),
1904            crate::class_registry::RuntimeProperty {
1905                name: "version".to_string(),
1906                is_static: true,
1907                is_constant: false,
1908                is_dependent: false,
1909                get_access: MemberAccess::Public,
1910                set_access: MemberAccess::Public,
1911                default_value: None,
1912            },
1913        );
1914        crate::class_registry::register_class(class_def);
1915
1916        let obj = ObjectInstance::new("StaticSetfield".to_string());
1917        let err = error_message(
1918            run_setfield(
1919                Value::Object(obj),
1920                vec![Value::from("version"), Value::Num(2.0)],
1921            )
1922            .expect_err("setfield should reject static property writes"),
1923        );
1924        assert!(
1925            err.contains("Property 'version' is static"),
1926            "unexpected error message: {err}"
1927        );
1928    }
1929
1930    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1931    #[test]
1932    fn setfield_rejects_inherited_static_property_assignment() {
1933        let parent_name = "runmat.unittest.StaticSetfieldParent";
1934        let child_name = "runmat.unittest.StaticSetfieldChild";
1935
1936        let mut parent = crate::class_registry::RuntimeClass {
1937            name: parent_name.to_string(),
1938            parent: None,
1939            properties: Default::default(),
1940            methods: Default::default(),
1941        };
1942        parent.properties.insert(
1943            "version".to_string(),
1944            crate::class_registry::RuntimeProperty {
1945                name: "version".to_string(),
1946                is_static: true,
1947                is_constant: false,
1948                is_dependent: false,
1949                get_access: MemberAccess::Public,
1950                set_access: MemberAccess::Public,
1951                default_value: None,
1952            },
1953        );
1954        crate::class_registry::register_class(parent);
1955        crate::class_registry::register_class(crate::class_registry::RuntimeClass {
1956            name: child_name.to_string(),
1957            parent: Some(parent_name.to_string()),
1958            properties: Default::default(),
1959            methods: Default::default(),
1960        });
1961
1962        let obj = ObjectInstance::new(child_name.to_string());
1963        let err = error_message(
1964            run_setfield(
1965                Value::Object(obj),
1966                vec![Value::from("version"), Value::Num(2.0)],
1967            )
1968            .expect_err("setfield should reject inherited static property writes"),
1969        );
1970        assert!(
1971            err.contains("Property 'version' is static"),
1972            "unexpected error message: {err}"
1973        );
1974    }
1975
1976    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1977    #[test]
1978    fn setfield_updates_handle_target() {
1979        let mut inner = ObjectInstance::new("PointHandle".to_string());
1980        inner.properties.insert("x".to_string(), Value::Num(0.0));
1981        let gc_ptr = gc_allocate(Value::Object(inner)).expect("gc allocation");
1982        let handle_ptr = gc_ptr;
1983        let handle = HandleRef {
1984            class_name: "PointHandle".to_string(),
1985            target: handle_ptr,
1986            valid: true,
1987        };
1988
1989        let updated = run_setfield(
1990            Value::HandleObject(handle.clone()),
1991            vec![Value::from("x"), Value::Num(7.0)],
1992        )
1993        .expect("setfield handle update");
1994
1995        match updated {
1996            Value::HandleObject(h) => assert!(crate::is_handle_valid(&h)),
1997            other => panic!("expected handle, got {other:?}"),
1998        }
1999
2000        let pointee = runmat_gc::gc_clone_value(&gc_ptr).expect("valid handle target");
2001        match pointee {
2002            Value::Object(obj) => {
2003                assert_eq!(obj.properties.get("x"), Some(&Value::Num(7.0)));
2004            }
2005            other => panic!("expected object pointee, got {other:?}"),
2006        }
2007    }
2008
2009    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2010    #[test]
2011    #[cfg(feature = "wgpu")]
2012    fn setfield_gpu_tensor_indexing_gathers_to_host() {
2013        use runmat_accelerate::backend::wgpu::provider::{
2014            register_wgpu_provider, WgpuProviderOptions,
2015        };
2016        use runmat_accelerate_api::HostTensorView;
2017
2018        if runmat_accelerate_api::provider().is_none()
2019            && register_wgpu_provider(WgpuProviderOptions::default()).is_err()
2020        {
2021            runmat_accelerate::simple_provider::register_inprocess_provider();
2022        }
2023
2024        let provider = runmat_accelerate_api::provider().expect("accel provider");
2025        let data = [1.0, 2.0, 3.0, 4.0];
2026        let shape = [2usize, 2usize];
2027        let view = HostTensorView {
2028            data: &data,
2029            shape: &shape,
2030        };
2031        let handle = provider.upload(&view).expect("upload");
2032
2033        let mut root = StructValue::new();
2034        root.fields
2035            .insert("values".to_string(), Value::GpuTensor(handle));
2036
2037        let index_cell = CellArray::new_with_shape(
2038            vec![Value::Int(IntValue::I32(2)), Value::Int(IntValue::I32(2))],
2039            vec![1, 2],
2040        )
2041        .unwrap();
2042
2043        let updated = run_setfield(
2044            Value::Struct(root),
2045            vec![
2046                Value::from("values"),
2047                Value::Cell(index_cell),
2048                Value::Num(99.0),
2049            ],
2050        )
2051        .expect("setfield gpu value");
2052
2053        match updated {
2054            Value::Struct(st) => {
2055                let values = st.fields.get("values").expect("values field");
2056                match values {
2057                    Value::Tensor(tensor) => {
2058                        assert_eq!(tensor.shape, vec![2, 2]);
2059                        assert_eq!(tensor.numeric_value_at(3), Some(NumericScalar::F32(99.0)));
2060                    }
2061                    other => panic!("expected tensor after gather, got {other:?}"),
2062                }
2063            }
2064            other => panic!("expected struct result, got {other:?}"),
2065        }
2066    }
2067
2068    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2069    #[test]
2070    fn setfield_undefined_detection_requires_identifier() {
2071        let with_identifier = build_runtime_error("missing")
2072            .with_identifier(crate::IDENT_UNDEFINED_FUNCTION)
2073            .build();
2074        assert!(is_undefined_function(&with_identifier));
2075
2076        let message_only =
2077            build_runtime_error(format!("{} message only", crate::IDENT_UNDEFINED_FUNCTION))
2078                .build();
2079        assert!(
2080            !is_undefined_function(&message_only),
2081            "message-only undefined markers should not trigger setter fallback"
2082        );
2083    }
2084}