1use crate::builtins::common::identifiers::is_valid_varname;
4use crate::builtins::common::spec::{
5 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
6 ReductionNaN, ResidencyPolicy, ShapeRequirements,
7};
8use crate::builtins::common::tensor;
9use runmat_builtins::catalog::definitions::{
10 STRUCT_ERROR_ASSEMBLE_FAILED, STRUCT_ERROR_CELL_SIZE_MISMATCH, STRUCT_ERROR_EMPTY_ARRAY_FAILED,
11 STRUCT_ERROR_FIELD_NAME_CHAR_VECTOR, STRUCT_ERROR_FIELD_NAME_EMPTY,
12 STRUCT_ERROR_FIELD_NAME_SCALAR, STRUCT_ERROR_FIELD_NAME_START_CHAR,
13 STRUCT_ERROR_FIELD_NAME_TYPE, STRUCT_ERROR_INVALID_SINGLE_INPUT, STRUCT_ERROR_NAME_VALUE_PAIRS,
14 STRUCT_ERROR_SIZE_OVERFLOW, STRUCT_ERROR_STRUCT_ARRAY_CONTENTS,
15 STRUCT_ERROR_STRUCT_ARRAY_COPY_FAILED,
16};
17use runmat_builtins::BuiltinErrorDescriptor;
18use runmat_macros::runtime_builtin;
19use runmat_value::{CellArray, CharArray, StructValue, Value};
20
21use crate::{build_runtime_error, BuiltinResult, RuntimeError};
22
23#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::structs::core::r#struct")]
24pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
25 name: "struct",
26 op_kind: GpuOpKind::Custom("struct"),
27 supported_precisions: &[],
28 broadcast: BroadcastSemantics::None,
29 provider_hooks: &[],
30 constant_strategy: ConstantStrategy::InlineLiteral,
31 residency: ResidencyPolicy::InheritInputs,
32 nan_mode: ReductionNaN::Include,
33 two_pass_threshold: None,
34 workgroup_size: None,
35 accepts_nan_mode: false,
36 notes: "Host-only construction; GPU values are preserved as handles without gathering.",
37};
38
39#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::structs::core::r#struct")]
40pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
41 name: "struct",
42 shape: ShapeRequirements::Any,
43 constant_strategy: ConstantStrategy::InlineLiteral,
44 elementwise: None,
45 reduction: None,
46 emits_nan: false,
47 notes: "Struct creation breaks fusion planning but retains GPU residency for field values.",
48};
49
50struct FieldEntry {
51 name: String,
52 value: FieldValue,
53}
54
55enum FieldValue {
56 Single(Value),
57 Cell(CellArray),
58}
59
60const BUILTIN_NAME: &str = "struct";
61
62fn struct_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
63 struct_error_with_message(error.message, error)
64}
65
66fn struct_error_with_message(
67 message: impl Into<String>,
68 error: &'static BuiltinErrorDescriptor,
69) -> RuntimeError {
70 let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
71 if let Some(identifier) = error.identifier {
72 builder = builder.with_identifier(identifier);
73 }
74 builder.build()
75}
76
77#[runtime_builtin(
78 name = "struct",
79 binding_variant = "default",
80 builtin_path = "crate::builtins::structs::core::r#struct"
81)]
82async fn struct_builtin(rest: Vec<Value>) -> BuiltinResult<Value> {
83 match rest.len() {
84 0 => Ok(Value::Struct(StructValue::new())),
85 1 => match rest.into_iter().next().unwrap() {
86 Value::Struct(existing) => Ok(Value::Struct(existing.clone())),
87 Value::Cell(cell) => clone_struct_array(&cell),
88 Value::Tensor(tensor) if tensor::tensor_element_len(&tensor) == 0 => {
89 empty_struct_array()
90 }
91 Value::LogicalArray(logical) if logical.data.is_empty() => empty_struct_array(),
92 other => Err(struct_error_with_message(
93 format!(
94 "{} (got {other:?})",
95 STRUCT_ERROR_INVALID_SINGLE_INPUT.message
96 ),
97 &STRUCT_ERROR_INVALID_SINGLE_INPUT,
98 )),
99 },
100 len if len % 2 == 0 => build_from_pairs(rest),
101 _ => Err(struct_error(&STRUCT_ERROR_NAME_VALUE_PAIRS)),
102 }
103}
104
105fn build_from_pairs(args: Vec<Value>) -> BuiltinResult<Value> {
106 let mut entries: Vec<FieldEntry> = Vec::new();
107 let mut target_shape: Option<Vec<usize>> = None;
108
109 let mut iter = args.into_iter();
110 while let (Some(name_value), Some(field_value)) = (iter.next(), iter.next()) {
111 let field_name = parse_field_name(&name_value)?;
112 match field_value {
113 Value::Cell(cell) => {
114 let shape = cell.shape.clone();
115 if let Some(existing) = &target_shape {
116 if *existing != shape {
117 return Err(struct_error(&STRUCT_ERROR_CELL_SIZE_MISMATCH));
118 }
119 } else {
120 target_shape = Some(shape);
121 }
122 entries.push(FieldEntry {
123 name: field_name,
124 value: FieldValue::Cell(cell),
125 });
126 }
127 other => entries.push(FieldEntry {
128 name: field_name,
129 value: FieldValue::Single(other),
130 }),
131 }
132 }
133
134 if let Some(shape) = target_shape {
135 build_struct_array(entries, shape)
136 } else {
137 build_scalar_struct(entries)
138 }
139}
140
141fn build_scalar_struct(entries: Vec<FieldEntry>) -> BuiltinResult<Value> {
142 let mut fields = StructValue::new();
143 for entry in entries {
144 match entry.value {
145 FieldValue::Single(value) => {
146 fields.fields.insert(entry.name, value);
147 }
148 FieldValue::Cell(cell) => {
149 let shape = cell.shape.clone();
150 return build_struct_array(
151 vec![FieldEntry {
152 name: entry.name,
153 value: FieldValue::Cell(cell),
154 }],
155 shape,
156 );
157 }
158 }
159 }
160 Ok(Value::Struct(fields))
161}
162
163fn build_struct_array(entries: Vec<FieldEntry>, shape: Vec<usize>) -> BuiltinResult<Value> {
164 let total_len = shape
165 .iter()
166 .try_fold(1usize, |acc, &dim| acc.checked_mul(dim))
167 .ok_or_else(|| struct_error(&STRUCT_ERROR_SIZE_OVERFLOW))?;
168
169 for entry in &entries {
170 if let FieldValue::Cell(cell) = &entry.value {
171 if cell.data.len() != total_len {
172 return Err(struct_error(&STRUCT_ERROR_CELL_SIZE_MISMATCH));
173 }
174 }
175 }
176
177 let mut structs: Vec<Value> = Vec::with_capacity(total_len);
178 for idx in 0..total_len {
179 let mut fields = StructValue::new();
180 for entry in &entries {
181 let value = match &entry.value {
182 FieldValue::Single(val) => val.clone(),
183 FieldValue::Cell(cell) => clone_cell_element(cell, idx)?,
184 };
185 fields.fields.insert(entry.name.clone(), value);
186 }
187 structs.push(Value::Struct(fields));
188 }
189
190 CellArray::new_with_shape(structs, shape)
191 .map(Value::Cell)
192 .map_err(|e| {
193 struct_error_with_message(
194 format!("{}: {e}", STRUCT_ERROR_ASSEMBLE_FAILED.message),
195 &STRUCT_ERROR_ASSEMBLE_FAILED,
196 )
197 })
198}
199
200fn clone_cell_element(cell: &CellArray, index: usize) -> BuiltinResult<Value> {
201 cell.data
202 .get(index)
203 .cloned()
204 .ok_or_else(|| struct_error(&STRUCT_ERROR_CELL_SIZE_MISMATCH))
205}
206
207fn empty_struct_array() -> BuiltinResult<Value> {
208 CellArray::new(Vec::new(), 0, 0)
209 .map(Value::Cell)
210 .map_err(|e| {
211 struct_error_with_message(
212 format!("{}: {e}", STRUCT_ERROR_EMPTY_ARRAY_FAILED.message),
213 &STRUCT_ERROR_EMPTY_ARRAY_FAILED,
214 )
215 })
216}
217
218fn clone_struct_array(array: &CellArray) -> BuiltinResult<Value> {
219 let mut values: Vec<Value> = Vec::with_capacity(array.data.len());
220 for (index, handle) in array.data.iter().enumerate() {
221 let value = handle.clone();
222 if !matches!(value, Value::Struct(_)) {
223 return Err(struct_error_with_message(
224 format!(
225 "{} (element {} is not a struct)",
226 STRUCT_ERROR_STRUCT_ARRAY_CONTENTS.message,
227 index + 1
228 ),
229 &STRUCT_ERROR_STRUCT_ARRAY_CONTENTS,
230 ));
231 }
232 values.push(value);
233 }
234 CellArray::new_with_shape(values, array.shape.clone())
235 .map(Value::Cell)
236 .map_err(|e| {
237 struct_error_with_message(
238 format!("{}: {e}", STRUCT_ERROR_STRUCT_ARRAY_COPY_FAILED.message),
239 &STRUCT_ERROR_STRUCT_ARRAY_COPY_FAILED,
240 )
241 })
242}
243
244fn parse_field_name(value: &Value) -> BuiltinResult<String> {
245 let text = match value {
246 Value::String(s) => s.clone(),
247 Value::StringArray(sa) => {
248 if sa.data.len() == 1 {
249 sa.data[0].clone()
250 } else {
251 return Err(struct_error(&STRUCT_ERROR_FIELD_NAME_SCALAR));
252 }
253 }
254 Value::CharArray(ca) => char_array_to_string(ca)?,
255 _ => return Err(struct_error(&STRUCT_ERROR_FIELD_NAME_TYPE)),
256 };
257
258 validate_field_name(&text)?;
259 Ok(text)
260}
261
262fn char_array_to_string(ca: &CharArray) -> BuiltinResult<String> {
263 if ca.rows > 1 {
264 return Err(struct_error(&STRUCT_ERROR_FIELD_NAME_CHAR_VECTOR));
265 }
266 let mut out = String::with_capacity(ca.data.len());
267 for ch in &ca.data {
268 out.push(*ch);
269 }
270 Ok(out)
271}
272
273fn validate_field_name(name: &str) -> BuiltinResult<()> {
274 if name.is_empty() {
275 return Err(struct_error(&STRUCT_ERROR_FIELD_NAME_EMPTY));
276 }
277
278 if !is_valid_varname(name) {
279 return Err(struct_error_with_message(
280 format!(
281 "{} (got '{name}')",
282 STRUCT_ERROR_FIELD_NAME_START_CHAR.message
283 ),
284 &STRUCT_ERROR_FIELD_NAME_START_CHAR,
285 ));
286 }
287 Ok(())
288}
289
290#[cfg(test)]
291pub(crate) mod tests {
292 use super::*;
293 use crate::builtins::common::identifiers::MATLAB_NAME_LENGTH_MAX;
294 use runmat_accelerate_api::GpuTensorHandle;
295 use runmat_value::{CellArray, IntValue, StringArray, StructValue, Tensor};
296
297 #[cfg(feature = "wgpu")]
298 use runmat_accelerate_api::HostTensorView;
299
300 fn error_message(err: crate::RuntimeError) -> String {
301 err.message().to_string()
302 }
303
304 fn run_struct(args: Vec<Value>) -> BuiltinResult<Value> {
305 futures::executor::block_on(struct_builtin(args))
306 }
307
308 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
309 #[test]
310 fn struct_empty() {
311 let Value::Struct(s) = run_struct(Vec::new()).expect("struct") else {
312 panic!("expected struct value");
313 };
314 assert!(s.fields.is_empty());
315 }
316
317 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
318 #[test]
319 fn struct_empty_from_empty_matrix() {
320 let tensor = Tensor::new(Vec::new(), vec![0, 0]).unwrap();
321 let value = run_struct(vec![Value::Tensor(tensor)]).expect("struct([])");
322 match value {
323 Value::Cell(cell) => {
324 assert_eq!(cell.rows, 0);
325 assert_eq!(cell.cols, 0);
326 assert!(cell.data.is_empty());
327 }
328 other => panic!("expected empty struct array, got {other:?}"),
329 }
330 }
331
332 #[test]
333 fn struct_uses_typed_integer_storage_to_detect_empty_input() {
334 let tensor =
335 Tensor::new_integer(runmat_value::IntegerStorage::U64(Vec::new()), vec![0, 0]).unwrap();
336
337 assert!(matches!(
338 run_struct(vec![Value::Tensor(tensor)]).unwrap(),
339 Value::Cell(_)
340 ));
341 }
342
343 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
344 #[test]
345 fn struct_name_value_pairs() {
346 let args = vec![
347 Value::from("name"),
348 Value::from("Ada"),
349 Value::from("score"),
350 Value::Int(IntValue::I32(42)),
351 ];
352 let Value::Struct(s) = run_struct(args).expect("struct") else {
353 panic!("expected struct value");
354 };
355 assert_eq!(s.fields.len(), 2);
356 assert!(matches!(s.fields.get("name"), Some(Value::String(v)) if v == "Ada"));
357 assert!(matches!(
358 s.fields.get("score"),
359 Some(Value::Int(IntValue::I32(42)))
360 ));
361 }
362
363 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
364 #[test]
365 fn struct_struct_array_from_cells() {
366 let names = CellArray::new(vec![Value::from("Ada"), Value::from("Grace")], 1, 2).unwrap();
367 let ages = CellArray::new(
368 vec![Value::Int(IntValue::I32(36)), Value::Int(IntValue::I32(45))],
369 1,
370 2,
371 )
372 .unwrap();
373 let result = run_struct(vec![
374 Value::from("name"),
375 Value::Cell(names),
376 Value::from("age"),
377 Value::Cell(ages),
378 ])
379 .expect("struct array");
380 let structs = expect_struct_array(result);
381 assert_eq!(structs.len(), 2);
382 assert!(matches!(
383 structs[0].fields.get("name"),
384 Some(Value::String(v)) if v == "Ada"
385 ));
386 assert!(matches!(
387 structs[1].fields.get("age"),
388 Some(Value::Int(IntValue::I32(45)))
389 ));
390 }
391
392 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
393 #[test]
394 fn struct_struct_array_replicates_scalars() {
395 let names = CellArray::new(vec![Value::from("Ada"), Value::from("Grace")], 1, 2).unwrap();
396 let result = run_struct(vec![
397 Value::from("name"),
398 Value::Cell(names),
399 Value::from("department"),
400 Value::from("Research"),
401 ])
402 .expect("struct array");
403 let structs = expect_struct_array(result);
404 assert_eq!(structs.len(), 2);
405 for entry in structs {
406 assert!(matches!(
407 entry.fields.get("department"),
408 Some(Value::String(v)) if v == "Research"
409 ));
410 }
411 }
412
413 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
414 #[test]
415 fn struct_struct_array_cell_size_mismatch_errors() {
416 let names = CellArray::new(vec![Value::from("Ada"), Value::from("Grace")], 1, 2).unwrap();
417 let scores = CellArray::new(vec![Value::Int(IntValue::I32(1))], 1, 1).unwrap();
418 let err = error_message(
419 run_struct(vec![
420 Value::from("name"),
421 Value::Cell(names),
422 Value::from("score"),
423 Value::Cell(scores),
424 ])
425 .unwrap_err(),
426 );
427 assert!(err.contains("matching sizes"));
428 }
429
430 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
431 #[test]
432 fn struct_overwrites_duplicates() {
433 let args = vec![
434 Value::from("version"),
435 Value::Int(IntValue::I32(1)),
436 Value::from("version"),
437 Value::Int(IntValue::I32(2)),
438 ];
439 let Value::Struct(s) = run_struct(args).expect("struct") else {
440 panic!("expected struct value");
441 };
442 assert_eq!(s.fields.len(), 1);
443 assert!(matches!(
444 s.fields.get("version"),
445 Some(Value::Int(IntValue::I32(2)))
446 ));
447 }
448
449 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
450 #[test]
451 fn struct_rejects_odd_arguments() {
452 let err = error_message(run_struct(vec![Value::from("name")]).unwrap_err());
453 assert!(err.contains("name/value pairs"));
454 }
455
456 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
457 #[test]
458 fn struct_rejects_invalid_field_name() {
459 let err = error_message(
460 run_struct(vec![Value::from("1bad"), Value::Int(IntValue::I32(1))]).unwrap_err(),
461 );
462 assert!(err.contains("valid MATLAB identifiers"));
463 }
464
465 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
466 #[test]
467 fn struct_field_names_follow_shared_identifier_rules() {
468 let max_len = "a".repeat(MATLAB_NAME_LENGTH_MAX);
469 let Value::Struct(s) = run_struct(vec![
470 Value::from(max_len.clone()),
471 Value::Int(IntValue::I32(1)),
472 ])
473 .expect("max length field name") else {
474 panic!("expected struct value");
475 };
476 assert!(s.fields.contains_key(&max_len));
477
478 for bad in [
479 "_hidden".to_string(),
480 "for".to_string(),
481 "éclair".to_string(),
482 "a".repeat(MATLAB_NAME_LENGTH_MAX + 1),
483 ] {
484 let err =
485 error_message(run_struct(vec![Value::from(bad), Value::Num(1.0)]).unwrap_err());
486 assert!(err.contains("valid MATLAB identifiers"));
487 }
488 }
489
490 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
491 #[test]
492 fn struct_rejects_non_text_field_name() {
493 let err = error_message(
494 run_struct(vec![Value::Num(1.0), Value::Int(IntValue::I32(1))]).unwrap_err(),
495 );
496 assert!(err.contains("strings or character vectors"));
497 }
498
499 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
500 #[test]
501 fn struct_accepts_char_vector_name() {
502 let chars = CharArray::new("field".chars().collect(), 1, 5).unwrap();
503 let args = vec![Value::CharArray(chars), Value::Num(1.0)];
504 let Value::Struct(s) = run_struct(args).expect("struct") else {
505 panic!("expected struct value");
506 };
507 assert!(s.fields.contains_key("field"));
508 }
509
510 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
511 #[test]
512 fn struct_accepts_string_scalar_name() {
513 let sa = StringArray::new(vec!["field".to_string()], vec![1]).unwrap();
514 let args = vec![Value::StringArray(sa), Value::Num(1.0)];
515 let Value::Struct(s) = run_struct(args).expect("struct") else {
516 panic!("expected struct value");
517 };
518 assert!(s.fields.contains_key("field"));
519 }
520
521 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
522 #[test]
523 fn struct_allows_existing_struct_copy() {
524 let mut base = StructValue::new();
525 base.fields
526 .insert("id".to_string(), Value::Int(IntValue::I32(7)));
527 let copy = run_struct(vec![Value::Struct(base.clone())]).expect("struct");
528 assert_eq!(copy, Value::Struct(base));
529 }
530
531 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
532 #[test]
533 fn struct_copies_struct_array_argument() {
534 let mut proto = StructValue::new();
535 proto
536 .fields
537 .insert("id".into(), Value::Int(IntValue::I32(7)));
538 let struct_array = CellArray::new(
539 vec![
540 Value::Struct(proto.clone()),
541 Value::Struct(proto.clone()),
542 Value::Struct(proto.clone()),
543 ],
544 1,
545 3,
546 )
547 .unwrap();
548 let original = struct_array.clone();
549 let result = run_struct(vec![Value::Cell(struct_array)]).expect("struct array clone");
550 let cloned = expect_struct_array(result);
551 let baseline = expect_struct_array(Value::Cell(original));
552 assert_eq!(cloned, baseline);
553 }
554
555 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
556 #[test]
557 fn struct_rejects_cell_argument_without_structs() {
558 let cell = CellArray::new(vec![Value::Num(1.0)], 1, 1).unwrap();
559 let err = error_message(run_struct(vec![Value::Cell(cell)]).unwrap_err());
560 assert!(err.contains("must contain structs"));
561 }
562
563 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
564 #[test]
565 fn struct_preserves_gpu_tensor_handles() {
566 let handle = GpuTensorHandle {
567 shape: vec![2, 2],
568 device_id: 1,
569 buffer_id: 99,
570 descriptor: Default::default(),
571 };
572 let args = vec![Value::from("data"), Value::GpuTensor(handle.clone())];
573 let Value::Struct(s) = run_struct(args).expect("struct") else {
574 panic!("expected struct value");
575 };
576 assert!(matches!(s.fields.get("data"), Some(Value::GpuTensor(h)) if h == &handle));
577 }
578
579 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
580 #[test]
581 fn struct_struct_array_preserves_gpu_handles() {
582 let first = GpuTensorHandle {
583 shape: vec![1, 1],
584 device_id: 2,
585 buffer_id: 11,
586 descriptor: Default::default(),
587 };
588 let second = GpuTensorHandle {
589 shape: vec![1, 1],
590 device_id: 2,
591 buffer_id: 12,
592 descriptor: Default::default(),
593 };
594 let cell = CellArray::new(
595 vec![
596 Value::GpuTensor(first.clone()),
597 Value::GpuTensor(second.clone()),
598 ],
599 1,
600 2,
601 )
602 .unwrap();
603 let result = run_struct(vec![Value::from("payload"), Value::Cell(cell)])
604 .expect("struct array gpu handles");
605 let structs = expect_struct_array(result);
606 assert!(matches!(
607 structs[0].fields.get("payload"),
608 Some(Value::GpuTensor(h)) if h == &first
609 ));
610 assert!(matches!(
611 structs[1].fields.get("payload"),
612 Some(Value::GpuTensor(h)) if h == &second
613 ));
614 }
615
616 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
617 #[test]
618 #[cfg(feature = "wgpu")]
619 fn struct_preserves_gpu_handles_with_registered_provider() {
620 let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
621 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
622 );
623 let provider = runmat_accelerate_api::provider().expect("wgpu provider");
624 let host = HostTensorView {
625 data: &[1.0, 2.0],
626 shape: &[2, 1],
627 };
628 let handle = provider.upload(&host).expect("upload");
629 let args = vec![Value::from("gpu"), Value::GpuTensor(handle.clone())];
630 let Value::Struct(s) = run_struct(args).expect("struct") else {
631 panic!("expected struct value");
632 };
633 assert!(matches!(s.fields.get("gpu"), Some(Value::GpuTensor(h)) if h == &handle));
634 }
635
636 fn expect_struct_array(value: Value) -> Vec<StructValue> {
637 match value {
638 Value::Cell(cell) => cell
639 .data
640 .into_iter()
641 .map(|value| match value {
642 Value::Struct(st) => st,
643 other => panic!("expected struct element, got {other:?}"),
644 })
645 .collect(),
646 Value::Struct(st) => vec![st],
647 other => panic!("expected struct or struct array, got {other:?}"),
648 }
649 }
650}