Skip to main content

runmat_runtime/builtins/structs/core/
orderfields.rs

1//! MATLAB-compatible `orderfields` builtin.
2
3use crate::builtins::common::spec::{
4    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
5    ReductionNaN, ResidencyPolicy, ShapeRequirements,
6};
7use crate::builtins::common::tensor;
8use crate::builtins::structs::type_resolvers::orderfields_type;
9
10use runmat_builtins::{CellArray, StructValue, Tensor, Value};
11use runmat_macros::runtime_builtin;
12use std::cmp::Ordering;
13use std::collections::{HashMap, HashSet};
14
15use crate::{build_runtime_error, BuiltinResult, RuntimeError};
16
17#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::structs::core::orderfields")]
18pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
19    name: "orderfields",
20    op_kind: GpuOpKind::Custom("orderfields"),
21    supported_precisions: &[],
22    broadcast: BroadcastSemantics::None,
23    provider_hooks: &[],
24    constant_strategy: ConstantStrategy::InlineLiteral,
25    residency: ResidencyPolicy::InheritInputs,
26    nan_mode: ReductionNaN::Include,
27    two_pass_threshold: None,
28    workgroup_size: None,
29    accepts_nan_mode: false,
30    notes: "Host-only metadata manipulation; struct values that live on the GPU remain resident.",
31};
32
33#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::structs::core::orderfields")]
34pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
35    name: "orderfields",
36    shape: ShapeRequirements::Any,
37    constant_strategy: ConstantStrategy::InlineLiteral,
38    elementwise: None,
39    reduction: None,
40    emits_nan: false,
41    notes: "Reordering fields is a metadata operation and does not participate in fusion planning.",
42};
43
44const MESSAGE_ID_TOO_MANY_INPUTS: &str = "orderfields:TooManyInputs";
45const MESSAGE_ID_INVALID_INPUT: &str = "orderfields:InvalidInput";
46const MESSAGE_ID_EMPTY_STRUCT_ARRAY: &str = "orderfields:EmptyStructArray";
47const MESSAGE_ID_NO_FIELDS: &str = "orderfields:NoFields";
48const MESSAGE_ID_INVALID_STRUCT_ARRAY: &str = "orderfields:InvalidStructArray";
49const MESSAGE_ID_INVALID_STRUCT_CONTENTS: &str = "orderfields:InvalidStructContents";
50const MESSAGE_ID_REBUILD_FAILED: &str = "orderfields:RebuildFailed";
51const MESSAGE_ID_INVALID_REFERENCE: &str = "orderfields:InvalidReference";
52const MESSAGE_ID_INVALID_NAME_LIST: &str = "orderfields:InvalidFieldNameList";
53const MESSAGE_ID_EMPTY_FIELD_NAME: &str = "orderfields:EmptyFieldName";
54const MESSAGE_ID_INVALID_PERMUTATION: &str = "orderfields:InvalidPermutation";
55const MESSAGE_ID_INDEX_NOT_INTEGER: &str = "orderfields:IndexNotInteger";
56const MESSAGE_ID_INDEX_OUT_OF_RANGE: &str = "orderfields:IndexOutOfRange";
57const MESSAGE_ID_INDEX_DUPLICATE: &str = "orderfields:DuplicateIndex";
58const MESSAGE_ID_FIELD_MISMATCH: &str = "orderfields:FieldMismatch";
59const MESSAGE_ID_UNKNOWN_FIELD: &str = "orderfields:UnknownField";
60const MESSAGE_ID_DUPLICATE_FIELD: &str = "orderfields:DuplicateField";
61const MESSAGE_ID_MISSING_FIELD: &str = "orderfields:MissingField";
62const MESSAGE_ID_INVALID_ORDER_ARGUMENT: &str = "orderfields:InvalidOrderArgument";
63const MESSAGE_ID_INTERNAL: &str = "orderfields:InternalError";
64
65fn orderfields_flow(message_id: &str, message: impl Into<String>) -> RuntimeError {
66    build_runtime_error(message)
67        .with_builtin("orderfields")
68        .with_identifier(message_id)
69        .build()
70}
71
72#[runtime_builtin(
73    name = "orderfields",
74    category = "structs/core",
75    summary = "Reorder structure field definitions alphabetically or using a supplied order.",
76    keywords = "orderfields,struct,reorder fields,alphabetical,struct array",
77    type_resolver(orderfields_type),
78    builtin_path = "crate::builtins::structs::core::orderfields"
79)]
80async fn orderfields_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
81    let eval = evaluate(value, &rest)?;
82    if let Some(out_count) = crate::output_count::current_output_count() {
83        if out_count == 0 {
84            return Ok(Value::OutputList(Vec::new()));
85        }
86        let (ordered, permutation) = eval.into_values();
87        let mut outputs = vec![ordered];
88        if out_count >= 2 {
89            outputs.push(permutation);
90        }
91        return Ok(crate::output_count::output_list_with_padding(
92            out_count, outputs,
93        ));
94    }
95    Ok(eval.into_ordered_value())
96}
97
98/// Evaluate the `orderfields` builtin once and expose both outputs.
99pub fn evaluate(value: Value, rest: &[Value]) -> BuiltinResult<OrderFieldsEvaluation> {
100    if rest.len() > 1 {
101        return Err(orderfields_flow(
102            MESSAGE_ID_TOO_MANY_INPUTS,
103            "orderfields: expected at most two input arguments",
104        ));
105    }
106    let order_arg = rest.first();
107
108    match value {
109        Value::Struct(struct_value) => {
110            let original: Vec<String> = struct_value.field_names().cloned().collect();
111            let order = resolve_order(&struct_value, order_arg)?;
112            let permutation = permutation_from(&original, &order)?;
113            let permutation = permutation_tensor(permutation)?;
114            let reordered = reorder_struct(&struct_value, &order)?;
115            Ok(OrderFieldsEvaluation::new(
116                Value::Struct(reordered),
117                permutation,
118            ))
119        }
120        Value::Cell(cell) => {
121            if cell.data.is_empty() {
122                let permutation = permutation_tensor(Vec::new())?;
123                if let Some(arg) = order_arg {
124                    if let Some(reference) = extract_reference_struct(arg)? {
125                        if reference.fields.is_empty() {
126                            return Ok(OrderFieldsEvaluation::new(Value::Cell(cell), permutation));
127                        } else {
128                            return Err(orderfields_flow(
129                                MESSAGE_ID_EMPTY_STRUCT_ARRAY,
130                                "orderfields: empty struct arrays cannot adopt a non-empty reference order",
131                            ));
132                        }
133                    }
134                    if let Some(names) = extract_name_list(arg)? {
135                        if names.is_empty() {
136                            return Ok(OrderFieldsEvaluation::new(Value::Cell(cell), permutation));
137                        }
138                        return Err(orderfields_flow(
139                            MESSAGE_ID_NO_FIELDS,
140                            "orderfields: struct array has no fields to reorder",
141                        ));
142                    }
143                    if let Value::Tensor(tensor) = arg {
144                        if tensor.data.is_empty() {
145                            return Ok(OrderFieldsEvaluation::new(Value::Cell(cell), permutation));
146                        }
147                        return Err(orderfields_flow(
148                            MESSAGE_ID_NO_FIELDS,
149                            "orderfields: struct array has no fields to reorder",
150                        ));
151                    }
152                    return Err(orderfields_flow(
153                        MESSAGE_ID_NO_FIELDS,
154                        "orderfields: struct array has no fields to reorder",
155                    ));
156                }
157                return Ok(OrderFieldsEvaluation::new(Value::Cell(cell), permutation));
158            }
159            let first = extract_struct_from_cell(&cell, 0)?;
160            let original: Vec<String> = first.field_names().cloned().collect();
161            let order = resolve_order(&first, order_arg)?;
162            let permutation = permutation_from(&original, &order)?;
163            let permutation = permutation_tensor(permutation)?;
164            let reordered = reorder_struct_array(&cell, &order)?;
165            Ok(OrderFieldsEvaluation::new(
166                Value::Cell(reordered),
167                permutation,
168            ))
169        }
170        other => Err(orderfields_flow(
171            MESSAGE_ID_INVALID_INPUT,
172            format!("orderfields: first argument must be a struct or struct array (got {other:?})"),
173        )),
174    }
175}
176
177pub struct OrderFieldsEvaluation {
178    ordered: Value,
179    permutation: Tensor,
180}
181
182impl OrderFieldsEvaluation {
183    fn new(ordered: Value, permutation: Tensor) -> Self {
184        Self {
185            ordered,
186            permutation,
187        }
188    }
189
190    pub fn into_ordered_value(self) -> Value {
191        self.ordered
192    }
193
194    pub fn permutation_value(&self) -> Value {
195        tensor::tensor_into_value(self.permutation.clone())
196    }
197
198    pub fn into_values(self) -> (Value, Value) {
199        let perm = tensor::tensor_into_value(self.permutation);
200        (self.ordered, perm)
201    }
202}
203
204fn reorder_struct_array(array: &CellArray, order: &[String]) -> BuiltinResult<CellArray> {
205    let mut reordered_elems = Vec::with_capacity(array.data.len());
206    for (index, handle) in array.data.iter().enumerate() {
207        let value = unsafe { &*handle.as_raw() };
208        let Value::Struct(st) = value else {
209            return Err(orderfields_flow(
210                MESSAGE_ID_INVALID_STRUCT_ARRAY,
211                format!(
212                    "orderfields: struct array element {} is not a struct",
213                    index + 1
214                ),
215            ));
216        };
217        ensure_same_field_set(order, st)?;
218        let reordered = reorder_struct(st, order)?;
219        reordered_elems.push(Value::Struct(reordered));
220    }
221    CellArray::new_with_shape(reordered_elems, array.shape.clone()).map_err(|e| {
222        orderfields_flow(
223            MESSAGE_ID_REBUILD_FAILED,
224            format!("orderfields: failed to rebuild struct array: {e}"),
225        )
226    })
227}
228
229fn reorder_struct(struct_value: &StructValue, order: &[String]) -> BuiltinResult<StructValue> {
230    let mut reordered = StructValue::new();
231    for name in order {
232        let value = struct_value
233            .fields
234            .get(name)
235            .ok_or_else(|| missing_field(name))?
236            .clone();
237        reordered.fields.insert(name.clone(), value);
238    }
239    Ok(reordered)
240}
241
242fn resolve_order(
243    struct_value: &StructValue,
244    order_arg: Option<&Value>,
245) -> BuiltinResult<Vec<String>> {
246    let mut current: Vec<String> = struct_value.field_names().cloned().collect();
247    if let Some(arg) = order_arg {
248        if let Some(reference) = extract_reference_struct(arg)? {
249            let reference_names: Vec<String> = reference.field_names().cloned().collect();
250            ensure_same_field_set(&reference_names, struct_value)?;
251            return Ok(reference_names);
252        }
253
254        if let Some(names) = extract_name_list(arg)? {
255            ensure_same_field_set(&names, struct_value)?;
256            return Ok(names);
257        }
258
259        if let Some(permutation) = extract_indices(&current, arg)? {
260            return Ok(permutation);
261        }
262
263        return Err(orderfields_flow(
264            MESSAGE_ID_INVALID_ORDER_ARGUMENT,
265            "orderfields: unrecognised ordering argument",
266        ));
267    }
268
269    sort_field_names(&mut current);
270    Ok(current)
271}
272
273fn permutation_from(original: &[String], order: &[String]) -> BuiltinResult<Vec<f64>> {
274    let mut index_map = HashMap::with_capacity(original.len());
275    for (idx, name) in original.iter().enumerate() {
276        index_map.insert(name.as_str(), idx);
277    }
278    let mut indices = Vec::with_capacity(order.len());
279    for name in order {
280        let Some(position) = index_map.get(name.as_str()) else {
281            return Err(missing_field(name));
282        };
283        indices.push((*position as f64) + 1.0);
284    }
285    Ok(indices)
286}
287
288fn permutation_tensor(indices: Vec<f64>) -> BuiltinResult<Tensor> {
289    let rows = indices.len();
290    let shape = vec![rows, 1];
291    Tensor::new(indices, shape)
292        .map_err(|e| orderfields_flow(MESSAGE_ID_INTERNAL, format!("orderfields: {e}")))
293}
294
295fn sort_field_names(names: &mut [String]) {
296    names.sort_by(|a, b| {
297        let lower_a = a.to_ascii_lowercase();
298        let lower_b = b.to_ascii_lowercase();
299        match lower_a.cmp(&lower_b) {
300            Ordering::Equal => a.cmp(b),
301            other => other,
302        }
303    });
304}
305
306fn extract_reference_struct(value: &Value) -> BuiltinResult<Option<StructValue>> {
307    match value {
308        Value::Struct(st) => Ok(Some(st.clone())),
309        Value::Cell(cell) => {
310            let mut first: Option<StructValue> = None;
311            for (index, handle) in cell.data.iter().enumerate() {
312                let value = unsafe { &*handle.as_raw() };
313                if let Value::Struct(st) = value {
314                    if first.is_none() {
315                        first = Some(st.clone());
316                    }
317                } else if first.is_some() {
318                    return Err(orderfields_flow(
319                        MESSAGE_ID_INVALID_REFERENCE,
320                        format!(
321                            "orderfields: reference struct array element {} is not a struct",
322                            index + 1
323                        ),
324                    ));
325                } else {
326                    return Ok(None);
327                }
328            }
329            Ok(first)
330        }
331        _ => Ok(None),
332    }
333}
334
335fn extract_name_list(arg: &Value) -> BuiltinResult<Option<Vec<String>>> {
336    match arg {
337        Value::Cell(cell) => {
338            let mut names = Vec::with_capacity(cell.data.len());
339            for (index, handle) in cell.data.iter().enumerate() {
340                let value = unsafe { &*handle.as_raw() };
341                let text = scalar_string(value).ok_or_else(|| {
342                    orderfields_flow(
343                        MESSAGE_ID_INVALID_NAME_LIST,
344                        format!(
345                            "orderfields: cell array element {} must be a string or character vector",
346                            index + 1
347                        ),
348                    )
349                })?;
350                if text.is_empty() {
351                    return Err(orderfields_flow(
352                        MESSAGE_ID_EMPTY_FIELD_NAME,
353                        "orderfields: field names must be nonempty",
354                    ));
355                }
356                names.push(text);
357            }
358            Ok(Some(names))
359        }
360        Value::StringArray(sa) => Ok(Some(sa.data.clone())),
361        Value::CharArray(ca) => {
362            if ca.rows == 0 {
363                return Ok(Some(Vec::new()));
364            }
365            let mut names = Vec::with_capacity(ca.rows);
366            for row in 0..ca.rows {
367                let start = row * ca.cols;
368                let end = start + ca.cols;
369                let mut text: String = ca.data[start..end].iter().collect();
370                while text.ends_with(' ') {
371                    text.pop();
372                }
373                if text.is_empty() {
374                    return Err(orderfields_flow(
375                        MESSAGE_ID_EMPTY_FIELD_NAME,
376                        "orderfields: field names must be nonempty",
377                    ));
378                }
379                names.push(text);
380            }
381            Ok(Some(names))
382        }
383        _ => Ok(None),
384    }
385}
386
387fn extract_indices(current: &[String], arg: &Value) -> BuiltinResult<Option<Vec<String>>> {
388    let Value::Tensor(tensor) = arg else {
389        return Ok(None);
390    };
391    if tensor.data.is_empty() && current.is_empty() {
392        return Ok(Some(Vec::new()));
393    }
394    if tensor.data.len() != current.len() {
395        return Err(orderfields_flow(
396            MESSAGE_ID_INVALID_PERMUTATION,
397            "orderfields: index vector must permute every field exactly once",
398        ));
399    }
400    let mut seen = HashSet::with_capacity(current.len());
401    let mut order = Vec::with_capacity(current.len());
402    for value in &tensor.data {
403        if !value.is_finite() || value.fract() != 0.0 {
404            return Err(orderfields_flow(
405                MESSAGE_ID_INDEX_NOT_INTEGER,
406                "orderfields: index vector must contain integers",
407            ));
408        }
409        let idx = *value as isize;
410        if idx < 1 || idx as usize > current.len() {
411            return Err(orderfields_flow(
412                MESSAGE_ID_INDEX_OUT_OF_RANGE,
413                "orderfields: index vector element out of range",
414            ));
415        }
416        let zero_based = (idx as usize) - 1;
417        if !seen.insert(zero_based) {
418            return Err(orderfields_flow(
419                MESSAGE_ID_INDEX_DUPLICATE,
420                "orderfields: index vector contains duplicate positions",
421            ));
422        }
423        order.push(current[zero_based].clone());
424    }
425    Ok(Some(order))
426}
427
428fn ensure_same_field_set(order: &[String], original: &StructValue) -> BuiltinResult<()> {
429    if order.len() != original.fields.len() {
430        return Err(orderfields_flow(
431            MESSAGE_ID_FIELD_MISMATCH,
432            "orderfields: field names must match the struct exactly",
433        ));
434    }
435    let mut seen = HashSet::with_capacity(order.len());
436    let original_set: HashSet<&str> = original.field_names().map(|s| s.as_str()).collect();
437    for name in order {
438        if !original_set.contains(name.as_str()) {
439            return Err(orderfields_flow(
440                MESSAGE_ID_UNKNOWN_FIELD,
441                format!("orderfields: unknown field '{name}' in requested order"),
442            ));
443        }
444        if !seen.insert(name.as_str()) {
445            return Err(orderfields_flow(
446                MESSAGE_ID_DUPLICATE_FIELD,
447                format!("orderfields: duplicate field '{name}' in requested order"),
448            ));
449        }
450    }
451    Ok(())
452}
453
454fn extract_struct_from_cell(cell: &CellArray, index: usize) -> BuiltinResult<StructValue> {
455    let value = unsafe { &*cell.data[index].as_raw() };
456    match value {
457        Value::Struct(st) => Ok(st.clone()),
458        other => Err(orderfields_flow(
459            MESSAGE_ID_INVALID_STRUCT_CONTENTS,
460            format!("orderfields: expected struct array contents to be structs (found {other:?})"),
461        )),
462    }
463}
464
465fn scalar_string(value: &Value) -> Option<String> {
466    match value {
467        Value::String(s) => Some(s.clone()),
468        Value::StringArray(sa) if sa.data.len() == 1 => Some(sa.data[0].clone()),
469        Value::CharArray(ca) if ca.rows == 1 => {
470            let mut text: String = ca.data.iter().collect();
471            while text.ends_with(' ') {
472                text.pop();
473            }
474            Some(text)
475        }
476        _ => None,
477    }
478}
479
480fn missing_field(name: &str) -> RuntimeError {
481    orderfields_flow(
482        MESSAGE_ID_MISSING_FIELD,
483        format!("orderfields: field '{name}' does not exist on the struct"),
484    )
485}
486
487#[cfg(test)]
488pub(crate) mod tests {
489    use super::*;
490    use futures::executor::block_on;
491    use runmat_builtins::{CellArray, CharArray, StringArray, Tensor};
492
493    fn run_orderfields(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
494        block_on(super::orderfields_builtin(value, rest))
495    }
496
497    fn assert_error_identifier(error: RuntimeError, expected: &str) {
498        assert_eq!(error.identifier(), Some(expected));
499    }
500
501    fn field_order(struct_value: &StructValue) -> Vec<String> {
502        struct_value.field_names().cloned().collect()
503    }
504
505    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
506    #[test]
507    fn default_sorts_alphabetically() {
508        let mut st = StructValue::new();
509        st.fields.insert("beta".to_string(), Value::Num(2.0));
510        st.fields.insert("alpha".to_string(), Value::Num(1.0));
511        st.fields.insert("gamma".to_string(), Value::Num(3.0));
512
513        let result = run_orderfields(Value::Struct(st), Vec::new()).expect("orderfields");
514        let Value::Struct(sorted) = result else {
515            panic!("expected struct result");
516        };
517        assert_eq!(
518            field_order(&sorted),
519            vec!["alpha".to_string(), "beta".to_string(), "gamma".to_string()]
520        );
521    }
522
523    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
524    #[test]
525    fn reorder_with_cell_name_list() {
526        let mut st = StructValue::new();
527        st.fields.insert("a".to_string(), Value::Num(1.0));
528        st.fields.insert("b".to_string(), Value::Num(2.0));
529        st.fields.insert("c".to_string(), Value::Num(3.0));
530        let names = CellArray::new(
531            vec![Value::from("c"), Value::from("a"), Value::from("b")],
532            1,
533            3,
534        )
535        .expect("cell");
536
537        let reordered =
538            run_orderfields(Value::Struct(st), vec![Value::Cell(names)]).expect("orderfields");
539        let Value::Struct(result) = reordered else {
540            panic!("expected struct result");
541        };
542        assert_eq!(
543            field_order(&result),
544            vec!["c".to_string(), "a".to_string(), "b".to_string()]
545        );
546    }
547
548    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
549    #[test]
550    fn reorder_with_string_array_names() {
551        let mut st = StructValue::new();
552        st.fields.insert("alpha".to_string(), Value::Num(1.0));
553        st.fields.insert("beta".to_string(), Value::Num(2.0));
554        st.fields.insert("gamma".to_string(), Value::Num(3.0));
555
556        let strings = StringArray::new(
557            vec!["gamma".into(), "alpha".into(), "beta".into()],
558            vec![1, 3],
559        )
560        .expect("string array");
561
562        let result = run_orderfields(Value::Struct(st), vec![Value::StringArray(strings)])
563            .expect("orderfields");
564        let Value::Struct(sorted) = result else {
565            panic!("expected struct result");
566        };
567        assert_eq!(
568            field_order(&sorted),
569            vec!["gamma".to_string(), "alpha".to_string(), "beta".to_string()]
570        );
571    }
572
573    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
574    #[test]
575    fn reorder_with_char_array_names() {
576        let mut st = StructValue::new();
577        st.fields.insert("cat".to_string(), Value::Num(1.0));
578        st.fields.insert("ant".to_string(), Value::Num(2.0));
579        st.fields.insert("bat".to_string(), Value::Num(3.0));
580
581        let data = vec!['b', 'a', 't', 'c', 'a', 't', 'a', 'n', 't'];
582        let char_array = CharArray::new(data, 3, 3).expect("char array");
583
584        let result =
585            run_orderfields(Value::Struct(st), vec![Value::CharArray(char_array)]).expect("order");
586        let Value::Struct(sorted) = result else {
587            panic!("expected struct result");
588        };
589        assert_eq!(
590            field_order(&sorted),
591            vec!["bat".to_string(), "cat".to_string(), "ant".to_string()]
592        );
593    }
594
595    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
596    #[test]
597    fn reorder_with_reference_struct() {
598        let mut source = StructValue::new();
599        source.fields.insert("y".to_string(), Value::Num(2.0));
600        source.fields.insert("x".to_string(), Value::Num(1.0));
601
602        let mut reference = StructValue::new();
603        reference.fields.insert("x".to_string(), Value::Num(0.0));
604        reference.fields.insert("y".to_string(), Value::Num(0.0));
605
606        let result = run_orderfields(
607            Value::Struct(source),
608            vec![Value::Struct(reference.clone())],
609        )
610        .expect("orderfields");
611        let Value::Struct(reordered) = result else {
612            panic!("expected struct result");
613        };
614        assert_eq!(
615            field_order(&reordered),
616            vec!["x".to_string(), "y".to_string()]
617        );
618    }
619
620    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
621    #[test]
622    fn reorder_with_index_vector() {
623        let mut st = StructValue::new();
624        st.fields.insert("first".to_string(), Value::Num(1.0));
625        st.fields.insert("second".to_string(), Value::Num(2.0));
626        st.fields.insert("third".to_string(), Value::Num(3.0));
627
628        let permutation = Tensor::new(vec![3.0, 1.0, 2.0], vec![1, 3]).expect("tensor permutation");
629        let result = run_orderfields(Value::Struct(st), vec![Value::Tensor(permutation)])
630            .expect("orderfields");
631        let Value::Struct(reordered) = result else {
632            panic!("expected struct result");
633        };
634        assert_eq!(
635            field_order(&reordered),
636            vec![
637                "third".to_string(),
638                "first".to_string(),
639                "second".to_string()
640            ]
641        );
642    }
643
644    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
645    #[test]
646    fn index_vector_must_be_integers() {
647        let mut st = StructValue::new();
648        st.fields.insert("one".to_string(), Value::Num(1.0));
649        st.fields.insert("two".to_string(), Value::Num(2.0));
650
651        let permutation = Tensor::new(vec![1.0, 1.5], vec![1, 2]).expect("tensor");
652        let err = run_orderfields(Value::Struct(st), vec![Value::Tensor(permutation)]).unwrap_err();
653        assert_error_identifier(err, MESSAGE_ID_INDEX_NOT_INTEGER);
654    }
655
656    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
657    #[test]
658    fn permutation_vector_matches_original_positions() {
659        let mut st = StructValue::new();
660        st.fields.insert("beta".to_string(), Value::Num(2.0));
661        st.fields.insert("alpha".to_string(), Value::Num(1.0));
662        st.fields.insert("gamma".to_string(), Value::Num(3.0));
663
664        let eval = evaluate(Value::Struct(st), &[]).expect("evaluate");
665        let perm = eval.permutation_value();
666        match perm {
667            Value::Tensor(t) => assert_eq!(t.data, vec![2.0, 1.0, 3.0]),
668            other => panic!("expected tensor permutation, got {other:?}"),
669        }
670        let Value::Struct(ordered) = eval.into_ordered_value() else {
671            panic!("expected struct result");
672        };
673        assert_eq!(
674            field_order(&ordered),
675            vec!["alpha".to_string(), "beta".to_string(), "gamma".to_string()]
676        );
677    }
678
679    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
680    #[test]
681    fn reorder_struct_array() {
682        let mut first = StructValue::new();
683        first.fields.insert("b".to_string(), Value::Num(1.0));
684        first.fields.insert("a".to_string(), Value::Num(2.0));
685        let mut second = StructValue::new();
686        second.fields.insert("b".to_string(), Value::Num(3.0));
687        second.fields.insert("a".to_string(), Value::Num(4.0));
688        let array = CellArray::new_with_shape(
689            vec![Value::Struct(first), Value::Struct(second)],
690            vec![1, 2],
691        )
692        .expect("struct array");
693        let names =
694            CellArray::new(vec![Value::from("a"), Value::from("b")], 1, 2).expect("cell names");
695
696        let result =
697            run_orderfields(Value::Cell(array), vec![Value::Cell(names)]).expect("orderfields");
698        let Value::Cell(reordered) = result else {
699            panic!("expected cell array");
700        };
701        for handle in &reordered.data {
702            let Value::Struct(st) = (unsafe { &*handle.as_raw() }) else {
703                panic!("expected struct element");
704            };
705            assert_eq!(field_order(st), vec!["a".to_string(), "b".to_string()]);
706        }
707    }
708
709    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
710    #[test]
711    fn struct_array_permutation_reuses_order() {
712        let mut first = StructValue::new();
713        first.fields.insert("z".to_string(), Value::Num(1.0));
714        first.fields.insert("x".to_string(), Value::Num(2.0));
715        first.fields.insert("y".to_string(), Value::Num(3.0));
716
717        let mut second = StructValue::new();
718        second.fields.insert("z".to_string(), Value::Num(4.0));
719        second.fields.insert("x".to_string(), Value::Num(5.0));
720        second.fields.insert("y".to_string(), Value::Num(6.0));
721
722        let array = CellArray::new_with_shape(
723            vec![Value::Struct(first), Value::Struct(second)],
724            vec![1, 2],
725        )
726        .expect("struct array");
727
728        let eval = evaluate(Value::Cell(array), &[]).expect("evaluate");
729        let perm = eval.permutation_value();
730        match perm {
731            Value::Tensor(t) => assert_eq!(t.data, vec![2.0, 3.0, 1.0]),
732            other => panic!("expected tensor permutation, got {other:?}"),
733        }
734    }
735
736    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
737    #[test]
738    fn rejects_unknown_field() {
739        let mut st = StructValue::new();
740        st.fields.insert("alpha".to_string(), Value::Num(1.0));
741        st.fields.insert("beta".to_string(), Value::Num(2.0));
742        let err = run_orderfields(
743            Value::Struct(st),
744            vec![Value::Cell(
745                CellArray::new(vec![Value::from("beta"), Value::from("gamma")], 1, 2)
746                    .expect("cell"),
747            )],
748        )
749        .unwrap_err();
750        assert_error_identifier(err, MESSAGE_ID_UNKNOWN_FIELD);
751    }
752
753    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
754    #[test]
755    fn duplicate_field_names_rejected() {
756        let mut st = StructValue::new();
757        st.fields.insert("alpha".to_string(), Value::Num(1.0));
758        st.fields.insert("beta".to_string(), Value::Num(2.0));
759
760        let names =
761            CellArray::new(vec![Value::from("alpha"), Value::from("alpha")], 1, 2).expect("cell");
762        let err = run_orderfields(Value::Struct(st), vec![Value::Cell(names)]).unwrap_err();
763        assert_error_identifier(err, MESSAGE_ID_DUPLICATE_FIELD);
764    }
765
766    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
767    #[test]
768    fn reference_struct_mismatch_errors() {
769        let mut source = StructValue::new();
770        source.fields.insert("x".to_string(), Value::Num(1.0));
771        source.fields.insert("y".to_string(), Value::Num(2.0));
772
773        let mut reference = StructValue::new();
774        reference.fields.insert("x".to_string(), Value::Num(0.0));
775
776        let err =
777            run_orderfields(Value::Struct(source), vec![Value::Struct(reference)]).unwrap_err();
778        assert_error_identifier(err, MESSAGE_ID_FIELD_MISMATCH);
779    }
780
781    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
782    #[test]
783    fn invalid_order_argument_type_errors() {
784        let mut st = StructValue::new();
785        st.fields.insert("x".to_string(), Value::Num(1.0));
786
787        let err = run_orderfields(Value::Struct(st), vec![Value::Num(1.0)]).unwrap_err();
788        assert_error_identifier(err, MESSAGE_ID_INVALID_ORDER_ARGUMENT);
789    }
790
791    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
792    #[test]
793    fn empty_struct_array_nonempty_reference_errors() {
794        let empty = CellArray::new(Vec::new(), 0, 0).expect("empty struct array");
795        let mut reference = StructValue::new();
796        reference
797            .fields
798            .insert("field".to_string(), Value::Num(1.0));
799
800        let err = run_orderfields(Value::Cell(empty), vec![Value::Struct(reference)]).unwrap_err();
801        assert_error_identifier(err, MESSAGE_ID_EMPTY_STRUCT_ARRAY);
802    }
803}