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::introspection::dynamicprops;
13use crate::builtins::structs::type_resolvers::setfield_type;
14use crate::{
15    build_runtime_error, call_builtin_async, gather_if_needed_async, object_property_getter_name,
16    object_property_setter_name, BuiltinResult, RuntimeError,
17};
18use runmat_builtins::{
19    Access, BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
20    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
21    CellArray, CharArray, ComplexTensor, HandleRef, LogicalArray, ObjectInstance, StructValue,
22    Tensor, Value,
23};
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 = handle.clone();
520    let updated = assign_into_value(current, steps, rhs).await?;
521    cell.data[position] = 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 = handle.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] = 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 dynamicprops::metadata_assignment(obj, name, rhs.clone())? {
993        return Ok(());
994    }
995
996    if let Some((prop, _owner)) = runmat_builtins::lookup_property(&obj.class_name, name) {
997        if prop.is_static {
998            return Err(setfield_static_access(format!(
999                "Property '{}' is static; use classref('{}').{}",
1000                name, obj.class_name, name
1001            )));
1002        }
1003        if prop.set_access == Access::Private {
1004            return Err(setfield_private_access(format!(
1005                "Property '{name}' is private"
1006            )));
1007        }
1008        if prop.is_dependent {
1009            let setter = object_property_setter_name(name);
1010            match call_builtin_async(&setter, &[Value::Object(obj.clone()), rhs.clone()]).await {
1011                Ok(value) => {
1012                    if let Value::Object(updated) = value {
1013                        *obj = updated;
1014                        return Ok(());
1015                    }
1016                    return Err(setfield_flow(format!(
1017                        "Dependent property setter for '{}' must return the updated object",
1018                        name
1019                    )));
1020                }
1021                Err(err) => {
1022                    if !is_undefined_function(&err) {
1023                        return Err(remap_setfield_flow(err, None));
1024                    }
1025                }
1026            }
1027            obj.properties.insert(format!("{name}_backing"), rhs);
1028            return Ok(());
1029        }
1030    }
1031
1032    if dynamicprops::dynamic_property_assign(obj, name, rhs.clone())? {
1033        return Ok(());
1034    }
1035
1036    obj.properties.insert(name.to_string(), rhs);
1037    Ok(())
1038}
1039
1040async fn assign_into_handle(
1041    handle: HandleRef,
1042    steps: &[FieldStep],
1043    rhs: Value,
1044) -> BuiltinResult<Value> {
1045    if steps.is_empty() {
1046        return Err(setfield_flow(
1047            "setfield: expected at least one field name when assigning into a handle",
1048        ));
1049    }
1050    if !crate::is_handle_valid(&handle) {
1051        return Err(setfield_flow(format!(
1052            "Invalid or deleted handle object '{}'.",
1053            handle.class_name
1054        )));
1055    }
1056    let current = runmat_gc::gc_clone_value(&handle.target)
1057        .map_err(|e| setfield_flow(format!("setfield: invalid handle target: {e}")))?;
1058    let updated = assign_into_value(current.clone(), steps, rhs).await?;
1059    runmat_gc::gc_with_value_mut(&handle.target, |target| -> BuiltinResult<()> {
1060        let target_valid = match target {
1061            Value::Object(obj) => !matches!(
1062                obj.properties.get(crate::HANDLE_VALID_FLAG_PROPERTY),
1063                Some(Value::Bool(false))
1064            ),
1065            _ => {
1066                return Err(setfield_flow(format!(
1067                    "Invalid or deleted handle object '{}'.",
1068                    handle.class_name
1069                )));
1070            }
1071        };
1072        if !target_valid {
1073            return Err(setfield_flow(format!(
1074                "Invalid or deleted handle object '{}'.",
1075                handle.class_name
1076            )));
1077        }
1078        if *target != current {
1079            return Err(setfield_flow(
1080                "setfield: handle target changed during asynchronous assignment",
1081            ));
1082        }
1083        runmat_gc::gc_record_handle_write(&handle.target, &updated);
1084        *target = updated;
1085        Ok(())
1086    })
1087    .map_err(|e| setfield_flow(format!("setfield: invalid handle target: {e}")))??;
1088    Ok(Value::HandleObject(handle))
1089}
1090
1091fn is_index_selector(value: &Value) -> bool {
1092    matches!(value, Value::Cell(_))
1093}
1094
1095fn parse_index_selector(value: Value) -> BuiltinResult<IndexSelector> {
1096    let Value::Cell(cell) = value else {
1097        return Err(setfield_flow(SETFIELD_ERROR_INDEX_SELECTOR_TYPE.message));
1098    };
1099    let mut components = Vec::with_capacity(cell.data.len());
1100    for handle in &cell.data {
1101        let entry = handle;
1102        components.push(parse_index_component(entry)?);
1103    }
1104    Ok(IndexSelector { components })
1105}
1106
1107fn parse_index_component(value: &Value) -> BuiltinResult<IndexComponent> {
1108    match value {
1109        Value::CharArray(ca) => {
1110            let text: String = ca.data.iter().collect();
1111            parse_index_text(text.trim())
1112        }
1113        Value::String(s) => parse_index_text(s.trim()),
1114        Value::StringArray(sa) if sa.data.len() == 1 => parse_index_text(sa.data[0].trim()),
1115        _ => {
1116            let idx = parse_positive_scalar(value).map_err(|err| {
1117                setfield_flow(format!(
1118                    "setfield: invalid index element ({})",
1119                    err.message()
1120                ))
1121            })?;
1122            Ok(IndexComponent::Scalar(idx))
1123        }
1124    }
1125}
1126
1127fn parse_index_text(text: &str) -> BuiltinResult<IndexComponent> {
1128    if text.eq_ignore_ascii_case("end") {
1129        return Ok(IndexComponent::End);
1130    }
1131    if text == ":" {
1132        return Err(setfield_flow(
1133            "setfield: ':' indexing is not currently supported",
1134        ));
1135    }
1136    if text.is_empty() {
1137        return Err(setfield_flow("setfield: index elements must not be empty"));
1138    }
1139    if let Ok(value) = text.parse::<usize>() {
1140        if value == 0 {
1141            return Err(setfield_flow("setfield: index must be >= 1"));
1142        }
1143        return Ok(IndexComponent::Scalar(value));
1144    }
1145    Err(setfield_flow(format!(
1146        "setfield: invalid index element '{}'",
1147        text
1148    )))
1149}
1150
1151fn parse_positive_scalar(value: &Value) -> BuiltinResult<usize> {
1152    let number = match value {
1153        Value::Int(i) => i.to_i64() as f64,
1154        Value::Num(n) => *n,
1155        Value::Tensor(t) if t.data.len() == 1 => t.data[0],
1156        _ => {
1157            let repr = format!("{value:?}");
1158            return Err(setfield_flow(format!(
1159                "expected positive integer index, got {repr}"
1160            )));
1161        }
1162    };
1163
1164    if !number.is_finite() {
1165        return Err(setfield_flow("index must be a finite number"));
1166    }
1167    if number.fract() != 0.0 {
1168        return Err(setfield_flow("index must be an integer"));
1169    }
1170    if number <= 0.0 {
1171        return Err(setfield_flow("index must be >= 1"));
1172    }
1173    if number > usize::MAX as f64 {
1174        return Err(setfield_flow("index exceeds platform limits"));
1175    }
1176    Ok(number as usize)
1177}
1178
1179fn parse_field_name(value: Value) -> BuiltinResult<String> {
1180    match value {
1181        Value::String(s) => Ok(s),
1182        Value::StringArray(sa) => {
1183            if sa.data.len() == 1 {
1184                Ok(sa.data[0].clone())
1185            } else {
1186                Err(setfield_flow(
1187                    "setfield: field names must be scalar string arrays or character vectors",
1188                ))
1189            }
1190        }
1191        Value::CharArray(ca) => {
1192            if ca.rows == 1 {
1193                Ok(ca.data.iter().collect())
1194            } else {
1195                Err(setfield_flow(
1196                    "setfield: field names must be 1-by-N character vectors",
1197                ))
1198            }
1199        }
1200        other => Err(setfield_flow(format!(
1201            "setfield: expected field name, got {other:?}"
1202        ))),
1203    }
1204}
1205
1206fn resolve_indices(value: &Value, selector: &IndexSelector) -> BuiltinResult<Vec<usize>> {
1207    let dims = selector.components.len();
1208    let mut resolved = Vec::with_capacity(dims);
1209    for (dim_idx, component) in selector.components.iter().enumerate() {
1210        let index = match component {
1211            IndexComponent::Scalar(idx) => *idx,
1212            IndexComponent::End => dimension_length(value, dims, dim_idx)?,
1213        };
1214        resolved.push(index);
1215    }
1216    Ok(resolved)
1217}
1218
1219fn dimension_length(value: &Value, dims: usize, dim_idx: usize) -> BuiltinResult<usize> {
1220    match value {
1221        Value::Tensor(tensor) => tensor_dimension_length(tensor, dims, dim_idx),
1222        Value::Cell(cell) => cell_dimension_length(cell, dims, dim_idx),
1223        Value::StringArray(array) => string_array_dimension_length(array, dims, dim_idx),
1224        Value::LogicalArray(logical) => logical_array_dimension_length(logical, dims, dim_idx),
1225        Value::CharArray(array) => char_array_dimension_length(array, dims, dim_idx),
1226        Value::ComplexTensor(tensor) => complex_tensor_dimension_length(tensor, dims, dim_idx),
1227        Value::Num(_) | Value::Int(_) | Value::Bool(_) => {
1228            if dims == 1 {
1229                Ok(1)
1230            } else {
1231                Err(setfield_flow(
1232                    "setfield: indexing with more than one dimension is not supported for scalars",
1233                ))
1234            }
1235        }
1236        other => Err(setfield_flow(format!(
1237            "Struct contents assignment to a {other:?} object is not supported."
1238        ))),
1239    }
1240}
1241
1242fn tensor_dimension_length(tensor: &Tensor, dims: usize, dim_idx: usize) -> BuiltinResult<usize> {
1243    if dims == 1 {
1244        let total = tensor.data.len();
1245        if total == 0 {
1246            return Err(setfield_flow(
1247                "Index exceeds the number of array elements (0).",
1248            ));
1249        }
1250        return Ok(total);
1251    }
1252    if dims > 2 {
1253        return Err(setfield_flow(
1254            "setfield: indexing with more than two indices is not supported yet",
1255        ));
1256    }
1257    let len = if dim_idx == 0 {
1258        tensor.rows()
1259    } else {
1260        tensor.cols()
1261    };
1262    if len == 0 {
1263        return Err(setfield_flow(
1264            "Index exceeds the number of array elements (0).",
1265        ));
1266    }
1267    Ok(len)
1268}
1269
1270fn cell_dimension_length(cell: &CellArray, dims: usize, dim_idx: usize) -> BuiltinResult<usize> {
1271    if dims == 1 {
1272        let total = cell.data.len();
1273        if total == 0 {
1274            return Err(setfield_flow(
1275                "Index exceeds the number of array elements (0).",
1276            ));
1277        }
1278        return Ok(total);
1279    }
1280    if dims > 2 {
1281        return Err(setfield_flow(
1282            "setfield: indexing with more than two indices is not supported yet",
1283        ));
1284    }
1285    let len = if dim_idx == 0 { cell.rows } else { cell.cols };
1286    if len == 0 {
1287        return Err(setfield_flow(
1288            "Index exceeds the number of array elements (0).",
1289        ));
1290    }
1291    Ok(len)
1292}
1293
1294fn string_array_dimension_length(
1295    array: &runmat_builtins::StringArray,
1296    dims: usize,
1297    dim_idx: usize,
1298) -> BuiltinResult<usize> {
1299    if dims == 1 {
1300        let total = array.data.len();
1301        if total == 0 {
1302            return Err(setfield_flow(
1303                "Index exceeds the number of array elements (0).",
1304            ));
1305        }
1306        return Ok(total);
1307    }
1308    if dims > 2 {
1309        return Err(setfield_flow(
1310            "setfield: indexing with more than two indices is not supported yet",
1311        ));
1312    }
1313    let len = if dim_idx == 0 { array.rows } else { array.cols };
1314    if len == 0 {
1315        return Err(setfield_flow(
1316            "Index exceeds the number of array elements (0).",
1317        ));
1318    }
1319    Ok(len)
1320}
1321
1322fn logical_array_dimension_length(
1323    array: &LogicalArray,
1324    dims: usize,
1325    dim_idx: usize,
1326) -> BuiltinResult<usize> {
1327    if dims == 1 {
1328        let total = array.data.len();
1329        if total == 0 {
1330            return Err(setfield_flow(
1331                "Index exceeds the number of array elements (0).",
1332            ));
1333        }
1334        return Ok(total);
1335    }
1336    if dims > 2 {
1337        return Err(setfield_flow(
1338            "setfield: indexing with more than two indices is not supported yet",
1339        ));
1340    }
1341    if array.shape.len() < dims {
1342        return Err(setfield_flow(
1343            "Index exceeds the number of array elements (0).",
1344        ));
1345    }
1346    let len = array.shape[dim_idx];
1347    if len == 0 {
1348        return Err(setfield_flow(
1349            "Index exceeds the number of array elements (0).",
1350        ));
1351    }
1352    Ok(len)
1353}
1354
1355fn char_array_dimension_length(
1356    array: &CharArray,
1357    dims: usize,
1358    dim_idx: usize,
1359) -> BuiltinResult<usize> {
1360    if dims == 1 {
1361        let total = array.data.len();
1362        if total == 0 {
1363            return Err(setfield_flow(
1364                "Index exceeds the number of array elements (0).",
1365            ));
1366        }
1367        return Ok(total);
1368    }
1369    if dims > 2 {
1370        return Err(setfield_flow(
1371            "setfield: indexing with more than two indices is not supported yet",
1372        ));
1373    }
1374    let len = if dim_idx == 0 { array.rows } else { array.cols };
1375    if len == 0 {
1376        return Err(setfield_flow(
1377            "Index exceeds the number of array elements (0).",
1378        ));
1379    }
1380    Ok(len)
1381}
1382
1383fn complex_tensor_dimension_length(
1384    tensor: &ComplexTensor,
1385    dims: usize,
1386    dim_idx: usize,
1387) -> BuiltinResult<usize> {
1388    if dims == 1 {
1389        let total = tensor.data.len();
1390        if total == 0 {
1391            return Err(setfield_flow(
1392                "Index exceeds the number of array elements (0).",
1393            ));
1394        }
1395        return Ok(total);
1396    }
1397    if dims > 2 {
1398        return Err(setfield_flow(
1399            "setfield: indexing with more than two indices is not supported yet",
1400        ));
1401    }
1402    let len = if dim_idx == 0 {
1403        tensor.rows
1404    } else {
1405        tensor.cols
1406    };
1407    if len == 0 {
1408        return Err(setfield_flow(
1409            "Index exceeds the number of array elements (0).",
1410        ));
1411    }
1412    Ok(len)
1413}
1414
1415fn value_to_scalar(value: Value) -> BuiltinResult<f64> {
1416    match value {
1417        Value::Num(n) => Ok(n),
1418        Value::Int(i) => Ok(i.to_f64()),
1419        Value::Bool(b) => Ok(if b { 1.0 } else { 0.0 }),
1420        Value::Tensor(t) if t.data.len() == 1 => Ok(t.data[0]),
1421        other => Err(setfield_flow(format!(
1422            "setfield: cannot assign {other:?} into a numeric tensor element"
1423        ))),
1424    }
1425}
1426
1427fn value_to_bool(value: Value) -> BuiltinResult<bool> {
1428    match value {
1429        Value::Bool(b) => Ok(b),
1430        Value::Num(n) => Ok(n != 0.0),
1431        Value::Int(i) => Ok(i.to_i64() != 0),
1432        Value::Tensor(t) if t.data.len() == 1 => Ok(t.data[0] != 0.0),
1433        other => Err(setfield_flow(format!(
1434            "setfield: cannot assign {other:?} into a logical array element"
1435        ))),
1436    }
1437}
1438
1439fn is_struct_array(cell: &CellArray) -> bool {
1440    cell.data
1441        .iter()
1442        .all(|handle| matches!(handle, Value::Struct(_)))
1443}
1444
1445#[cfg(test)]
1446pub(crate) mod tests {
1447    use super::*;
1448    use runmat_builtins::{
1449        Access, CellArray, ClassDef, HandleRef, IntValue, ObjectInstance, PropertyDef, StructValue,
1450    };
1451    use runmat_gc::gc_allocate;
1452
1453    fn error_message(err: crate::RuntimeError) -> String {
1454        err.message().to_string()
1455    }
1456
1457    fn run_setfield(base: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
1458        futures::executor::block_on(setfield_builtin(base, rest))
1459    }
1460
1461    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1462    #[test]
1463    fn setfield_creates_scalar_field() {
1464        let struct_value = StructValue::new();
1465        let updated = run_setfield(
1466            Value::Struct(struct_value),
1467            vec![Value::from("answer"), Value::Num(42.0)],
1468        )
1469        .expect("setfield");
1470        match updated {
1471            Value::Struct(st) => {
1472                assert_eq!(
1473                    st.fields.get("answer"),
1474                    Some(&Value::Num(42.0)),
1475                    "field should be inserted"
1476                );
1477            }
1478            other => panic!("expected struct result, got {other:?}"),
1479        }
1480    }
1481
1482    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1483    #[test]
1484    fn setfield_creates_nested_structs() {
1485        let struct_value = StructValue::new();
1486        let updated = run_setfield(
1487            Value::Struct(struct_value),
1488            vec![
1489                Value::from("solver"),
1490                Value::from("name"),
1491                Value::from("cg"),
1492            ],
1493        )
1494        .expect("setfield");
1495        match updated {
1496            Value::Struct(st) => {
1497                let solver = st.fields.get("solver").expect("solver field");
1498                match solver {
1499                    Value::Struct(inner) => {
1500                        assert_eq!(
1501                            inner.fields.get("name"),
1502                            Some(&Value::from("cg")),
1503                            "inner field should exist"
1504                        );
1505                    }
1506                    other => panic!("expected inner struct, got {other:?}"),
1507                }
1508            }
1509            other => panic!("expected struct result, got {other:?}"),
1510        }
1511    }
1512
1513    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1514    #[test]
1515    fn setfield_updates_struct_array_element() {
1516        let mut a = StructValue::new();
1517        a.fields
1518            .insert("id".to_string(), Value::Int(IntValue::I32(1)));
1519        let mut b = StructValue::new();
1520        b.fields
1521            .insert("id".to_string(), Value::Int(IntValue::I32(2)));
1522        let array = CellArray::new_with_shape(vec![Value::Struct(a), Value::Struct(b)], vec![1, 2])
1523            .unwrap();
1524        let indices =
1525            CellArray::new_with_shape(vec![Value::Int(IntValue::I32(2))], vec![1, 1]).unwrap();
1526        let updated = run_setfield(
1527            Value::Cell(array),
1528            vec![
1529                Value::Cell(indices),
1530                Value::from("id"),
1531                Value::Int(IntValue::I32(42)),
1532            ],
1533        )
1534        .expect("setfield");
1535        match updated {
1536            Value::Cell(cell) => {
1537                let second = &cell.data[1].clone();
1538                match second {
1539                    Value::Struct(st) => {
1540                        assert_eq!(st.fields.get("id"), Some(&Value::Int(IntValue::I32(42))));
1541                    }
1542                    other => panic!("expected struct element, got {other:?}"),
1543                }
1544            }
1545            other => panic!("expected cell array, got {other:?}"),
1546        }
1547    }
1548
1549    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1550    #[test]
1551    fn setfield_assigns_into_cell_then_struct() {
1552        let mut inner1 = StructValue::new();
1553        inner1.fields.insert("value".to_string(), Value::Num(1.0));
1554        let mut inner2 = StructValue::new();
1555        inner2.fields.insert("value".to_string(), Value::Num(2.0));
1556        let cell = CellArray::new_with_shape(
1557            vec![Value::Struct(inner1), Value::Struct(inner2)],
1558            vec![1, 2],
1559        )
1560        .unwrap();
1561        let mut root = StructValue::new();
1562        root.fields.insert("samples".to_string(), Value::Cell(cell));
1563
1564        let index_cell =
1565            CellArray::new_with_shape(vec![Value::Int(IntValue::I32(2))], vec![1, 1]).unwrap();
1566        let updated = run_setfield(
1567            Value::Struct(root),
1568            vec![
1569                Value::from("samples"),
1570                Value::Cell(index_cell),
1571                Value::from("value"),
1572                Value::Num(10.0),
1573            ],
1574        )
1575        .expect("setfield");
1576
1577        match updated {
1578            Value::Struct(st) => {
1579                let samples = st.fields.get("samples").expect("samples field");
1580                match samples {
1581                    Value::Cell(cell) => {
1582                        let value = &cell.data[1].clone();
1583                        match value {
1584                            Value::Struct(inner) => {
1585                                assert_eq!(inner.fields.get("value"), Some(&Value::Num(10.0)));
1586                            }
1587                            other => panic!("expected struct, got {other:?}"),
1588                        }
1589                    }
1590                    other => panic!("expected cell array, got {other:?}"),
1591                }
1592            }
1593            other => panic!("expected struct, got {other:?}"),
1594        }
1595    }
1596
1597    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1598    #[test]
1599    fn setfield_struct_array_with_end_index() {
1600        let mut first = StructValue::new();
1601        first
1602            .fields
1603            .insert("id".to_string(), Value::Int(IntValue::I32(1)));
1604        let mut second = StructValue::new();
1605        second
1606            .fields
1607            .insert("id".to_string(), Value::Int(IntValue::I32(2)));
1608        let array = CellArray::new_with_shape(
1609            vec![Value::Struct(first), Value::Struct(second)],
1610            vec![1, 2],
1611        )
1612        .unwrap();
1613        let index_cell = CellArray::new_with_shape(vec![Value::from("end")], vec![1, 1]).unwrap();
1614        let updated = run_setfield(
1615            Value::Cell(array),
1616            vec![
1617                Value::Cell(index_cell),
1618                Value::from("id"),
1619                Value::Int(IntValue::I32(99)),
1620            ],
1621        )
1622        .expect("setfield");
1623        match updated {
1624            Value::Cell(cell) => {
1625                let second = &cell.data[1].clone();
1626                match second {
1627                    Value::Struct(st) => {
1628                        assert_eq!(st.fields.get("id"), Some(&Value::Int(IntValue::I32(99))));
1629                    }
1630                    other => panic!("expected struct element, got {other:?}"),
1631                }
1632            }
1633            other => panic!("expected cell array result, got {other:?}"),
1634        }
1635    }
1636
1637    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1638    #[test]
1639    fn setfield_assigns_object_property() {
1640        let mut class_def = ClassDef {
1641            name: "Simple".to_string(),
1642            parent: None,
1643            properties: Default::default(),
1644            methods: Default::default(),
1645        };
1646        class_def.properties.insert(
1647            "x".to_string(),
1648            PropertyDef {
1649                name: "x".to_string(),
1650                is_static: false,
1651                is_constant: false,
1652                is_dependent: false,
1653                get_access: Access::Public,
1654                set_access: Access::Public,
1655                default_value: None,
1656            },
1657        );
1658        runmat_builtins::register_class(class_def);
1659
1660        let mut obj = ObjectInstance::new("Simple".to_string());
1661        obj.properties.insert("x".to_string(), Value::Num(0.0));
1662
1663        let updated = run_setfield(Value::Object(obj), vec![Value::from("x"), Value::Num(5.0)])
1664            .expect("setfield");
1665
1666        match updated {
1667            Value::Object(o) => {
1668                assert_eq!(o.properties.get("x"), Some(&Value::Num(5.0)));
1669            }
1670            other => panic!("expected object result, got {other:?}"),
1671        }
1672    }
1673
1674    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1675    #[test]
1676    fn setfield_errors_when_indexing_missing_field() {
1677        let struct_value = StructValue::new();
1678        let index_cell =
1679            CellArray::new_with_shape(vec![Value::Int(IntValue::I32(1))], vec![1, 1]).unwrap();
1680        let err = error_message(
1681            run_setfield(
1682                Value::Struct(struct_value),
1683                vec![
1684                    Value::from("missing"),
1685                    Value::Cell(index_cell),
1686                    Value::Num(1.0),
1687                ],
1688            )
1689            .expect_err("setfield should fail when field is missing"),
1690        );
1691        assert!(
1692            err.contains("Reference to non-existent field 'missing'."),
1693            "unexpected error message: {err}"
1694        );
1695    }
1696
1697    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1698    #[test]
1699    fn setfield_errors_on_static_property_assignment() {
1700        let mut class_def = ClassDef {
1701            name: "StaticSetfield".to_string(),
1702            parent: None,
1703            properties: Default::default(),
1704            methods: Default::default(),
1705        };
1706        class_def.properties.insert(
1707            "version".to_string(),
1708            PropertyDef {
1709                name: "version".to_string(),
1710                is_static: true,
1711                is_constant: false,
1712                is_dependent: false,
1713                get_access: Access::Public,
1714                set_access: Access::Public,
1715                default_value: None,
1716            },
1717        );
1718        runmat_builtins::register_class(class_def);
1719
1720        let obj = ObjectInstance::new("StaticSetfield".to_string());
1721        let err = error_message(
1722            run_setfield(
1723                Value::Object(obj),
1724                vec![Value::from("version"), Value::Num(2.0)],
1725            )
1726            .expect_err("setfield should reject static property writes"),
1727        );
1728        assert!(
1729            err.contains("Property 'version' is static"),
1730            "unexpected error message: {err}"
1731        );
1732    }
1733
1734    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1735    #[test]
1736    fn setfield_rejects_inherited_static_property_assignment() {
1737        let parent_name = "runmat.unittest.StaticSetfieldParent";
1738        let child_name = "runmat.unittest.StaticSetfieldChild";
1739
1740        let mut parent = ClassDef {
1741            name: parent_name.to_string(),
1742            parent: None,
1743            properties: Default::default(),
1744            methods: Default::default(),
1745        };
1746        parent.properties.insert(
1747            "version".to_string(),
1748            PropertyDef {
1749                name: "version".to_string(),
1750                is_static: true,
1751                is_constant: false,
1752                is_dependent: false,
1753                get_access: Access::Public,
1754                set_access: Access::Public,
1755                default_value: None,
1756            },
1757        );
1758        runmat_builtins::register_class(parent);
1759        runmat_builtins::register_class(ClassDef {
1760            name: child_name.to_string(),
1761            parent: Some(parent_name.to_string()),
1762            properties: Default::default(),
1763            methods: Default::default(),
1764        });
1765
1766        let obj = ObjectInstance::new(child_name.to_string());
1767        let err = error_message(
1768            run_setfield(
1769                Value::Object(obj),
1770                vec![Value::from("version"), Value::Num(2.0)],
1771            )
1772            .expect_err("setfield should reject inherited static property writes"),
1773        );
1774        assert!(
1775            err.contains("Property 'version' is static"),
1776            "unexpected error message: {err}"
1777        );
1778    }
1779
1780    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1781    #[test]
1782    fn setfield_updates_handle_target() {
1783        let mut inner = ObjectInstance::new("PointHandle".to_string());
1784        inner.properties.insert("x".to_string(), Value::Num(0.0));
1785        let gc_ptr = gc_allocate(Value::Object(inner)).expect("gc allocation");
1786        let handle_ptr = gc_ptr;
1787        let handle = HandleRef {
1788            class_name: "PointHandle".to_string(),
1789            target: handle_ptr,
1790            valid: true,
1791        };
1792
1793        let updated = run_setfield(
1794            Value::HandleObject(handle.clone()),
1795            vec![Value::from("x"), Value::Num(7.0)],
1796        )
1797        .expect("setfield handle update");
1798
1799        match updated {
1800            Value::HandleObject(h) => assert!(crate::is_handle_valid(&h)),
1801            other => panic!("expected handle, got {other:?}"),
1802        }
1803
1804        let pointee = runmat_gc::gc_clone_value(&gc_ptr).expect("valid handle target");
1805        match pointee {
1806            Value::Object(obj) => {
1807                assert_eq!(obj.properties.get("x"), Some(&Value::Num(7.0)));
1808            }
1809            other => panic!("expected object pointee, got {other:?}"),
1810        }
1811    }
1812
1813    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1814    #[test]
1815    #[cfg(feature = "wgpu")]
1816    fn setfield_gpu_tensor_indexing_gathers_to_host() {
1817        use runmat_accelerate::backend::wgpu::provider::{
1818            register_wgpu_provider, WgpuProviderOptions,
1819        };
1820        use runmat_accelerate_api::HostTensorView;
1821
1822        if runmat_accelerate_api::provider().is_none()
1823            && register_wgpu_provider(WgpuProviderOptions::default()).is_err()
1824        {
1825            runmat_accelerate::simple_provider::register_inprocess_provider();
1826        }
1827
1828        let provider = runmat_accelerate_api::provider().expect("accel provider");
1829        let data = [1.0, 2.0, 3.0, 4.0];
1830        let shape = [2usize, 2usize];
1831        let view = HostTensorView {
1832            data: &data,
1833            shape: &shape,
1834        };
1835        let handle = provider.upload(&view).expect("upload");
1836
1837        let mut root = StructValue::new();
1838        root.fields
1839            .insert("values".to_string(), Value::GpuTensor(handle));
1840
1841        let index_cell = CellArray::new_with_shape(
1842            vec![Value::Int(IntValue::I32(2)), Value::Int(IntValue::I32(2))],
1843            vec![1, 2],
1844        )
1845        .unwrap();
1846
1847        let updated = run_setfield(
1848            Value::Struct(root),
1849            vec![
1850                Value::from("values"),
1851                Value::Cell(index_cell),
1852                Value::Num(99.0),
1853            ],
1854        )
1855        .expect("setfield gpu value");
1856
1857        match updated {
1858            Value::Struct(st) => {
1859                let values = st.fields.get("values").expect("values field");
1860                match values {
1861                    Value::Tensor(tensor) => {
1862                        assert_eq!(tensor.shape, vec![2, 2]);
1863                        assert_eq!(tensor.data[3], 99.0);
1864                    }
1865                    other => panic!("expected tensor after gather, got {other:?}"),
1866                }
1867            }
1868            other => panic!("expected struct result, got {other:?}"),
1869        }
1870    }
1871
1872    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1873    #[test]
1874    fn setfield_undefined_detection_requires_identifier() {
1875        let with_identifier = build_runtime_error("missing")
1876            .with_identifier(crate::IDENT_UNDEFINED_FUNCTION)
1877            .build();
1878        assert!(is_undefined_function(&with_identifier));
1879
1880        let message_only =
1881            build_runtime_error(format!("{} message only", crate::IDENT_UNDEFINED_FUNCTION))
1882                .build();
1883        assert!(
1884            !is_undefined_function(&message_only),
1885            "message-only undefined markers should not trigger setter fallback"
1886        );
1887    }
1888}