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