Skip to main content

runmat_runtime/builtins/structs/core/
struct.rs

1//! MATLAB-compatible `struct` builtin.
2
3use 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}