1use 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
235pub 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(¤t, 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}