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