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::{register_builtin_fusion_spec, register_builtin_gpu_spec};
9use runmat_builtins::{CellArray, StructValue, Tensor, Value};
10use runmat_macros::runtime_builtin;
11use std::cmp::Ordering;
12use std::collections::{HashMap, HashSet};
13
14#[cfg(feature = "doc_export")]
15use crate::register_builtin_doc_text;
16
17#[cfg(feature = "doc_export")]
18pub const DOC_MD: &str = r#"---
19title: "orderfields"
20category: "structs/core"
21keywords: ["orderfields", "struct", "reorder fields", "alphabetical", "struct array", "field order"]
22summary: "Reorder structure field definitions alphabetically or according to a custom order."
23references: []
24gpu_support:
25  elementwise: false
26  reduction: false
27  precisions: []
28  broadcasting: "none"
29  notes: "Runs entirely on the host. When structs contain GPU-resident values, those handles remain on the device."
30fusion:
31  elementwise: false
32  reduction: false
33  max_inputs: 1
34  constants: "inline"
35requires_feature: null
36tested:
37  unit: "builtins::structs::core::orderfields::tests"
38  integration: null
39---
40
41# What does the `orderfields` function do in MATLAB / RunMat?
42`orderfields(S)` reorders the field definitions of a scalar struct or struct array. By default the fields
43are sorted alphabetically. Optional arguments let you match the order of another struct, provide an explicit
44list of names, or supply a permutation vector.
45
46## How does the `orderfields` function behave in MATLAB / RunMat?
47- Works with scalar structs and struct arrays (RunMat stores struct arrays internally as cell arrays of structs).
48- The default behaviour `orderfields(S)` sorts field names alphabetically using MATLAB's case-insensitive ordering.
49- `orderfields(S, referenceStruct)` matches the order of `referenceStruct`. Both structs must contain the same field names.
50- `orderfields(S, {'b','a','c'})` or `orderfields(S, string(['b','a','c']))` uses the supplied list of field names.
51- `orderfields(S, [2 1 3])` reorders fields using a permutation vector that references the current field order.
52- `[T, P] = orderfields(S, ___)` returns the reordered struct (or struct array) `T` and a permutation vector `P` whose elements are the original 1-based field positions. Reuse `P` to apply the same order to other structs that share the field set.
53- The function never copies field contents unnecessarily—values (including GPU handles) are re-used and remain resident.
54- Errors are raised when requested field names do not match the struct, when indices are invalid, or when the input is not a struct.
55
56## `orderfields` Function GPU Execution Behaviour
57`orderfields` operates on host-side struct metadata only. When a struct contains GPU tensors or handles, the handles
58remain valid and resident on the device. No kernels are dispatched and no data is gathered or copied between host and device.
59
60## GPU residency in RunMat (Do I need `gpuArray`?)
61No. Struct field reordering never moves or converts the values stored inside the struct. Existing GPU handles remain on
62the device. You can freely combine `orderfields` with `gpuArray`, `gather`, or auto-offload features without
63affecting residency.
64
65## Examples of using the `orderfields` function in MATLAB / RunMat
66
67### How to sort struct fields alphabetically
68```matlab
69s = struct("beta", 2, "alpha", 1, "gamma", 3);
70t = orderfields(s);
71fieldnames(t)
72```
73
74Expected output:
75```matlab
76ans =
77    {'alpha'}
78    {'beta'}
79    {'gamma'}
80```
81
82### Match the field order of another struct
83```matlab
84source = struct("y", 20, "x", 10);
85template = struct("x", 0, "y", 0);
86[aligned, order] = orderfields(source, template);
87```
88`aligned` now has fields ordered `x`, then `y`. The permutation vector `order` is `[2 1]`, indicating that field 2 (`x`) moved to the first position.
89
90### Reorder fields with a cell array of names
91```matlab
92s = struct("a", 1, "b", 2, "c", 3);
93u = orderfields(s, {"c", "a", "b"});
94fieldnames(u)
95```
96
97Expected output:
98```matlab
99ans =
100    {'c'}
101    {'a'}
102    {'b'}
103```
104
105### Reorder fields with an index vector
106```matlab
107s = struct("first", 1, "second", 2, "third", 3);
108permuted = orderfields(s, [3 1 2]);
109fieldnames(permuted)
110```
111
112Expected output:
113```matlab
114ans =
115    {'third'}
116    {'first'}
117    {'second'}
118```
119
120### Apply a custom order to every element of a struct array
121```matlab
122records = struct("name", {"Ada", "Grace"}, "id", {101, 102});
123[reordered, perm] = orderfields(records, {"id", "name"});
124{reordered.id}
125```
126
127Expected output:
128```matlab
129ans =
130    {[101]}    {[102]}
131```
132
133The permutation vector `perm` is `[2 1]`, which you can reuse with `orderfields(otherStruct, perm)` for any struct that contains the same fields.
134
135### Sort fields in descending alphabetical order
136```matlab
137s = struct("alpha", 1, "delta", 4, "beta", 2);
138names = string(fieldnames(s));
139desc = orderfields(s, flip(names));
140fieldnames(desc)
141```
142
143Expected output:
144```matlab
145ans =
146    {'delta'}
147    {'beta'}
148    {'alpha'}
149```
150`flip` reverses the alphabetical list returned by `fieldnames`, so `orderfields` applies the desired descending order without needing a special mode argument.
151
152## FAQ
153
154### What argument forms does `orderfields` accept?
155You can pass a reference struct (scalar or struct array), a cell array or string array of field names, or a numeric
156permutation vector. Every variant must reference each existing field exactly once.
157
158### Does the reference struct have to contain the same fields?
159Yes. RunMat mirrors MATLAB and requires that the reference struct contain exactly the same field names. Missing or extra
160fields raise an error.
161
162### Can I reorder struct arrays?
163Yes. Every element in the struct array is reordered using the same field order. The array must contain structs only.
164
165### How are numeric vectors interpreted?
166Numeric vectors are treated as permutations of the current field order. Values must be positive integers that reference
167each existing field exactly once.
168
169### What happens when I pass duplicate field names?
170Duplicates are rejected with an error. Every field must appear exactly once in the requested order.
171
172### Does `orderfields` gather GPU data back to the CPU?
173No. The builtin only reorders metadata in the struct. GPU handles remain on the device and are not touched.
174
175### Can I reorder an empty struct array?
176Empty struct arrays are returned unchanged. Because RunMat stores field metadata per element, you must supply at least one element before an explicit order can be derived.
177
178### How do I maintain the existing order?
179Capture the permutation output once: `[~, P] = orderfields(S);`. You can later call `orderfields(S, P)` (or apply the
180same `P` to another struct with identical fields) to reapply the original order.
181
182### Does `orderfields` affect nested structs?
183Only the top-level struct passed to `orderfields` is reordered. Nested structs retain their current order.
184
185## See Also
186[struct](./struct), [fieldnames](./fieldnames), [getfield](./getfield), [setfield](./setfield), [rmfield](./rmfield)
187
188## Source & Feedback
189- Implementation: `crates/runmat-runtime/src/builtins/structs/core/orderfields.rs`
190- Found a behavioural mismatch? Please open an issue at `https://github.com/runmat-org/runmat/issues/new/choose`.
191"#;
192
193pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
194    name: "orderfields",
195    op_kind: GpuOpKind::Custom("orderfields"),
196    supported_precisions: &[],
197    broadcast: BroadcastSemantics::None,
198    provider_hooks: &[],
199    constant_strategy: ConstantStrategy::InlineLiteral,
200    residency: ResidencyPolicy::InheritInputs,
201    nan_mode: ReductionNaN::Include,
202    two_pass_threshold: None,
203    workgroup_size: None,
204    accepts_nan_mode: false,
205    notes: "Host-only metadata manipulation; struct values that live on the GPU remain resident.",
206};
207
208register_builtin_gpu_spec!(GPU_SPEC);
209
210pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
211    name: "orderfields",
212    shape: ShapeRequirements::Any,
213    constant_strategy: ConstantStrategy::InlineLiteral,
214    elementwise: None,
215    reduction: None,
216    emits_nan: false,
217    notes: "Reordering fields is a metadata operation and does not participate in fusion planning.",
218};
219
220register_builtin_fusion_spec!(FUSION_SPEC);
221
222#[cfg(feature = "doc_export")]
223register_builtin_doc_text!("orderfields", DOC_MD);
224
225#[runtime_builtin(
226    name = "orderfields",
227    category = "structs/core",
228    summary = "Reorder structure field definitions alphabetically or using a supplied order.",
229    keywords = "orderfields,struct,reorder fields,alphabetical,struct array"
230)]
231fn orderfields_builtin(value: Value, rest: Vec<Value>) -> Result<Value, String> {
232    evaluate(value, &rest).map(|eval| eval.into_ordered_value())
233}
234
235/// Evaluate the `orderfields` builtin once and expose both outputs.
236pub fn evaluate(value: Value, rest: &[Value]) -> Result<OrderFieldsEvaluation, String> {
237    if rest.len() > 1 {
238        return Err("orderfields: expected at most two input arguments".to_string());
239    }
240    let order_arg = rest.first();
241
242    match value {
243        Value::Struct(struct_value) => {
244            let original: Vec<String> = struct_value.field_names().cloned().collect();
245            let order = resolve_order(&struct_value, order_arg)?;
246            let permutation = permutation_from(&original, &order)?;
247            let permutation = permutation_tensor(permutation)?;
248            let reordered = reorder_struct(&struct_value, &order)?;
249            Ok(OrderFieldsEvaluation::new(
250                Value::Struct(reordered),
251                permutation,
252            ))
253        }
254        Value::Cell(cell) => {
255            if cell.data.is_empty() {
256                let permutation = permutation_tensor(Vec::new())?;
257                if let Some(arg) = order_arg {
258                    if let Some(reference) = extract_reference_struct(arg)? {
259                        if reference.fields.is_empty() {
260                            return Ok(OrderFieldsEvaluation::new(Value::Cell(cell), permutation));
261                        } else {
262                            return Err("orderfields: empty struct arrays cannot adopt a non-empty reference order".to_string());
263                        }
264                    }
265                    if let Some(names) = extract_name_list(arg)? {
266                        if names.is_empty() {
267                            return Ok(OrderFieldsEvaluation::new(Value::Cell(cell), permutation));
268                        }
269                        return Err(
270                            "orderfields: struct array has no fields to reorder".to_string()
271                        );
272                    }
273                    if let Value::Tensor(tensor) = arg {
274                        if tensor.data.is_empty() {
275                            return Ok(OrderFieldsEvaluation::new(Value::Cell(cell), permutation));
276                        }
277                        return Err(
278                            "orderfields: struct array has no fields to reorder".to_string()
279                        );
280                    }
281                    return Err("orderfields: struct array has no fields to reorder".to_string());
282                }
283                return Ok(OrderFieldsEvaluation::new(Value::Cell(cell), permutation));
284            }
285            let first = extract_struct_from_cell(&cell, 0)?;
286            let original: Vec<String> = first.field_names().cloned().collect();
287            let order = resolve_order(&first, order_arg)?;
288            let permutation = permutation_from(&original, &order)?;
289            let permutation = permutation_tensor(permutation)?;
290            let reordered = reorder_struct_array(&cell, &order)?;
291            Ok(OrderFieldsEvaluation::new(
292                Value::Cell(reordered),
293                permutation,
294            ))
295        }
296        other => Err(format!(
297            "orderfields: first argument must be a struct or struct array (got {other:?})"
298        )),
299    }
300}
301
302pub struct OrderFieldsEvaluation {
303    ordered: Value,
304    permutation: Tensor,
305}
306
307impl OrderFieldsEvaluation {
308    fn new(ordered: Value, permutation: Tensor) -> Self {
309        Self {
310            ordered,
311            permutation,
312        }
313    }
314
315    pub fn into_ordered_value(self) -> Value {
316        self.ordered
317    }
318
319    pub fn permutation_value(&self) -> Value {
320        tensor::tensor_into_value(self.permutation.clone())
321    }
322
323    pub fn into_values(self) -> (Value, Value) {
324        let perm = tensor::tensor_into_value(self.permutation);
325        (self.ordered, perm)
326    }
327}
328
329fn reorder_struct_array(array: &CellArray, order: &[String]) -> Result<CellArray, String> {
330    let mut reordered_elems = Vec::with_capacity(array.data.len());
331    for (index, handle) in array.data.iter().enumerate() {
332        let value = unsafe { &*handle.as_raw() };
333        let Value::Struct(st) = value else {
334            return Err(format!(
335                "orderfields: struct array element {} is not a struct",
336                index + 1
337            ));
338        };
339        ensure_same_field_set(order, st)?;
340        let reordered = reorder_struct(st, order)?;
341        reordered_elems.push(Value::Struct(reordered));
342    }
343    CellArray::new_with_shape(reordered_elems, array.shape.clone())
344        .map_err(|e| format!("orderfields: failed to rebuild struct array: {e}"))
345}
346
347fn reorder_struct(struct_value: &StructValue, order: &[String]) -> Result<StructValue, String> {
348    let mut reordered = StructValue::new();
349    for name in order {
350        let value = struct_value
351            .fields
352            .get(name)
353            .ok_or_else(|| missing_field(name))?
354            .clone();
355        reordered.fields.insert(name.clone(), value);
356    }
357    Ok(reordered)
358}
359
360fn resolve_order(
361    struct_value: &StructValue,
362    order_arg: Option<&Value>,
363) -> Result<Vec<String>, String> {
364    let mut current: Vec<String> = struct_value.field_names().cloned().collect();
365    if let Some(arg) = order_arg {
366        if let Some(reference) = extract_reference_struct(arg)? {
367            let reference_names: Vec<String> = reference.field_names().cloned().collect();
368            ensure_same_field_set(&reference_names, struct_value)?;
369            return Ok(reference_names);
370        }
371
372        if let Some(names) = extract_name_list(arg)? {
373            ensure_same_field_set(&names, struct_value)?;
374            return Ok(names);
375        }
376
377        if let Some(permutation) = extract_indices(&current, arg)? {
378            return Ok(permutation);
379        }
380
381        return Err("orderfields: unrecognised ordering argument".to_string());
382    }
383
384    sort_field_names(&mut current);
385    Ok(current)
386}
387
388fn permutation_from(original: &[String], order: &[String]) -> Result<Vec<f64>, String> {
389    let mut index_map = HashMap::with_capacity(original.len());
390    for (idx, name) in original.iter().enumerate() {
391        index_map.insert(name.as_str(), idx);
392    }
393    let mut indices = Vec::with_capacity(order.len());
394    for name in order {
395        let Some(position) = index_map.get(name.as_str()) else {
396            return Err(missing_field(name));
397        };
398        indices.push((*position as f64) + 1.0);
399    }
400    Ok(indices)
401}
402
403fn permutation_tensor(indices: Vec<f64>) -> Result<Tensor, String> {
404    let rows = indices.len();
405    let shape = vec![rows, 1];
406    Tensor::new(indices, shape).map_err(|e| format!("orderfields: {e}"))
407}
408
409fn sort_field_names(names: &mut [String]) {
410    names.sort_by(|a, b| {
411        let lower_a = a.to_ascii_lowercase();
412        let lower_b = b.to_ascii_lowercase();
413        match lower_a.cmp(&lower_b) {
414            Ordering::Equal => a.cmp(b),
415            other => other,
416        }
417    });
418}
419
420fn extract_reference_struct(value: &Value) -> Result<Option<StructValue>, String> {
421    match value {
422        Value::Struct(st) => Ok(Some(st.clone())),
423        Value::Cell(cell) => {
424            let mut first: Option<StructValue> = None;
425            for (index, handle) in cell.data.iter().enumerate() {
426                let value = unsafe { &*handle.as_raw() };
427                if let Value::Struct(st) = value {
428                    if first.is_none() {
429                        first = Some(st.clone());
430                    }
431                } else if first.is_some() {
432                    return Err(format!(
433                        "orderfields: reference struct array element {} is not a struct",
434                        index + 1
435                    ));
436                } else {
437                    return Ok(None);
438                }
439            }
440            Ok(first)
441        }
442        _ => Ok(None),
443    }
444}
445
446fn extract_name_list(arg: &Value) -> Result<Option<Vec<String>>, String> {
447    match arg {
448        Value::Cell(cell) => {
449            let mut names = Vec::with_capacity(cell.data.len());
450            for (index, handle) in cell.data.iter().enumerate() {
451                let value = unsafe { &*handle.as_raw() };
452                let text = scalar_string(value).ok_or_else(|| {
453                    format!(
454                        "orderfields: cell array element {} must be a string or character vector",
455                        index + 1
456                    )
457                })?;
458                if text.is_empty() {
459                    return Err("orderfields: field names must be nonempty".to_string());
460                }
461                names.push(text);
462            }
463            Ok(Some(names))
464        }
465        Value::StringArray(sa) => Ok(Some(sa.data.clone())),
466        Value::CharArray(ca) => {
467            if ca.rows == 0 {
468                return Ok(Some(Vec::new()));
469            }
470            let mut names = Vec::with_capacity(ca.rows);
471            for row in 0..ca.rows {
472                let start = row * ca.cols;
473                let end = start + ca.cols;
474                let mut text: String = ca.data[start..end].iter().collect();
475                while text.ends_with(' ') {
476                    text.pop();
477                }
478                if text.is_empty() {
479                    return Err("orderfields: field names must be nonempty".to_string());
480                }
481                names.push(text);
482            }
483            Ok(Some(names))
484        }
485        _ => Ok(None),
486    }
487}
488
489fn extract_indices(current: &[String], arg: &Value) -> Result<Option<Vec<String>>, String> {
490    let Value::Tensor(tensor) = arg else {
491        return Ok(None);
492    };
493    if tensor.data.is_empty() && current.is_empty() {
494        return Ok(Some(Vec::new()));
495    }
496    if tensor.data.len() != current.len() {
497        return Err("orderfields: index vector must permute every field exactly once".to_string());
498    }
499    let mut seen = HashSet::with_capacity(current.len());
500    let mut order = Vec::with_capacity(current.len());
501    for value in &tensor.data {
502        if !value.is_finite() || value.fract() != 0.0 {
503            return Err("orderfields: index vector must contain integers".to_string());
504        }
505        let idx = *value as isize;
506        if idx < 1 || idx as usize > current.len() {
507            return Err("orderfields: index vector element out of range".to_string());
508        }
509        let zero_based = (idx as usize) - 1;
510        if !seen.insert(zero_based) {
511            return Err("orderfields: index vector contains duplicate positions".to_string());
512        }
513        order.push(current[zero_based].clone());
514    }
515    Ok(Some(order))
516}
517
518fn ensure_same_field_set(order: &[String], original: &StructValue) -> Result<(), String> {
519    if order.len() != original.fields.len() {
520        return Err("orderfields: field names must match the struct exactly".to_string());
521    }
522    let mut seen = HashSet::with_capacity(order.len());
523    let original_set: HashSet<&str> = original.field_names().map(|s| s.as_str()).collect();
524    for name in order {
525        if !original_set.contains(name.as_str()) {
526            return Err(format!(
527                "orderfields: unknown field '{name}' in requested order"
528            ));
529        }
530        if !seen.insert(name.as_str()) {
531            return Err(format!(
532                "orderfields: duplicate field '{name}' in requested order"
533            ));
534        }
535    }
536    Ok(())
537}
538
539fn extract_struct_from_cell(cell: &CellArray, index: usize) -> Result<StructValue, String> {
540    let value = unsafe { &*cell.data[index].as_raw() };
541    match value {
542        Value::Struct(st) => Ok(st.clone()),
543        other => Err(format!(
544            "orderfields: expected struct array contents to be structs (found {other:?})"
545        )),
546    }
547}
548
549fn scalar_string(value: &Value) -> Option<String> {
550    match value {
551        Value::String(s) => Some(s.clone()),
552        Value::StringArray(sa) if sa.data.len() == 1 => Some(sa.data[0].clone()),
553        Value::CharArray(ca) if ca.rows == 1 => {
554            let mut text: String = ca.data.iter().collect();
555            while text.ends_with(' ') {
556                text.pop();
557            }
558            Some(text)
559        }
560        _ => None,
561    }
562}
563
564fn missing_field(name: &str) -> String {
565    format!("orderfields: field '{name}' does not exist on the struct")
566}
567
568#[cfg(test)]
569mod tests {
570    use super::*;
571    use runmat_builtins::{CellArray, CharArray, StringArray, Tensor};
572
573    #[cfg(feature = "doc_export")]
574    use crate::builtins::common::test_support;
575
576    fn field_order(struct_value: &StructValue) -> Vec<String> {
577        struct_value.field_names().cloned().collect()
578    }
579
580    #[test]
581    fn default_sorts_alphabetically() {
582        let mut st = StructValue::new();
583        st.fields.insert("beta".to_string(), Value::Num(2.0));
584        st.fields.insert("alpha".to_string(), Value::Num(1.0));
585        st.fields.insert("gamma".to_string(), Value::Num(3.0));
586
587        let result = orderfields_builtin(Value::Struct(st), Vec::new()).expect("orderfields");
588        let Value::Struct(sorted) = result else {
589            panic!("expected struct result");
590        };
591        assert_eq!(
592            field_order(&sorted),
593            vec!["alpha".to_string(), "beta".to_string(), "gamma".to_string()]
594        );
595    }
596
597    #[test]
598    fn reorder_with_cell_name_list() {
599        let mut st = StructValue::new();
600        st.fields.insert("a".to_string(), Value::Num(1.0));
601        st.fields.insert("b".to_string(), Value::Num(2.0));
602        st.fields.insert("c".to_string(), Value::Num(3.0));
603        let names = CellArray::new(
604            vec![Value::from("c"), Value::from("a"), Value::from("b")],
605            1,
606            3,
607        )
608        .expect("cell");
609
610        let reordered =
611            orderfields_builtin(Value::Struct(st), vec![Value::Cell(names)]).expect("orderfields");
612        let Value::Struct(result) = reordered else {
613            panic!("expected struct result");
614        };
615        assert_eq!(
616            field_order(&result),
617            vec!["c".to_string(), "a".to_string(), "b".to_string()]
618        );
619    }
620
621    #[test]
622    fn reorder_with_string_array_names() {
623        let mut st = StructValue::new();
624        st.fields.insert("alpha".to_string(), Value::Num(1.0));
625        st.fields.insert("beta".to_string(), Value::Num(2.0));
626        st.fields.insert("gamma".to_string(), Value::Num(3.0));
627
628        let strings = StringArray::new(
629            vec!["gamma".into(), "alpha".into(), "beta".into()],
630            vec![1, 3],
631        )
632        .expect("string array");
633
634        let result = orderfields_builtin(Value::Struct(st), vec![Value::StringArray(strings)])
635            .expect("orderfields");
636        let Value::Struct(sorted) = result else {
637            panic!("expected struct result");
638        };
639        assert_eq!(
640            field_order(&sorted),
641            vec!["gamma".to_string(), "alpha".to_string(), "beta".to_string()]
642        );
643    }
644
645    #[test]
646    fn reorder_with_char_array_names() {
647        let mut st = StructValue::new();
648        st.fields.insert("cat".to_string(), Value::Num(1.0));
649        st.fields.insert("ant".to_string(), Value::Num(2.0));
650        st.fields.insert("bat".to_string(), Value::Num(3.0));
651
652        let data = vec!['b', 'a', 't', 'c', 'a', 't', 'a', 'n', 't'];
653        let char_array = CharArray::new(data, 3, 3).expect("char array");
654
655        let result = orderfields_builtin(Value::Struct(st), vec![Value::CharArray(char_array)])
656            .expect("order");
657        let Value::Struct(sorted) = result else {
658            panic!("expected struct result");
659        };
660        assert_eq!(
661            field_order(&sorted),
662            vec!["bat".to_string(), "cat".to_string(), "ant".to_string()]
663        );
664    }
665
666    #[test]
667    fn reorder_with_reference_struct() {
668        let mut source = StructValue::new();
669        source.fields.insert("y".to_string(), Value::Num(2.0));
670        source.fields.insert("x".to_string(), Value::Num(1.0));
671
672        let mut reference = StructValue::new();
673        reference.fields.insert("x".to_string(), Value::Num(0.0));
674        reference.fields.insert("y".to_string(), Value::Num(0.0));
675
676        let result = orderfields_builtin(
677            Value::Struct(source),
678            vec![Value::Struct(reference.clone())],
679        )
680        .expect("orderfields");
681        let Value::Struct(reordered) = result else {
682            panic!("expected struct result");
683        };
684        assert_eq!(
685            field_order(&reordered),
686            vec!["x".to_string(), "y".to_string()]
687        );
688    }
689
690    #[test]
691    fn reorder_with_index_vector() {
692        let mut st = StructValue::new();
693        st.fields.insert("first".to_string(), Value::Num(1.0));
694        st.fields.insert("second".to_string(), Value::Num(2.0));
695        st.fields.insert("third".to_string(), Value::Num(3.0));
696
697        let permutation = Tensor::new(vec![3.0, 1.0, 2.0], vec![1, 3]).expect("tensor permutation");
698        let result = orderfields_builtin(Value::Struct(st), vec![Value::Tensor(permutation)])
699            .expect("orderfields");
700        let Value::Struct(reordered) = result else {
701            panic!("expected struct result");
702        };
703        assert_eq!(
704            field_order(&reordered),
705            vec![
706                "third".to_string(),
707                "first".to_string(),
708                "second".to_string()
709            ]
710        );
711    }
712
713    #[test]
714    fn index_vector_must_be_integers() {
715        let mut st = StructValue::new();
716        st.fields.insert("one".to_string(), Value::Num(1.0));
717        st.fields.insert("two".to_string(), Value::Num(2.0));
718
719        let permutation = Tensor::new(vec![1.0, 1.5], vec![1, 2]).expect("tensor");
720        let err =
721            orderfields_builtin(Value::Struct(st), vec![Value::Tensor(permutation)]).unwrap_err();
722        assert!(
723            err.contains("index vector must contain integers"),
724            "unexpected error: {err}"
725        );
726    }
727
728    #[test]
729    fn permutation_vector_matches_original_positions() {
730        let mut st = StructValue::new();
731        st.fields.insert("beta".to_string(), Value::Num(2.0));
732        st.fields.insert("alpha".to_string(), Value::Num(1.0));
733        st.fields.insert("gamma".to_string(), Value::Num(3.0));
734
735        let eval = evaluate(Value::Struct(st), &[]).expect("evaluate");
736        let perm = eval.permutation_value();
737        match perm {
738            Value::Tensor(t) => assert_eq!(t.data, vec![2.0, 1.0, 3.0]),
739            other => panic!("expected tensor permutation, got {other:?}"),
740        }
741        let Value::Struct(ordered) = eval.into_ordered_value() else {
742            panic!("expected struct result");
743        };
744        assert_eq!(
745            field_order(&ordered),
746            vec!["alpha".to_string(), "beta".to_string(), "gamma".to_string()]
747        );
748    }
749
750    #[test]
751    fn reorder_struct_array() {
752        let mut first = StructValue::new();
753        first.fields.insert("b".to_string(), Value::Num(1.0));
754        first.fields.insert("a".to_string(), Value::Num(2.0));
755        let mut second = StructValue::new();
756        second.fields.insert("b".to_string(), Value::Num(3.0));
757        second.fields.insert("a".to_string(), Value::Num(4.0));
758        let array = CellArray::new_with_shape(
759            vec![Value::Struct(first), Value::Struct(second)],
760            vec![1, 2],
761        )
762        .expect("struct array");
763        let names =
764            CellArray::new(vec![Value::from("a"), Value::from("b")], 1, 2).expect("cell names");
765
766        let result =
767            orderfields_builtin(Value::Cell(array), vec![Value::Cell(names)]).expect("orderfields");
768        let Value::Cell(reordered) = result else {
769            panic!("expected cell array");
770        };
771        for handle in &reordered.data {
772            let Value::Struct(st) = (unsafe { &*handle.as_raw() }) else {
773                panic!("expected struct element");
774            };
775            assert_eq!(field_order(st), vec!["a".to_string(), "b".to_string()]);
776        }
777    }
778
779    #[test]
780    fn struct_array_permutation_reuses_order() {
781        let mut first = StructValue::new();
782        first.fields.insert("z".to_string(), Value::Num(1.0));
783        first.fields.insert("x".to_string(), Value::Num(2.0));
784        first.fields.insert("y".to_string(), Value::Num(3.0));
785
786        let mut second = StructValue::new();
787        second.fields.insert("z".to_string(), Value::Num(4.0));
788        second.fields.insert("x".to_string(), Value::Num(5.0));
789        second.fields.insert("y".to_string(), Value::Num(6.0));
790
791        let array = CellArray::new_with_shape(
792            vec![Value::Struct(first), Value::Struct(second)],
793            vec![1, 2],
794        )
795        .expect("struct array");
796
797        let eval = evaluate(Value::Cell(array), &[]).expect("evaluate");
798        let perm = eval.permutation_value();
799        match perm {
800            Value::Tensor(t) => assert_eq!(t.data, vec![2.0, 3.0, 1.0]),
801            other => panic!("expected tensor permutation, got {other:?}"),
802        }
803    }
804
805    #[test]
806    fn rejects_unknown_field() {
807        let mut st = StructValue::new();
808        st.fields.insert("alpha".to_string(), Value::Num(1.0));
809        st.fields.insert("beta".to_string(), Value::Num(2.0));
810        let err = orderfields_builtin(
811            Value::Struct(st),
812            vec![Value::Cell(
813                CellArray::new(vec![Value::from("beta"), Value::from("gamma")], 1, 2)
814                    .expect("cell"),
815            )],
816        )
817        .unwrap_err();
818        assert!(
819            err.contains("unknown field 'gamma'"),
820            "unexpected error: {err}"
821        );
822    }
823
824    #[test]
825    fn duplicate_field_names_rejected() {
826        let mut st = StructValue::new();
827        st.fields.insert("alpha".to_string(), Value::Num(1.0));
828        st.fields.insert("beta".to_string(), Value::Num(2.0));
829
830        let names =
831            CellArray::new(vec![Value::from("alpha"), Value::from("alpha")], 1, 2).expect("cell");
832        let err = orderfields_builtin(Value::Struct(st), vec![Value::Cell(names)]).unwrap_err();
833        assert!(
834            err.contains("duplicate field 'alpha'"),
835            "unexpected error: {err}"
836        );
837    }
838
839    #[test]
840    fn reference_struct_mismatch_errors() {
841        let mut source = StructValue::new();
842        source.fields.insert("x".to_string(), Value::Num(1.0));
843        source.fields.insert("y".to_string(), Value::Num(2.0));
844
845        let mut reference = StructValue::new();
846        reference.fields.insert("x".to_string(), Value::Num(0.0));
847
848        let err =
849            orderfields_builtin(Value::Struct(source), vec![Value::Struct(reference)]).unwrap_err();
850        assert!(
851            err.contains("field names must match the struct exactly"),
852            "unexpected error: {err}"
853        );
854    }
855
856    #[test]
857    fn invalid_order_argument_type_errors() {
858        let mut st = StructValue::new();
859        st.fields.insert("x".to_string(), Value::Num(1.0));
860
861        let err = orderfields_builtin(Value::Struct(st), vec![Value::Num(1.0)]).unwrap_err();
862        assert!(
863            err.contains("unrecognised ordering argument"),
864            "unexpected error: {err}"
865        );
866    }
867
868    #[test]
869    fn empty_struct_array_nonempty_reference_errors() {
870        let empty = CellArray::new(Vec::new(), 0, 0).expect("empty struct array");
871        let mut reference = StructValue::new();
872        reference
873            .fields
874            .insert("field".to_string(), Value::Num(1.0));
875
876        let err =
877            orderfields_builtin(Value::Cell(empty), vec![Value::Struct(reference)]).unwrap_err();
878        assert!(
879            err.contains("empty struct arrays cannot adopt a non-empty reference order"),
880            "unexpected error: {err}"
881        );
882    }
883
884    #[test]
885    #[cfg(feature = "doc_export")]
886    fn doc_examples_compile() {
887        let blocks = test_support::doc_examples(DOC_MD);
888        assert!(!blocks.is_empty());
889    }
890}