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::structs::type_resolvers::struct_type;
9use runmat_builtins::{
10 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
11 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
12 CellArray, CharArray, StructValue, Value,
13};
14use runmat_macros::runtime_builtin;
15
16use crate::{build_runtime_error, BuiltinResult, RuntimeError};
17
18#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::structs::core::r#struct")]
19pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
20 name: "struct",
21 op_kind: GpuOpKind::Custom("struct"),
22 supported_precisions: &[],
23 broadcast: BroadcastSemantics::None,
24 provider_hooks: &[],
25 constant_strategy: ConstantStrategy::InlineLiteral,
26 residency: ResidencyPolicy::InheritInputs,
27 nan_mode: ReductionNaN::Include,
28 two_pass_threshold: None,
29 workgroup_size: None,
30 accepts_nan_mode: false,
31 notes: "Host-only construction; GPU values are preserved as handles without gathering.",
32};
33
34#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::structs::core::r#struct")]
35pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
36 name: "struct",
37 shape: ShapeRequirements::Any,
38 constant_strategy: ConstantStrategy::InlineLiteral,
39 elementwise: None,
40 reduction: None,
41 emits_nan: false,
42 notes: "Struct creation breaks fusion planning but retains GPU residency for field values.",
43};
44
45struct FieldEntry {
46 name: String,
47 value: FieldValue,
48}
49
50enum FieldValue {
51 Single(Value),
52 Cell(CellArray),
53}
54
55const BUILTIN_NAME: &str = "struct";
56
57const STRUCT_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
58 name: "S",
59 ty: BuiltinParamType::Any,
60 arity: BuiltinParamArity::Required,
61 default: None,
62 description: "Scalar struct or struct array.",
63}];
64
65const STRUCT_INPUTS_EMPTY: [BuiltinParamDescriptor; 0] = [];
66const STRUCT_INPUTS_TEMPLATE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
67 name: "template",
68 ty: BuiltinParamType::Any,
69 arity: BuiltinParamArity::Required,
70 default: None,
71 description: "Existing struct/struct-array template or empty array for struct([]).",
72}];
73const STRUCT_INPUTS_PAIRS: [BuiltinParamDescriptor; 3] = [
74 BuiltinParamDescriptor {
75 name: "field",
76 ty: BuiltinParamType::PropertyName,
77 arity: BuiltinParamArity::Required,
78 default: None,
79 description: "Field name.",
80 },
81 BuiltinParamDescriptor {
82 name: "value",
83 ty: BuiltinParamType::Any,
84 arity: BuiltinParamArity::Required,
85 default: None,
86 description: "Field value or cell array of field values.",
87 },
88 BuiltinParamDescriptor {
89 name: "name_value_pairs",
90 ty: BuiltinParamType::Any,
91 arity: BuiltinParamArity::Variadic,
92 default: None,
93 description: "Additional field/value pairs.",
94 },
95];
96
97const STRUCT_SIGNATURES: [BuiltinSignatureDescriptor; 3] = [
98 BuiltinSignatureDescriptor {
99 label: "S = struct()",
100 inputs: &STRUCT_INPUTS_EMPTY,
101 outputs: &STRUCT_OUTPUT,
102 },
103 BuiltinSignatureDescriptor {
104 label: "S = struct(template)",
105 inputs: &STRUCT_INPUTS_TEMPLATE,
106 outputs: &STRUCT_OUTPUT,
107 },
108 BuiltinSignatureDescriptor {
109 label: "S = struct(field, value, ...)",
110 inputs: &STRUCT_INPUTS_PAIRS,
111 outputs: &STRUCT_OUTPUT,
112 },
113];
114
115const STRUCT_ERROR_INVALID_SINGLE_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
116 code: "RM.STRUCT.INVALID_SINGLE_INPUT",
117 identifier: Some("RunMat:struct:InvalidSingleInput"),
118 when: "Single input is neither struct, struct-array cell, nor empty numeric/logical array.",
119 message:
120 "struct: expected name/value pairs, an existing struct or struct array, or [] to create an empty struct array",
121};
122
123const STRUCT_ERROR_NAME_VALUE_PAIRS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
124 code: "RM.STRUCT.NAME_VALUE_PAIRS",
125 identifier: Some("RunMat:struct:NameValuePairs"),
126 when: "Name/value arguments are not supplied in complete pairs.",
127 message: "struct: expected name/value pairs",
128};
129
130const STRUCT_ERROR_CELL_SIZE_MISMATCH: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
131 code: "RM.STRUCT.CELL_SIZE_MISMATCH",
132 identifier: Some("RunMat:struct:CellSizeMismatch"),
133 when: "Cell value inputs for struct-array construction do not share the same shape.",
134 message: "struct: cell inputs must have matching sizes",
135};
136
137const STRUCT_ERROR_SIZE_OVERFLOW: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
138 code: "RM.STRUCT.SIZE_OVERFLOW",
139 identifier: Some("RunMat:struct:SizeOverflow"),
140 when: "Requested struct-array size exceeds platform limits.",
141 message: "struct: struct array size exceeds platform limits",
142};
143
144const STRUCT_ERROR_ASSEMBLE_FAILED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
145 code: "RM.STRUCT.ASSEMBLE_FAILED",
146 identifier: Some("RunMat:struct:AssembleFailed"),
147 when: "Internal struct-array assembly failed.",
148 message: "struct: failed to assemble struct array",
149};
150
151const STRUCT_ERROR_EMPTY_ARRAY_FAILED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
152 code: "RM.STRUCT.EMPTY_ARRAY_FAILED",
153 identifier: Some("RunMat:struct:EmptyArrayFailed"),
154 when: "Internal empty struct-array creation failed.",
155 message: "struct: failed to create empty struct array",
156};
157
158const STRUCT_ERROR_STRUCT_ARRAY_CONTENTS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
159 code: "RM.STRUCT.STRUCT_ARRAY_CONTENTS",
160 identifier: Some("RunMat:struct:StructArrayContents"),
161 when: "Single-argument struct-array cell input contains non-struct values.",
162 message: "struct: single argument cell input must contain structs",
163};
164
165const STRUCT_ERROR_STRUCT_ARRAY_COPY_FAILED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
166 code: "RM.STRUCT.STRUCT_ARRAY_COPY_FAILED",
167 identifier: Some("RunMat:struct:StructArrayCopyFailed"),
168 when: "Copying a single-argument struct-array cell input failed.",
169 message: "struct: failed to copy struct array",
170};
171
172const STRUCT_ERROR_FIELD_NAME_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
173 code: "RM.STRUCT.FIELD_NAME_TYPE",
174 identifier: Some("RunMat:struct:FieldNameType"),
175 when: "Field name is not a string scalar or 1xN character vector.",
176 message: "struct: field names must be strings or character vectors",
177};
178
179const STRUCT_ERROR_FIELD_NAME_SCALAR: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
180 code: "RM.STRUCT.FIELD_NAME_SCALAR",
181 identifier: Some("RunMat:struct:FieldNameScalar"),
182 when: "Field name char/string-array input is not scalar.",
183 message: "struct: field names must be scalar string arrays or character vectors",
184};
185
186const STRUCT_ERROR_FIELD_NAME_CHAR_VECTOR: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
187 code: "RM.STRUCT.FIELD_NAME_CHAR_VECTOR",
188 identifier: Some("RunMat:struct:FieldNameCharVector"),
189 when: "Character-array field name input is not a 1-by-N character vector.",
190 message: "struct: field names must be 1-by-N character vectors",
191};
192
193const STRUCT_ERROR_FIELD_NAME_EMPTY: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
194 code: "RM.STRUCT.FIELD_NAME_EMPTY",
195 identifier: Some("RunMat:struct:FieldNameEmpty"),
196 when: "Field name is empty.",
197 message: "struct: field names must be nonempty",
198};
199
200const STRUCT_ERROR_FIELD_NAME_START_CHAR: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
201 code: "RM.STRUCT.FIELD_NAME_START_CHAR",
202 identifier: Some("RunMat:struct:FieldNameStartChar"),
203 when: "Field name is not a valid MATLAB identifier.",
204 message: "struct: field names must be valid MATLAB identifiers",
205};
206
207const STRUCT_ERRORS: [BuiltinErrorDescriptor; 13] = [
208 STRUCT_ERROR_INVALID_SINGLE_INPUT,
209 STRUCT_ERROR_NAME_VALUE_PAIRS,
210 STRUCT_ERROR_CELL_SIZE_MISMATCH,
211 STRUCT_ERROR_SIZE_OVERFLOW,
212 STRUCT_ERROR_ASSEMBLE_FAILED,
213 STRUCT_ERROR_EMPTY_ARRAY_FAILED,
214 STRUCT_ERROR_STRUCT_ARRAY_CONTENTS,
215 STRUCT_ERROR_STRUCT_ARRAY_COPY_FAILED,
216 STRUCT_ERROR_FIELD_NAME_TYPE,
217 STRUCT_ERROR_FIELD_NAME_SCALAR,
218 STRUCT_ERROR_FIELD_NAME_CHAR_VECTOR,
219 STRUCT_ERROR_FIELD_NAME_EMPTY,
220 STRUCT_ERROR_FIELD_NAME_START_CHAR,
221];
222
223pub const STRUCT_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
224 signatures: &STRUCT_SIGNATURES,
225 output_mode: BuiltinOutputMode::Fixed,
226 completion_policy: BuiltinCompletionPolicy::Public,
227 errors: &STRUCT_ERRORS,
228};
229
230fn struct_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
231 struct_error_with_message(error.message, error)
232}
233
234fn struct_error_with_message(
235 message: impl Into<String>,
236 error: &'static BuiltinErrorDescriptor,
237) -> RuntimeError {
238 let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
239 if let Some(identifier) = error.identifier {
240 builder = builder.with_identifier(identifier);
241 }
242 builder.build()
243}
244
245#[runtime_builtin(
246 name = "struct",
247 category = "structs/core",
248 summary = "Create scalar structs or struct arrays from field/value inputs.",
249 keywords = "struct,structure,name-value,record",
250 type_resolver(struct_type),
251 descriptor(crate::builtins::structs::core::r#struct::STRUCT_DESCRIPTOR),
252 builtin_path = "crate::builtins::structs::core::r#struct"
253)]
254async fn struct_builtin(rest: Vec<Value>) -> BuiltinResult<Value> {
255 match rest.len() {
256 0 => Ok(Value::Struct(StructValue::new())),
257 1 => match rest.into_iter().next().unwrap() {
258 Value::Struct(existing) => Ok(Value::Struct(existing.clone())),
259 Value::Cell(cell) => clone_struct_array(&cell),
260 Value::Tensor(tensor) if tensor.data.is_empty() => empty_struct_array(),
261 Value::LogicalArray(logical) if logical.data.is_empty() => empty_struct_array(),
262 other => Err(struct_error_with_message(
263 format!(
264 "{} (got {other:?})",
265 STRUCT_ERROR_INVALID_SINGLE_INPUT.message
266 ),
267 &STRUCT_ERROR_INVALID_SINGLE_INPUT,
268 )),
269 },
270 len if len % 2 == 0 => build_from_pairs(rest),
271 _ => Err(struct_error(&STRUCT_ERROR_NAME_VALUE_PAIRS)),
272 }
273}
274
275fn build_from_pairs(args: Vec<Value>) -> BuiltinResult<Value> {
276 let mut entries: Vec<FieldEntry> = Vec::new();
277 let mut target_shape: Option<Vec<usize>> = None;
278
279 let mut iter = args.into_iter();
280 while let (Some(name_value), Some(field_value)) = (iter.next(), iter.next()) {
281 let field_name = parse_field_name(&name_value)?;
282 match field_value {
283 Value::Cell(cell) => {
284 let shape = cell.shape.clone();
285 if let Some(existing) = &target_shape {
286 if *existing != shape {
287 return Err(struct_error(&STRUCT_ERROR_CELL_SIZE_MISMATCH));
288 }
289 } else {
290 target_shape = Some(shape);
291 }
292 entries.push(FieldEntry {
293 name: field_name,
294 value: FieldValue::Cell(cell),
295 });
296 }
297 other => entries.push(FieldEntry {
298 name: field_name,
299 value: FieldValue::Single(other),
300 }),
301 }
302 }
303
304 if let Some(shape) = target_shape {
305 build_struct_array(entries, shape)
306 } else {
307 build_scalar_struct(entries)
308 }
309}
310
311fn build_scalar_struct(entries: Vec<FieldEntry>) -> BuiltinResult<Value> {
312 let mut fields = StructValue::new();
313 for entry in entries {
314 match entry.value {
315 FieldValue::Single(value) => {
316 fields.fields.insert(entry.name, value);
317 }
318 FieldValue::Cell(cell) => {
319 let shape = cell.shape.clone();
320 return build_struct_array(
321 vec![FieldEntry {
322 name: entry.name,
323 value: FieldValue::Cell(cell),
324 }],
325 shape,
326 );
327 }
328 }
329 }
330 Ok(Value::Struct(fields))
331}
332
333fn build_struct_array(entries: Vec<FieldEntry>, shape: Vec<usize>) -> BuiltinResult<Value> {
334 let total_len = shape
335 .iter()
336 .try_fold(1usize, |acc, &dim| acc.checked_mul(dim))
337 .ok_or_else(|| struct_error(&STRUCT_ERROR_SIZE_OVERFLOW))?;
338
339 for entry in &entries {
340 if let FieldValue::Cell(cell) = &entry.value {
341 if cell.data.len() != total_len {
342 return Err(struct_error(&STRUCT_ERROR_CELL_SIZE_MISMATCH));
343 }
344 }
345 }
346
347 let mut structs: Vec<Value> = Vec::with_capacity(total_len);
348 for idx in 0..total_len {
349 let mut fields = StructValue::new();
350 for entry in &entries {
351 let value = match &entry.value {
352 FieldValue::Single(val) => val.clone(),
353 FieldValue::Cell(cell) => clone_cell_element(cell, idx)?,
354 };
355 fields.fields.insert(entry.name.clone(), value);
356 }
357 structs.push(Value::Struct(fields));
358 }
359
360 CellArray::new_with_shape(structs, shape)
361 .map(Value::Cell)
362 .map_err(|e| {
363 struct_error_with_message(
364 format!("{}: {e}", STRUCT_ERROR_ASSEMBLE_FAILED.message),
365 &STRUCT_ERROR_ASSEMBLE_FAILED,
366 )
367 })
368}
369
370fn clone_cell_element(cell: &CellArray, index: usize) -> BuiltinResult<Value> {
371 cell.data
372 .get(index)
373 .cloned()
374 .ok_or_else(|| struct_error(&STRUCT_ERROR_CELL_SIZE_MISMATCH))
375}
376
377fn empty_struct_array() -> BuiltinResult<Value> {
378 CellArray::new(Vec::new(), 0, 0)
379 .map(Value::Cell)
380 .map_err(|e| {
381 struct_error_with_message(
382 format!("{}: {e}", STRUCT_ERROR_EMPTY_ARRAY_FAILED.message),
383 &STRUCT_ERROR_EMPTY_ARRAY_FAILED,
384 )
385 })
386}
387
388fn clone_struct_array(array: &CellArray) -> BuiltinResult<Value> {
389 let mut values: Vec<Value> = Vec::with_capacity(array.data.len());
390 for (index, handle) in array.data.iter().enumerate() {
391 let value = handle.clone();
392 if !matches!(value, Value::Struct(_)) {
393 return Err(struct_error_with_message(
394 format!(
395 "{} (element {} is not a struct)",
396 STRUCT_ERROR_STRUCT_ARRAY_CONTENTS.message,
397 index + 1
398 ),
399 &STRUCT_ERROR_STRUCT_ARRAY_CONTENTS,
400 ));
401 }
402 values.push(value);
403 }
404 CellArray::new_with_shape(values, array.shape.clone())
405 .map(Value::Cell)
406 .map_err(|e| {
407 struct_error_with_message(
408 format!("{}: {e}", STRUCT_ERROR_STRUCT_ARRAY_COPY_FAILED.message),
409 &STRUCT_ERROR_STRUCT_ARRAY_COPY_FAILED,
410 )
411 })
412}
413
414fn parse_field_name(value: &Value) -> BuiltinResult<String> {
415 let text = match value {
416 Value::String(s) => s.clone(),
417 Value::StringArray(sa) => {
418 if sa.data.len() == 1 {
419 sa.data[0].clone()
420 } else {
421 return Err(struct_error(&STRUCT_ERROR_FIELD_NAME_SCALAR));
422 }
423 }
424 Value::CharArray(ca) => char_array_to_string(ca)?,
425 _ => return Err(struct_error(&STRUCT_ERROR_FIELD_NAME_TYPE)),
426 };
427
428 validate_field_name(&text)?;
429 Ok(text)
430}
431
432fn char_array_to_string(ca: &CharArray) -> BuiltinResult<String> {
433 if ca.rows > 1 {
434 return Err(struct_error(&STRUCT_ERROR_FIELD_NAME_CHAR_VECTOR));
435 }
436 let mut out = String::with_capacity(ca.data.len());
437 for ch in &ca.data {
438 out.push(*ch);
439 }
440 Ok(out)
441}
442
443fn validate_field_name(name: &str) -> BuiltinResult<()> {
444 if name.is_empty() {
445 return Err(struct_error(&STRUCT_ERROR_FIELD_NAME_EMPTY));
446 }
447
448 if !is_valid_varname(name) {
449 return Err(struct_error_with_message(
450 format!(
451 "{} (got '{name}')",
452 STRUCT_ERROR_FIELD_NAME_START_CHAR.message
453 ),
454 &STRUCT_ERROR_FIELD_NAME_START_CHAR,
455 ));
456 }
457 Ok(())
458}
459
460#[cfg(test)]
461pub(crate) mod tests {
462 use super::*;
463 use crate::builtins::common::identifiers::MATLAB_NAME_LENGTH_MAX;
464 use runmat_accelerate_api::GpuTensorHandle;
465 use runmat_builtins::{CellArray, IntValue, StringArray, StructValue, Tensor};
466
467 #[cfg(feature = "wgpu")]
468 use runmat_accelerate_api::HostTensorView;
469
470 fn error_message(err: crate::RuntimeError) -> String {
471 err.message().to_string()
472 }
473
474 fn run_struct(args: Vec<Value>) -> BuiltinResult<Value> {
475 futures::executor::block_on(struct_builtin(args))
476 }
477
478 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
479 #[test]
480 fn struct_empty() {
481 let Value::Struct(s) = run_struct(Vec::new()).expect("struct") else {
482 panic!("expected struct value");
483 };
484 assert!(s.fields.is_empty());
485 }
486
487 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
488 #[test]
489 fn struct_empty_from_empty_matrix() {
490 let tensor = Tensor::new(Vec::new(), vec![0, 0]).unwrap();
491 let value = run_struct(vec![Value::Tensor(tensor)]).expect("struct([])");
492 match value {
493 Value::Cell(cell) => {
494 assert_eq!(cell.rows, 0);
495 assert_eq!(cell.cols, 0);
496 assert!(cell.data.is_empty());
497 }
498 other => panic!("expected empty struct array, got {other:?}"),
499 }
500 }
501
502 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
503 #[test]
504 fn struct_name_value_pairs() {
505 let args = vec![
506 Value::from("name"),
507 Value::from("Ada"),
508 Value::from("score"),
509 Value::Int(IntValue::I32(42)),
510 ];
511 let Value::Struct(s) = run_struct(args).expect("struct") else {
512 panic!("expected struct value");
513 };
514 assert_eq!(s.fields.len(), 2);
515 assert!(matches!(s.fields.get("name"), Some(Value::String(v)) if v == "Ada"));
516 assert!(matches!(
517 s.fields.get("score"),
518 Some(Value::Int(IntValue::I32(42)))
519 ));
520 }
521
522 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
523 #[test]
524 fn struct_struct_array_from_cells() {
525 let names = CellArray::new(vec![Value::from("Ada"), Value::from("Grace")], 1, 2).unwrap();
526 let ages = CellArray::new(
527 vec![Value::Int(IntValue::I32(36)), Value::Int(IntValue::I32(45))],
528 1,
529 2,
530 )
531 .unwrap();
532 let result = run_struct(vec![
533 Value::from("name"),
534 Value::Cell(names),
535 Value::from("age"),
536 Value::Cell(ages),
537 ])
538 .expect("struct array");
539 let structs = expect_struct_array(result);
540 assert_eq!(structs.len(), 2);
541 assert!(matches!(
542 structs[0].fields.get("name"),
543 Some(Value::String(v)) if v == "Ada"
544 ));
545 assert!(matches!(
546 structs[1].fields.get("age"),
547 Some(Value::Int(IntValue::I32(45)))
548 ));
549 }
550
551 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
552 #[test]
553 fn struct_struct_array_replicates_scalars() {
554 let names = CellArray::new(vec![Value::from("Ada"), Value::from("Grace")], 1, 2).unwrap();
555 let result = run_struct(vec![
556 Value::from("name"),
557 Value::Cell(names),
558 Value::from("department"),
559 Value::from("Research"),
560 ])
561 .expect("struct array");
562 let structs = expect_struct_array(result);
563 assert_eq!(structs.len(), 2);
564 for entry in structs {
565 assert!(matches!(
566 entry.fields.get("department"),
567 Some(Value::String(v)) if v == "Research"
568 ));
569 }
570 }
571
572 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
573 #[test]
574 fn struct_struct_array_cell_size_mismatch_errors() {
575 let names = CellArray::new(vec![Value::from("Ada"), Value::from("Grace")], 1, 2).unwrap();
576 let scores = CellArray::new(vec![Value::Int(IntValue::I32(1))], 1, 1).unwrap();
577 let err = error_message(
578 run_struct(vec![
579 Value::from("name"),
580 Value::Cell(names),
581 Value::from("score"),
582 Value::Cell(scores),
583 ])
584 .unwrap_err(),
585 );
586 assert!(err.contains("matching sizes"));
587 }
588
589 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
590 #[test]
591 fn struct_overwrites_duplicates() {
592 let args = vec![
593 Value::from("version"),
594 Value::Int(IntValue::I32(1)),
595 Value::from("version"),
596 Value::Int(IntValue::I32(2)),
597 ];
598 let Value::Struct(s) = run_struct(args).expect("struct") else {
599 panic!("expected struct value");
600 };
601 assert_eq!(s.fields.len(), 1);
602 assert!(matches!(
603 s.fields.get("version"),
604 Some(Value::Int(IntValue::I32(2)))
605 ));
606 }
607
608 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
609 #[test]
610 fn struct_rejects_odd_arguments() {
611 let err = error_message(run_struct(vec![Value::from("name")]).unwrap_err());
612 assert!(err.contains("name/value pairs"));
613 }
614
615 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
616 #[test]
617 fn struct_rejects_invalid_field_name() {
618 let err = error_message(
619 run_struct(vec![Value::from("1bad"), Value::Int(IntValue::I32(1))]).unwrap_err(),
620 );
621 assert!(err.contains("valid MATLAB identifiers"));
622 }
623
624 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
625 #[test]
626 fn struct_field_names_follow_shared_identifier_rules() {
627 let max_len = "a".repeat(MATLAB_NAME_LENGTH_MAX);
628 let Value::Struct(s) = run_struct(vec![
629 Value::from(max_len.clone()),
630 Value::Int(IntValue::I32(1)),
631 ])
632 .expect("max length field name") else {
633 panic!("expected struct value");
634 };
635 assert!(s.fields.contains_key(&max_len));
636
637 for bad in [
638 "_hidden".to_string(),
639 "for".to_string(),
640 "a".repeat(MATLAB_NAME_LENGTH_MAX + 1),
641 ] {
642 let err =
643 error_message(run_struct(vec![Value::from(bad), Value::Num(1.0)]).unwrap_err());
644 assert!(err.contains("valid MATLAB identifiers"));
645 }
646
647 let Value::Struct(s) =
648 run_struct(vec![Value::from("éclair"), Value::Num(1.0)]).expect("unicode field name")
649 else {
650 panic!("expected struct value");
651 };
652 assert!(s.fields.contains_key("éclair"));
653 }
654
655 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
656 #[test]
657 fn struct_rejects_non_text_field_name() {
658 let err = error_message(
659 run_struct(vec![Value::Num(1.0), Value::Int(IntValue::I32(1))]).unwrap_err(),
660 );
661 assert!(err.contains("strings or character vectors"));
662 }
663
664 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
665 #[test]
666 fn struct_accepts_char_vector_name() {
667 let chars = CharArray::new("field".chars().collect(), 1, 5).unwrap();
668 let args = vec![Value::CharArray(chars), Value::Num(1.0)];
669 let Value::Struct(s) = run_struct(args).expect("struct") else {
670 panic!("expected struct value");
671 };
672 assert!(s.fields.contains_key("field"));
673 }
674
675 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
676 #[test]
677 fn struct_accepts_string_scalar_name() {
678 let sa = StringArray::new(vec!["field".to_string()], vec![1]).unwrap();
679 let args = vec![Value::StringArray(sa), Value::Num(1.0)];
680 let Value::Struct(s) = run_struct(args).expect("struct") else {
681 panic!("expected struct value");
682 };
683 assert!(s.fields.contains_key("field"));
684 }
685
686 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
687 #[test]
688 fn struct_allows_existing_struct_copy() {
689 let mut base = StructValue::new();
690 base.fields
691 .insert("id".to_string(), Value::Int(IntValue::I32(7)));
692 let copy = run_struct(vec![Value::Struct(base.clone())]).expect("struct");
693 assert_eq!(copy, Value::Struct(base));
694 }
695
696 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
697 #[test]
698 fn struct_copies_struct_array_argument() {
699 let mut proto = StructValue::new();
700 proto
701 .fields
702 .insert("id".into(), Value::Int(IntValue::I32(7)));
703 let struct_array = CellArray::new(
704 vec![
705 Value::Struct(proto.clone()),
706 Value::Struct(proto.clone()),
707 Value::Struct(proto.clone()),
708 ],
709 1,
710 3,
711 )
712 .unwrap();
713 let original = struct_array.clone();
714 let result = run_struct(vec![Value::Cell(struct_array)]).expect("struct array clone");
715 let cloned = expect_struct_array(result);
716 let baseline = expect_struct_array(Value::Cell(original));
717 assert_eq!(cloned, baseline);
718 }
719
720 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
721 #[test]
722 fn struct_rejects_cell_argument_without_structs() {
723 let cell = CellArray::new(vec![Value::Num(1.0)], 1, 1).unwrap();
724 let err = error_message(run_struct(vec![Value::Cell(cell)]).unwrap_err());
725 assert!(err.contains("must contain structs"));
726 }
727
728 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
729 #[test]
730 fn struct_preserves_gpu_tensor_handles() {
731 let handle = GpuTensorHandle {
732 shape: vec![2, 2],
733 device_id: 1,
734 buffer_id: 99,
735 };
736 let args = vec![Value::from("data"), Value::GpuTensor(handle.clone())];
737 let Value::Struct(s) = run_struct(args).expect("struct") else {
738 panic!("expected struct value");
739 };
740 assert!(matches!(s.fields.get("data"), Some(Value::GpuTensor(h)) if h == &handle));
741 }
742
743 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
744 #[test]
745 fn struct_struct_array_preserves_gpu_handles() {
746 let first = GpuTensorHandle {
747 shape: vec![1, 1],
748 device_id: 2,
749 buffer_id: 11,
750 };
751 let second = GpuTensorHandle {
752 shape: vec![1, 1],
753 device_id: 2,
754 buffer_id: 12,
755 };
756 let cell = CellArray::new(
757 vec![
758 Value::GpuTensor(first.clone()),
759 Value::GpuTensor(second.clone()),
760 ],
761 1,
762 2,
763 )
764 .unwrap();
765 let result = run_struct(vec![Value::from("payload"), Value::Cell(cell)])
766 .expect("struct array gpu handles");
767 let structs = expect_struct_array(result);
768 assert!(matches!(
769 structs[0].fields.get("payload"),
770 Some(Value::GpuTensor(h)) if h == &first
771 ));
772 assert!(matches!(
773 structs[1].fields.get("payload"),
774 Some(Value::GpuTensor(h)) if h == &second
775 ));
776 }
777
778 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
779 #[test]
780 #[cfg(feature = "wgpu")]
781 fn struct_preserves_gpu_handles_with_registered_provider() {
782 let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
783 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
784 );
785 let provider = runmat_accelerate_api::provider().expect("wgpu provider");
786 let host = HostTensorView {
787 data: &[1.0, 2.0],
788 shape: &[2, 1],
789 };
790 let handle = provider.upload(&host).expect("upload");
791 let args = vec![Value::from("gpu"), Value::GpuTensor(handle.clone())];
792 let Value::Struct(s) = run_struct(args).expect("struct") else {
793 panic!("expected struct value");
794 };
795 assert!(matches!(s.fields.get("gpu"), Some(Value::GpuTensor(h)) if h == &handle));
796 }
797
798 fn expect_struct_array(value: Value) -> Vec<StructValue> {
799 match value {
800 Value::Cell(cell) => cell
801 .data
802 .into_iter()
803 .map(|value| match value {
804 Value::Struct(st) => st,
805 other => panic!("expected struct element, got {other:?}"),
806 })
807 .collect(),
808 Value::Struct(st) => vec![st],
809 other => panic!("expected struct or struct array, got {other:?}"),
810 }
811 }
812}