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