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