1use std::cmp::Ordering;
4
5use runmat_builtins::{CharArray, ComplexTensor, StringArray, Tensor, Value};
6use runmat_macros::runtime_builtin;
7
8use crate::builtins::common::gpu_helpers;
9use crate::builtins::common::spec::{
10 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
11 ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
12};
13use crate::builtins::common::tensor;
14#[cfg(feature = "doc_export")]
15use crate::register_builtin_doc_text;
16use crate::{register_builtin_fusion_spec, register_builtin_gpu_spec};
17
18#[cfg(feature = "doc_export")]
19pub const DOC_MD: &str = r#"---
20title: "issorted"
21category: "array/sorting_sets"
22keywords: ["issorted", "sorted", "monotonic", "strictascend", "rows", "comparisonmethod", "missingplacement"]
23summary: "Determine whether an array is already sorted along a dimension or across rows."
24references:
25 - https://www.mathworks.com/help/matlab/ref/double.issorted.html
26gpu_support:
27 elementwise: false
28 reduction: false
29 precisions: ["f32", "f64"]
30 broadcasting: "none"
31 notes: "GPU inputs are gathered to the host while providers gain native predicate support."
32fusion:
33 elementwise: false
34 reduction: false
35 max_inputs: 1
36 constants: "inline"
37requires_feature: null
38tested:
39 unit: "builtins::array::sorting_sets::issorted::tests"
40 integration: "builtins::array::sorting_sets::issorted::tests::issorted_gpu_roundtrip"
41 doc: "builtins::array::sorting_sets::issorted::tests::issorted_doc_examples"
42---
43
44# What does `issorted` do in MATLAB / RunMat?
45`issorted(A)` checks whether the elements of `A` already appear in sorted order.
46You can examine a specific dimension, enforce strict monotonicity, require descending order, and
47control how missing values are positioned. The function also supports row-wise checks that match
48`issorted(A,'rows')` from MATLAB.
49
50## How does `issorted` behave in MATLAB / RunMat?
51- `issorted(A)` examines the first non-singleton dimension and returns a logical scalar.
52- `issorted(A, dim)` selects the dimension explicitly (1-based).
53- Direction flags include `'ascend'`, `'descend'`, `'monotonic'`, `'strictascend'`,
54 `'strictdescend'`, and `'strictmonotonic'`.
55- Name-value options mirror MATLAB:
56 - `'MissingPlacement'` accepts `'auto'`, `'first'`, or `'last'`.
57 - `'ComparisonMethod'` accepts `'auto'`, `'real'`, or `'abs'` for numeric and complex data.
58- `issorted(A,'rows', ...)` verifies lexicographic ordering of rows.
59- Logical and character arrays are promoted to double precision for comparison; string arrays are
60 compared lexicographically, recognising the literal `<missing>` token as a missing element.
61- Empty arrays, scalars, and singleton dimensions are always reported as sorted.
62
63## GPU execution in RunMat
64- `issorted` is registered as a sink builtin. When the input tensor lives on the GPU the runtime
65 gathers it to host memory and performs the check there, guaranteeing MATLAB-compatible semantics.
66- Future providers may implement a predicate hook so that simple monotonic checks can execute
67 entirely on device. Until then the behaviour is identical for CPU and GPU arrays.
68- The result is a logical scalar (`true` or `false`) regardless of input residency.
69
70## Examples of using `issorted` in MATLAB / RunMat
71
72### Checking an ascending vector
73```matlab
74A = [5 12 33 39 78 90 95 107];
75tf = issorted(A);
76```
77Expected output:
78```matlab
79tf =
80 1
81```
82
83### Verifying strict increase
84```matlab
85B = [1 1 2 3];
86tf = issorted(B, 'strictascend');
87```
88Expected output:
89```matlab
90tf =
91 0
92```
93
94### Using a specific dimension
95```matlab
96C = [1 4 2; 3 6 5];
97tf = issorted(C, 2, 'descend');
98```
99Expected output:
100```matlab
101tf =
102 0
103```
104
105### Allowing either ascending or descending order
106```matlab
107D = [10 8 8 5 1];
108tf = issorted(D, 'monotonic');
109```
110Expected output:
111```matlab
112tf =
113 1
114```
115
116### Controlling missing placements
117```matlab
118E = [NaN NaN 4 7];
119tf = issorted(E, 'MissingPlacement', 'first');
120```
121Expected output:
122```matlab
123tf =
124 0
125```
126
127### Comparing complex data by magnitude
128```matlab
129Z = [1+1i, 2+3i, 4+0i];
130tf = issorted(Z, 'ComparisonMethod', 'abs');
131```
132Expected output:
133```matlab
134tf =
135 1
136```
137
138### Checking row order
139```matlab
140R = [1 2 3; 1 2 4; 2 0 1];
141tf = issorted(R, 'rows');
142```
143Expected output:
144```matlab
145tf =
146 1
147```
148
149### Working with string arrays
150```matlab
151str = [ "apple" "banana"; "apple" "carrot" ];
152tf = issorted(str, 2);
153```
154Expected output:
155```matlab
156tf =
157 0
158```
159
160## FAQ
161
162### Does `issorted` modify its input?
163No. The function inspects the data in-place and returns a logical scalar.
164
165### What is the difference between `'ascend'` and `'strictascend'`?
166`'ascend'` allows consecutive equal values, while `'strictascend'` requires every element to be
167strictly greater than its predecessor and rejects missing values.
168
169### How does `'monotonic'` work?
170`'monotonic'` succeeds when the data is entirely non-decreasing *or* non-increasing. Use
171`'strictmonotonic'` to forbid repeated or missing values.
172
173### Can I control where NaN values appear?
174Yes. `'MissingPlacement','first'` requires missing values to precede finite ones, `'last'` requires
175them to trail, and `'auto'` follows MATLAB’s default (end for ascending checks, start for descending).
176
177### Is `'ComparisonMethod'` relevant for real data?
178For real data `'auto'` and `'real'` are identical. `'abs'` compares magnitudes first and breaks ties
179using the signed value, matching MATLAB.
180
181### Does the function support GPU arrays?
182Yes. GPU inputs are gathered automatically and the result is computed on the host to guarantee
183correctness until dedicated provider hooks are available.
184
185### How are string arrays treated?
186Strings are compared lexicographically using Unicode code-point order. The literal `<missing>` is
187treated as a missing value so `'MissingPlacement'` rules apply.
188
189### What about empty arrays or dimensions of length 0?
190Empty slices are considered sorted. Passing a dimension larger than `ndims(A)` also returns `true`.
191
192## See Also
193- [sort](./sort)
194- [sortrows](./sortrows)
195- [unique](./unique)
196- [issortedrows](https://www.mathworks.com/help/matlab/ref/issortedrows.html) (MATLAB reference)
197
198## Source & Feedback
199- Source code: [`crates/runmat-runtime/src/builtins/array/sorting_sets/issorted.rs`](https://github.com/runmat-org/runmat/blob/main/crates/runmat-runtime/src/builtins/array/sorting_sets/issorted.rs)
200- Found a bug? [Open an issue](https://github.com/runmat-org/runmat/issues/new/choose) with a minimal reproduction.
201"#;
202
203pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
204 name: "issorted",
205 op_kind: GpuOpKind::Custom("predicate"),
206 supported_precisions: &[ScalarType::F32, ScalarType::F64],
207 broadcast: BroadcastSemantics::None,
208 provider_hooks: &[],
209 constant_strategy: ConstantStrategy::InlineLiteral,
210 residency: ResidencyPolicy::GatherImmediately,
211 nan_mode: ReductionNaN::Include,
212 two_pass_threshold: None,
213 workgroup_size: None,
214 accepts_nan_mode: true,
215 notes: "GPU inputs gather to the host until providers implement dedicated predicate kernels.",
216};
217
218register_builtin_gpu_spec!(GPU_SPEC);
219
220pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
221 name: "issorted",
222 shape: ShapeRequirements::Any,
223 constant_strategy: ConstantStrategy::InlineLiteral,
224 elementwise: None,
225 reduction: None,
226 emits_nan: false,
227 notes: "Predicate builtin evaluated outside fusion; planner prevents kernel generation.",
228};
229
230register_builtin_fusion_spec!(FUSION_SPEC);
231
232#[cfg(feature = "doc_export")]
233register_builtin_doc_text!("issorted", DOC_MD);
234
235#[runtime_builtin(
236 name = "issorted",
237 category = "array/sorting_sets",
238 summary = "Determine whether an array is already sorted.",
239 keywords = "issorted,sorted,monotonic,rows",
240 accel = "sink",
241 sink = true
242)]
243fn issorted_builtin(value: Value, rest: Vec<Value>) -> Result<Value, String> {
244 let input = normalize_input(value)?;
245 let shape = input.shape();
246 let args = IssortedArgs::parse(&rest, &shape)?;
247
248 let result = match input {
249 InputArray::Real(tensor) => issorted_real(&tensor, &args)?,
250 InputArray::Complex(tensor) => issorted_complex(&tensor, &args)?,
251 InputArray::String(array) => issorted_string(&array, &args)?,
252 };
253
254 Ok(Value::Bool(result))
255}
256
257struct IssortedArgs {
258 mode: CheckMode,
259 direction: Direction,
260 comparison: ComparisonMethod,
261 missing: MissingPlacement,
262}
263
264#[derive(Clone, Copy, Debug, PartialEq, Eq)]
265enum CheckMode {
266 Dimension(usize),
267 Rows,
268}
269
270#[derive(Clone, Copy, Debug, PartialEq, Eq)]
271enum Direction {
272 Ascend,
273 Descend,
274 Monotonic,
275 StrictAscend,
276 StrictDescend,
277 StrictMonotonic,
278}
279
280#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
281enum ComparisonMethod {
282 #[default]
283 Auto,
284 Real,
285 Abs,
286}
287
288#[derive(Clone, Copy, Debug, PartialEq, Eq)]
289enum MissingPlacement {
290 Auto,
291 First,
292 Last,
293}
294
295#[derive(Clone, Copy, Debug, PartialEq, Eq)]
296enum MissingPlacementResolved {
297 First,
298 Last,
299}
300
301impl MissingPlacement {
302 fn resolve(self, direction: SortDirection) -> MissingPlacementResolved {
303 match self {
304 MissingPlacement::First => MissingPlacementResolved::First,
305 MissingPlacement::Last => MissingPlacementResolved::Last,
306 MissingPlacement::Auto => match direction {
307 SortDirection::Ascend => MissingPlacementResolved::Last,
308 SortDirection::Descend => MissingPlacementResolved::First,
309 },
310 }
311 }
312}
313
314#[derive(Clone, Copy, Debug, PartialEq, Eq)]
315enum SortDirection {
316 Ascend,
317 Descend,
318}
319
320#[derive(Clone, Copy)]
321struct OrderSpec {
322 direction: SortDirection,
323 strict: bool,
324}
325
326enum InputArray {
327 Real(Tensor),
328 Complex(ComplexTensor),
329 String(StringArray),
330}
331
332impl InputArray {
333 fn shape(&self) -> Vec<usize> {
334 match self {
335 InputArray::Real(t) => t.shape.clone(),
336 InputArray::Complex(t) => t.shape.clone(),
337 InputArray::String(sa) => sa.shape.clone(),
338 }
339 }
340}
341
342impl IssortedArgs {
343 fn parse(args: &[Value], shape: &[usize]) -> Result<Self, String> {
344 let mut dim_arg: Option<usize> = None;
345 let mut direction: Option<Direction> = None;
346 let mut comparison: ComparisonMethod = ComparisonMethod::Auto;
347 let mut missing: MissingPlacement = MissingPlacement::Auto;
348 let mut mode = CheckMode::Dimension(default_dimension(shape));
349 let mut saw_rows = false;
350
351 let mut idx = 0;
352 while idx < args.len() {
353 let arg = &args[idx];
354 if let Some(token) = value_to_string_lower(arg) {
355 match token.as_str() {
356 "rows" => {
357 if saw_rows {
358 return Err("issorted: 'rows' specified more than once".to_string());
359 }
360 if dim_arg.is_some() {
361 return Err(
362 "issorted: cannot combine 'rows' with a dimension argument"
363 .to_string(),
364 );
365 }
366 saw_rows = true;
367 mode = CheckMode::Rows;
368 idx += 1;
369 continue;
370 }
371 "ascend" => {
372 ensure_unique_direction(&direction)?;
373 direction = Some(Direction::Ascend);
374 idx += 1;
375 continue;
376 }
377 "descend" => {
378 ensure_unique_direction(&direction)?;
379 direction = Some(Direction::Descend);
380 idx += 1;
381 continue;
382 }
383 "monotonic" => {
384 ensure_unique_direction(&direction)?;
385 direction = Some(Direction::Monotonic);
386 idx += 1;
387 continue;
388 }
389 "strictascend" => {
390 ensure_unique_direction(&direction)?;
391 direction = Some(Direction::StrictAscend);
392 idx += 1;
393 continue;
394 }
395 "strictdescend" => {
396 ensure_unique_direction(&direction)?;
397 direction = Some(Direction::StrictDescend);
398 idx += 1;
399 continue;
400 }
401 "strictmonotonic" => {
402 ensure_unique_direction(&direction)?;
403 direction = Some(Direction::StrictMonotonic);
404 idx += 1;
405 continue;
406 }
407 "comparisonmethod" => {
408 idx += 1;
409 if idx >= args.len() {
410 return Err(
411 "issorted: expected a value for 'ComparisonMethod'".to_string()
412 );
413 }
414 let value = value_to_string_lower(&args[idx]).ok_or_else(|| {
415 "issorted: 'ComparisonMethod' expects a string value".to_string()
416 })?;
417 comparison = match value.as_str() {
418 "auto" => ComparisonMethod::Auto,
419 "real" => ComparisonMethod::Real,
420 "abs" | "magnitude" => ComparisonMethod::Abs,
421 other => {
422 return Err(format!(
423 "issorted: unsupported ComparisonMethod '{other}'"
424 ));
425 }
426 };
427 idx += 1;
428 continue;
429 }
430 "missingplacement" => {
431 idx += 1;
432 if idx >= args.len() {
433 return Err(
434 "issorted: expected a value for 'MissingPlacement'".to_string()
435 );
436 }
437 let value = value_to_string_lower(&args[idx]).ok_or_else(|| {
438 "issorted: 'MissingPlacement' expects a string value".to_string()
439 })?;
440 missing = match value.as_str() {
441 "auto" => MissingPlacement::Auto,
442 "first" => MissingPlacement::First,
443 "last" => MissingPlacement::Last,
444 other => {
445 return Err(format!(
446 "issorted: unsupported MissingPlacement '{other}'"
447 ));
448 }
449 };
450 idx += 1;
451 continue;
452 }
453 _ => {}
454 }
455 }
456
457 if !saw_rows && dim_arg.is_none() {
458 if let Ok(dim) = tensor::parse_dimension(arg, "issorted") {
459 dim_arg = Some(dim);
460 idx += 1;
461 continue;
462 }
463 }
464
465 return Err(format!("issorted: unrecognised argument {:?}", arg));
466 }
467
468 if let Some(dim) = dim_arg {
469 mode = CheckMode::Dimension(dim);
470 }
471
472 Ok(IssortedArgs {
473 mode,
474 direction: direction.unwrap_or(Direction::Ascend),
475 comparison,
476 missing,
477 })
478 }
479}
480
481fn ensure_unique_direction(direction: &Option<Direction>) -> Result<(), String> {
482 if direction.is_some() {
483 Err("issorted: sorting direction specified more than once".to_string())
484 } else {
485 Ok(())
486 }
487}
488
489fn normalize_input(value: Value) -> Result<InputArray, String> {
490 match value {
491 Value::Tensor(tensor) => Ok(InputArray::Real(tensor)),
492 Value::LogicalArray(logical) => {
493 let tensor = tensor::logical_to_tensor(&logical)?;
494 Ok(InputArray::Real(tensor))
495 }
496 Value::Num(_) | Value::Int(_) | Value::Bool(_) => {
497 let tensor = tensor::value_into_tensor_for("issorted", value)?;
498 Ok(InputArray::Real(tensor))
499 }
500 Value::ComplexTensor(ct) => Ok(InputArray::Complex(ct)),
501 Value::Complex(re, im) => {
502 let tensor = ComplexTensor::new(vec![(re, im)], vec![1, 1])
503 .map_err(|e| format!("issorted: {e}"))?;
504 Ok(InputArray::Complex(tensor))
505 }
506 Value::CharArray(ca) => {
507 let tensor = char_array_to_tensor(&ca)?;
508 Ok(InputArray::Real(tensor))
509 }
510 Value::StringArray(sa) => Ok(InputArray::String(sa)),
511 Value::String(s) => {
512 let array =
513 StringArray::new(vec![s], vec![1, 1]).map_err(|e| format!("issorted: {e}"))?;
514 Ok(InputArray::String(array))
515 }
516 Value::GpuTensor(handle) => {
517 let tensor = gpu_helpers::gather_tensor(&handle)?;
518 Ok(InputArray::Real(tensor))
519 }
520 other => Err(format!(
521 "issorted: unsupported input type {:?}; expected numeric, logical, complex, char, or string arrays",
522 other
523 )),
524 }
525}
526
527fn issorted_real(tensor: &Tensor, args: &IssortedArgs) -> Result<bool, String> {
528 if tensor.data.is_empty() {
529 return Ok(true);
530 }
531 match args.mode {
532 CheckMode::Dimension(dim) => Ok(check_real_dimension(tensor, dim, args)),
533 CheckMode::Rows => check_real_rows(tensor, args),
534 }
535}
536
537fn issorted_complex(tensor: &ComplexTensor, args: &IssortedArgs) -> Result<bool, String> {
538 if tensor.data.is_empty() {
539 return Ok(true);
540 }
541 match args.mode {
542 CheckMode::Dimension(dim) => Ok(check_complex_dimension(tensor, dim, args)),
543 CheckMode::Rows => check_complex_rows(tensor, args),
544 }
545}
546
547fn issorted_string(array: &StringArray, args: &IssortedArgs) -> Result<bool, String> {
548 if array.data.is_empty() {
549 return Ok(true);
550 }
551 if !matches!(args.comparison, ComparisonMethod::Auto) {
552 return Err("issorted: 'ComparisonMethod' is not supported for string arrays".to_string());
553 }
554 match args.mode {
555 CheckMode::Dimension(dim) => Ok(check_string_dimension(array, dim, args)),
556 CheckMode::Rows => check_string_rows(array, args),
557 }
558}
559
560fn check_real_dimension(tensor: &Tensor, dim: usize, args: &IssortedArgs) -> bool {
561 let dim_index = dim.saturating_sub(1);
562 if dim_index >= tensor.shape.len() {
563 return true;
564 }
565 let len_dim = tensor.shape[dim_index];
566 if len_dim <= 1 {
567 return true;
568 }
569
570 let before = product(&tensor.shape[..dim_index]);
571 let after = product(&tensor.shape[dim_index + 1..]);
572 let effective_comp = match args.comparison {
573 ComparisonMethod::Auto => ComparisonMethod::Real,
574 other => other,
575 };
576 let mut slice = Vec::with_capacity(len_dim);
577 for after_idx in 0..after {
578 for before_idx in 0..before {
579 slice.clear();
580 for k in 0..len_dim {
581 let idx = before_idx + k * before + after_idx * before * len_dim;
582 slice.push(tensor.data[idx]);
583 }
584 if !check_real_slice(&slice, args.direction, effective_comp, args.missing) {
585 return false;
586 }
587 }
588 }
589 true
590}
591
592fn check_complex_dimension(tensor: &ComplexTensor, dim: usize, args: &IssortedArgs) -> bool {
593 let dim_index = dim.saturating_sub(1);
594 if dim_index >= tensor.shape.len() {
595 return true;
596 }
597 let len_dim = tensor.shape[dim_index];
598 if len_dim <= 1 {
599 return true;
600 }
601 let before = product(&tensor.shape[..dim_index]);
602 let after = product(&tensor.shape[dim_index + 1..]);
603 let effective_comp = match args.comparison {
604 ComparisonMethod::Auto => ComparisonMethod::Abs,
605 other => other,
606 };
607 let mut slice = Vec::with_capacity(len_dim);
608 for after_idx in 0..after {
609 for before_idx in 0..before {
610 slice.clear();
611 for k in 0..len_dim {
612 let idx = before_idx + k * before + after_idx * before * len_dim;
613 slice.push(tensor.data[idx]);
614 }
615 if !check_complex_slice(&slice, args.direction, effective_comp, args.missing) {
616 return false;
617 }
618 }
619 }
620 true
621}
622
623fn check_string_dimension(array: &StringArray, dim: usize, args: &IssortedArgs) -> bool {
624 let dim_index = dim.saturating_sub(1);
625 if dim_index >= array.shape.len() {
626 return true;
627 }
628 let len_dim = array.shape[dim_index];
629 if len_dim <= 1 {
630 return true;
631 }
632 let before = product(&array.shape[..dim_index]);
633 let after = product(&array.shape[dim_index + 1..]);
634 let mut slice = Vec::with_capacity(len_dim);
635 for after_idx in 0..after {
636 for before_idx in 0..before {
637 slice.clear();
638 for k in 0..len_dim {
639 let idx = before_idx + k * before + after_idx * before * len_dim;
640 slice.push(array.data[idx].as_str());
641 }
642 if !check_string_slice(&slice, args.direction, args.missing) {
643 return false;
644 }
645 }
646 }
647 true
648}
649
650fn check_real_rows(tensor: &Tensor, args: &IssortedArgs) -> Result<bool, String> {
651 if tensor.shape.len() > 2 {
652 return Err("issorted: 'rows' expects a 2-D matrix".to_string());
653 }
654 let rows = tensor.rows();
655 let cols = tensor.cols();
656 if rows <= 1 || cols == 0 {
657 return Ok(true);
658 }
659 let effective_comp = match args.comparison {
660 ComparisonMethod::Auto => ComparisonMethod::Real,
661 other => other,
662 };
663 let orders = direction_orders(args.direction);
664 for &order in orders {
665 if real_rows_in_order(tensor, rows, cols, order, effective_comp, args.missing) {
666 return Ok(true);
667 }
668 }
669 Ok(false)
670}
671
672fn check_complex_rows(tensor: &ComplexTensor, args: &IssortedArgs) -> Result<bool, String> {
673 if tensor.shape.len() > 2 {
674 return Err("issorted: 'rows' expects a 2-D matrix".to_string());
675 }
676 let rows = tensor.rows;
677 let cols = tensor.cols;
678 if rows <= 1 || cols == 0 {
679 return Ok(true);
680 }
681 let effective_comp = match args.comparison {
682 ComparisonMethod::Auto => ComparisonMethod::Abs,
683 other => other,
684 };
685 let orders = direction_orders(args.direction);
686 for &order in orders {
687 if complex_rows_in_order(tensor, rows, cols, order, effective_comp, args.missing) {
688 return Ok(true);
689 }
690 }
691 Ok(false)
692}
693
694fn check_string_rows(array: &StringArray, args: &IssortedArgs) -> Result<bool, String> {
695 if array.shape.len() > 2 {
696 return Err("issorted: 'rows' expects a 2-D matrix".to_string());
697 }
698 let rows = array.rows;
699 let cols = array.cols;
700 if rows <= 1 || cols == 0 {
701 return Ok(true);
702 }
703 let orders = direction_orders(args.direction);
704 for &order in orders {
705 if string_rows_in_order(array, rows, cols, order, args.missing) {
706 return Ok(true);
707 }
708 }
709 Ok(false)
710}
711
712fn real_rows_in_order(
713 tensor: &Tensor,
714 rows: usize,
715 cols: usize,
716 order: OrderSpec,
717 comparison: ComparisonMethod,
718 missing: MissingPlacement,
719) -> bool {
720 if order.strict && tensor.data.iter().any(|v| v.is_nan()) {
721 return false;
722 }
723 let missing_resolved = missing.resolve(order.direction);
724 for row in 0..rows - 1 {
725 let ord = compare_real_row_pair(
726 tensor,
727 rows,
728 cols,
729 row,
730 row + 1,
731 order.direction,
732 comparison,
733 missing_resolved,
734 );
735 if !order_satisfied(ord, order) {
736 return false;
737 }
738 }
739 true
740}
741
742fn complex_rows_in_order(
743 tensor: &ComplexTensor,
744 rows: usize,
745 cols: usize,
746 order: OrderSpec,
747 comparison: ComparisonMethod,
748 missing: MissingPlacement,
749) -> bool {
750 if order.strict && tensor.data.iter().any(|v| complex_is_nan(*v)) {
751 return false;
752 }
753 let missing_resolved = missing.resolve(order.direction);
754 for row in 0..rows - 1 {
755 let ord = compare_complex_row_pair(
756 tensor,
757 rows,
758 cols,
759 row,
760 row + 1,
761 order.direction,
762 comparison,
763 missing_resolved,
764 );
765 if !order_satisfied(ord, order) {
766 return false;
767 }
768 }
769 true
770}
771
772fn string_rows_in_order(
773 array: &StringArray,
774 rows: usize,
775 cols: usize,
776 order: OrderSpec,
777 missing: MissingPlacement,
778) -> bool {
779 if order.strict && array.data.iter().any(|s| is_string_missing(s)) {
780 return false;
781 }
782 let missing_resolved = missing.resolve(order.direction);
783 for row in 0..rows - 1 {
784 let ord = compare_string_row_pair(
785 array,
786 rows,
787 cols,
788 row,
789 row + 1,
790 order.direction,
791 missing_resolved,
792 );
793 if !order_satisfied(ord, order) {
794 return false;
795 }
796 }
797 true
798}
799
800#[allow(clippy::too_many_arguments)]
801fn compare_real_row_pair(
802 tensor: &Tensor,
803 rows: usize,
804 cols: usize,
805 a: usize,
806 b: usize,
807 direction: SortDirection,
808 comparison: ComparisonMethod,
809 missing: MissingPlacementResolved,
810) -> Ordering {
811 for col in 0..cols {
812 let idx_a = a + col * rows;
813 let idx_b = b + col * rows;
814 let ord = compare_real_scalars(
815 tensor.data[idx_a],
816 tensor.data[idx_b],
817 direction,
818 comparison,
819 missing,
820 );
821 if ord != Ordering::Equal {
822 return ord;
823 }
824 }
825 Ordering::Equal
826}
827
828#[allow(clippy::too_many_arguments)]
829fn compare_complex_row_pair(
830 tensor: &ComplexTensor,
831 rows: usize,
832 cols: usize,
833 a: usize,
834 b: usize,
835 direction: SortDirection,
836 comparison: ComparisonMethod,
837 missing: MissingPlacementResolved,
838) -> Ordering {
839 for col in 0..cols {
840 let idx_a = a + col * rows;
841 let idx_b = b + col * rows;
842 let ord = compare_complex_scalars(
843 tensor.data[idx_a],
844 tensor.data[idx_b],
845 direction,
846 comparison,
847 missing,
848 );
849 if ord != Ordering::Equal {
850 return ord;
851 }
852 }
853 Ordering::Equal
854}
855
856fn compare_string_row_pair(
857 array: &StringArray,
858 rows: usize,
859 cols: usize,
860 a: usize,
861 b: usize,
862 direction: SortDirection,
863 missing: MissingPlacementResolved,
864) -> Ordering {
865 for col in 0..cols {
866 let idx_a = a + col * rows;
867 let idx_b = b + col * rows;
868 let ord = compare_string_scalars(
869 array.data[idx_a].as_str(),
870 array.data[idx_b].as_str(),
871 direction,
872 missing,
873 );
874 if ord != Ordering::Equal {
875 return ord;
876 }
877 }
878 Ordering::Equal
879}
880
881fn order_satisfied(ord: Ordering, order: OrderSpec) -> bool {
882 match order.direction {
883 SortDirection::Ascend => match ord {
884 Ordering::Greater => false,
885 Ordering::Equal => !order.strict,
886 Ordering::Less => true,
887 },
888 SortDirection::Descend => match ord {
889 Ordering::Less => true,
890 Ordering::Equal => !order.strict,
891 Ordering::Greater => false,
892 },
893 }
894}
895
896fn check_real_slice(
897 slice: &[f64],
898 direction: Direction,
899 comparison: ComparisonMethod,
900 missing: MissingPlacement,
901) -> bool {
902 if slice.len() <= 1 {
903 return true;
904 }
905 let orders = direction_orders(direction);
906 for &order in orders {
907 if order.strict && slice.iter().any(|v| v.is_nan()) {
908 continue;
909 }
910 let missing_resolved = missing.resolve(order.direction);
911 if real_slice_in_order(slice, order, comparison, missing_resolved) {
912 return true;
913 }
914 }
915 false
916}
917
918fn check_complex_slice(
919 slice: &[(f64, f64)],
920 direction: Direction,
921 comparison: ComparisonMethod,
922 missing: MissingPlacement,
923) -> bool {
924 if slice.len() <= 1 {
925 return true;
926 }
927 let orders = direction_orders(direction);
928 for &order in orders {
929 if order.strict && slice.iter().any(|v| complex_is_nan(*v)) {
930 continue;
931 }
932 let missing_resolved = missing.resolve(order.direction);
933 if complex_slice_in_order(slice, order, comparison, missing_resolved) {
934 return true;
935 }
936 }
937 false
938}
939
940fn check_string_slice(slice: &[&str], direction: Direction, missing: MissingPlacement) -> bool {
941 if slice.len() <= 1 {
942 return true;
943 }
944 let orders = direction_orders(direction);
945 for &order in orders {
946 if order.strict && slice.iter().any(|s| is_string_missing(s)) {
947 continue;
948 }
949 let missing_resolved = missing.resolve(order.direction);
950 if string_slice_in_order(slice, order, missing_resolved) {
951 return true;
952 }
953 }
954 false
955}
956
957fn real_slice_in_order(
958 slice: &[f64],
959 order: OrderSpec,
960 comparison: ComparisonMethod,
961 missing: MissingPlacementResolved,
962) -> bool {
963 for pair in slice.windows(2) {
964 let ord = compare_real_scalars(pair[0], pair[1], order.direction, comparison, missing);
965 if !order_satisfied(ord, order) {
966 return false;
967 }
968 }
969 true
970}
971
972fn complex_slice_in_order(
973 slice: &[(f64, f64)],
974 order: OrderSpec,
975 comparison: ComparisonMethod,
976 missing: MissingPlacementResolved,
977) -> bool {
978 for pair in slice.windows(2) {
979 let ord = compare_complex_scalars(pair[0], pair[1], order.direction, comparison, missing);
980 if !order_satisfied(ord, order) {
981 return false;
982 }
983 }
984 true
985}
986
987fn string_slice_in_order(
988 slice: &[&str],
989 order: OrderSpec,
990 missing: MissingPlacementResolved,
991) -> bool {
992 for pair in slice.windows(2) {
993 let ord = compare_string_scalars(pair[0], pair[1], order.direction, missing);
994 if !order_satisfied(ord, order) {
995 return false;
996 }
997 }
998 true
999}
1000
1001fn compare_real_scalars(
1002 a: f64,
1003 b: f64,
1004 direction: SortDirection,
1005 comparison: ComparisonMethod,
1006 missing: MissingPlacementResolved,
1007) -> Ordering {
1008 match (a.is_nan(), b.is_nan()) {
1009 (true, true) => Ordering::Equal,
1010 (true, false) => match missing {
1011 MissingPlacementResolved::First => Ordering::Less,
1012 MissingPlacementResolved::Last => Ordering::Greater,
1013 },
1014 (false, true) => match missing {
1015 MissingPlacementResolved::First => Ordering::Greater,
1016 MissingPlacementResolved::Last => Ordering::Less,
1017 },
1018 (false, false) => compare_real_finite_scalars(a, b, direction, comparison),
1019 }
1020}
1021
1022fn compare_real_finite_scalars(
1023 a: f64,
1024 b: f64,
1025 direction: SortDirection,
1026 comparison: ComparisonMethod,
1027) -> Ordering {
1028 if matches!(comparison, ComparisonMethod::Abs) {
1029 let abs_cmp = a.abs().partial_cmp(&b.abs()).unwrap_or(Ordering::Equal);
1030 if abs_cmp != Ordering::Equal {
1031 return match direction {
1032 SortDirection::Ascend => abs_cmp,
1033 SortDirection::Descend => abs_cmp.reverse(),
1034 };
1035 }
1036 }
1037 match direction {
1038 SortDirection::Ascend => a.partial_cmp(&b).unwrap_or(Ordering::Equal),
1039 SortDirection::Descend => b.partial_cmp(&a).unwrap_or(Ordering::Equal),
1040 }
1041}
1042
1043fn compare_complex_scalars(
1044 a: (f64, f64),
1045 b: (f64, f64),
1046 direction: SortDirection,
1047 comparison: ComparisonMethod,
1048 missing: MissingPlacementResolved,
1049) -> Ordering {
1050 match (complex_is_nan(a), complex_is_nan(b)) {
1051 (true, true) => Ordering::Equal,
1052 (true, false) => match missing {
1053 MissingPlacementResolved::First => Ordering::Less,
1054 MissingPlacementResolved::Last => Ordering::Greater,
1055 },
1056 (false, true) => match missing {
1057 MissingPlacementResolved::First => Ordering::Greater,
1058 MissingPlacementResolved::Last => Ordering::Less,
1059 },
1060 (false, false) => compare_complex_finite_scalars(a, b, direction, comparison),
1061 }
1062}
1063
1064fn compare_complex_finite_scalars(
1065 a: (f64, f64),
1066 b: (f64, f64),
1067 direction: SortDirection,
1068 comparison: ComparisonMethod,
1069) -> Ordering {
1070 match comparison {
1071 ComparisonMethod::Real => compare_complex_real_first(a, b, direction),
1072 ComparisonMethod::Abs | ComparisonMethod::Auto => {
1073 let abs_cmp = complex_abs(a)
1074 .partial_cmp(&complex_abs(b))
1075 .unwrap_or(Ordering::Equal);
1076 if abs_cmp != Ordering::Equal {
1077 return match direction {
1078 SortDirection::Ascend => abs_cmp,
1079 SortDirection::Descend => abs_cmp.reverse(),
1080 };
1081 }
1082 compare_complex_real_first(a, b, direction)
1083 }
1084 }
1085}
1086
1087fn compare_complex_real_first(a: (f64, f64), b: (f64, f64), direction: SortDirection) -> Ordering {
1088 let real_cmp = match direction {
1089 SortDirection::Ascend => a.0.partial_cmp(&b.0),
1090 SortDirection::Descend => b.0.partial_cmp(&a.0),
1091 }
1092 .unwrap_or(Ordering::Equal);
1093 if real_cmp != Ordering::Equal {
1094 return real_cmp;
1095 }
1096 match direction {
1097 SortDirection::Ascend => a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal),
1098 SortDirection::Descend => b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal),
1099 }
1100}
1101
1102fn compare_string_scalars(
1103 a: &str,
1104 b: &str,
1105 direction: SortDirection,
1106 missing: MissingPlacementResolved,
1107) -> Ordering {
1108 let missing_a = is_string_missing(a);
1109 let missing_b = is_string_missing(b);
1110 match (missing_a, missing_b) {
1111 (true, true) => Ordering::Equal,
1112 (true, false) => match missing {
1113 MissingPlacementResolved::First => Ordering::Less,
1114 MissingPlacementResolved::Last => Ordering::Greater,
1115 },
1116 (false, true) => match missing {
1117 MissingPlacementResolved::First => Ordering::Greater,
1118 MissingPlacementResolved::Last => Ordering::Less,
1119 },
1120 (false, false) => match direction {
1121 SortDirection::Ascend => a.cmp(b),
1122 SortDirection::Descend => b.cmp(a),
1123 },
1124 }
1125}
1126
1127fn complex_is_nan(value: (f64, f64)) -> bool {
1128 value.0.is_nan() || value.1.is_nan()
1129}
1130
1131fn complex_abs(value: (f64, f64)) -> f64 {
1132 value.0.hypot(value.1)
1133}
1134
1135fn is_string_missing(value: &str) -> bool {
1136 value.eq_ignore_ascii_case("<missing>")
1137}
1138
1139fn direction_orders(direction: Direction) -> &'static [OrderSpec] {
1140 match direction {
1141 Direction::Ascend => &[OrderSpec {
1142 direction: SortDirection::Ascend,
1143 strict: false,
1144 }],
1145 Direction::Descend => &[OrderSpec {
1146 direction: SortDirection::Descend,
1147 strict: false,
1148 }],
1149 Direction::Monotonic => &[
1150 OrderSpec {
1151 direction: SortDirection::Ascend,
1152 strict: false,
1153 },
1154 OrderSpec {
1155 direction: SortDirection::Descend,
1156 strict: false,
1157 },
1158 ],
1159 Direction::StrictAscend => &[OrderSpec {
1160 direction: SortDirection::Ascend,
1161 strict: true,
1162 }],
1163 Direction::StrictDescend => &[OrderSpec {
1164 direction: SortDirection::Descend,
1165 strict: true,
1166 }],
1167 Direction::StrictMonotonic => &[
1168 OrderSpec {
1169 direction: SortDirection::Ascend,
1170 strict: true,
1171 },
1172 OrderSpec {
1173 direction: SortDirection::Descend,
1174 strict: true,
1175 },
1176 ],
1177 }
1178}
1179
1180fn default_dimension(shape: &[usize]) -> usize {
1181 if shape.is_empty() {
1182 return 1;
1183 }
1184 shape
1185 .iter()
1186 .position(|&extent| extent > 1)
1187 .map(|idx| idx + 1)
1188 .unwrap_or(1)
1189}
1190
1191fn product(slice: &[usize]) -> usize {
1192 slice
1193 .iter()
1194 .copied()
1195 .fold(1usize, |acc, value| acc.saturating_mul(value.max(1)))
1196}
1197
1198fn value_to_string_lower(value: &Value) -> Option<String> {
1199 match String::try_from(value) {
1200 Ok(text) => Some(text.trim().to_ascii_lowercase()),
1201 Err(_) => None,
1202 }
1203}
1204
1205fn char_array_to_tensor(array: &CharArray) -> Result<Tensor, String> {
1206 let rows = array.rows;
1207 let cols = array.cols;
1208 let mut data = vec![0.0f64; rows * cols];
1209 for r in 0..rows {
1210 for c in 0..cols {
1211 let ch = array.data[r * cols + c];
1212 let idx = r + c * rows;
1213 data[idx] = ch as u32 as f64;
1214 }
1215 }
1216 Tensor::new(data, vec![rows, cols]).map_err(|e| format!("issorted: {e}"))
1217}
1218
1219#[cfg(test)]
1220mod tests {
1221 use super::*;
1222 use crate::builtins::common::test_support;
1223 use runmat_builtins::{IntValue, LogicalArray, Value};
1224
1225 #[test]
1226 fn issorted_numeric_vector_true() {
1227 let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
1228 let result = issorted_builtin(Value::Tensor(tensor), vec![]).expect("issorted");
1229 assert_eq!(result, Value::Bool(true));
1230 }
1231
1232 #[test]
1233 fn issorted_numeric_vector_false() {
1234 let tensor = Tensor::new(vec![3.0, 2.0, 1.0], vec![3, 1]).unwrap();
1235 let result = issorted_builtin(Value::Tensor(tensor), vec![]).expect("issorted");
1236 assert_eq!(result, Value::Bool(false));
1237 }
1238
1239 #[test]
1240 fn issorted_logical_vector() {
1241 let logical = LogicalArray::new(vec![0, 1, 1], vec![3, 1]).unwrap();
1242 let result =
1243 issorted_builtin(Value::LogicalArray(logical), vec![]).expect("issorted logical");
1244 assert_eq!(result, Value::Bool(true));
1245 }
1246
1247 #[test]
1248 fn issorted_dimension_argument() {
1249 let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 3.0], vec![2, 2]).unwrap();
1250 let args = vec![Value::Int(IntValue::I32(2))];
1251 let result = issorted_builtin(Value::Tensor(tensor), args).expect("issorted");
1252 assert_eq!(result, Value::Bool(true));
1253 }
1254
1255 #[test]
1256 fn issorted_strictascend_rejects_duplicates() {
1257 let tensor = Tensor::new(vec![1.0, 1.0, 2.0], vec![3, 1]).unwrap();
1258 let args = vec![Value::from("strictascend")];
1259 let result = issorted_builtin(Value::Tensor(tensor), args).expect("issorted");
1260 assert_eq!(result, Value::Bool(false));
1261 }
1262
1263 #[test]
1264 fn issorted_strictmonotonic_true_with_descend() {
1265 let tensor = Tensor::new(vec![9.0, 4.0, 1.0], vec![3, 1]).unwrap();
1266 let args = vec![Value::from("strictmonotonic")];
1267 let result = issorted_builtin(Value::Tensor(tensor), args).expect("issorted");
1268 assert_eq!(result, Value::Bool(true));
1269 }
1270
1271 #[test]
1272 fn issorted_strictmonotonic_rejects_plateaus() {
1273 let tensor = Tensor::new(vec![4.0, 4.0, 2.0, 1.0], vec![4, 1]).unwrap();
1274 let args = vec![Value::from("strictmonotonic")];
1275 let result = issorted_builtin(Value::Tensor(tensor), args).expect("issorted");
1276 assert_eq!(result, Value::Bool(false));
1277 }
1278
1279 #[test]
1280 fn issorted_monotonic_accepts_descending() {
1281 let tensor = Tensor::new(vec![5.0, 4.0, 4.0, 1.0], vec![4, 1]).unwrap();
1282 let args = vec![Value::from("monotonic")];
1283 let result = issorted_builtin(Value::Tensor(tensor), args).expect("issorted");
1284 assert_eq!(result, Value::Bool(true));
1285 }
1286
1287 #[test]
1288 fn issorted_monotonic_rejects_unsorted_data() {
1289 let tensor = Tensor::new(vec![1.0, 3.0, 2.0], vec![3, 1]).unwrap();
1290 let args = vec![Value::from("monotonic")];
1291 let result = issorted_builtin(Value::Tensor(tensor), args).expect("issorted");
1292 assert_eq!(result, Value::Bool(false));
1293 }
1294
1295 #[test]
1296 fn issorted_missingplacement_first() {
1297 let tensor = Tensor::new(vec![f64::NAN, 2.0, 3.0], vec![3, 1]).unwrap();
1298 let args = vec![Value::from("MissingPlacement"), Value::from("first")];
1299 let result = issorted_builtin(Value::Tensor(tensor), args).expect("issorted");
1300 assert_eq!(result, Value::Bool(true));
1301 }
1302
1303 #[test]
1304 fn issorted_missingplacement_first_violation() {
1305 let tensor = Tensor::new(vec![2.0, f64::NAN, 3.0], vec![3, 1]).unwrap();
1306 let args = vec![Value::from("MissingPlacement"), Value::from("first")];
1307 let result = issorted_builtin(Value::Tensor(tensor), args).expect("issorted");
1308 assert_eq!(result, Value::Bool(false));
1309 }
1310
1311 #[test]
1312 fn issorted_missingplacement_auto_descend_prefers_front() {
1313 let tensor = Tensor::new(vec![f64::NAN, 5.0, 3.0], vec![3, 1]).unwrap();
1314 let args = vec![Value::from("descend")];
1315 let result = issorted_builtin(Value::Tensor(tensor), args).expect("issorted");
1316 assert_eq!(result, Value::Bool(true));
1317 }
1318
1319 #[test]
1320 fn issorted_comparison_abs() {
1321 let tensor = Tensor::new(vec![-1.0, 1.5, -2.0], vec![3, 1]).unwrap();
1322 let args = vec![Value::from("ComparisonMethod"), Value::from("abs")];
1323 let result = issorted_builtin(Value::Tensor(tensor), args).expect("issorted");
1324 assert_eq!(result, Value::Bool(true));
1325 }
1326
1327 #[test]
1328 fn issorted_complex_abs_method() {
1329 let tensor =
1330 ComplexTensor::new(vec![(1.0, 1.0), (2.0, 0.0), (2.0, 3.0)], vec![3, 1]).unwrap();
1331 let args = vec![Value::from("ComparisonMethod"), Value::from("abs")];
1332 let result = issorted_builtin(Value::ComplexTensor(tensor), args).expect("issorted");
1333 assert_eq!(result, Value::Bool(true));
1334 }
1335
1336 #[test]
1337 fn issorted_complex_real_method() {
1338 let tensor =
1339 ComplexTensor::new(vec![(1.0, 1.0), (1.0, 1.0), (2.0, 0.0)], vec![3, 1]).unwrap();
1340 let args = vec![
1341 Value::from("ComparisonMethod"),
1342 Value::from("real"),
1343 Value::from("strictascend"),
1344 ];
1345 let result = issorted_builtin(Value::ComplexTensor(tensor), args).expect("issorted");
1346 assert_eq!(result, Value::Bool(false));
1347 }
1348
1349 #[test]
1350 fn issorted_rows_true() {
1351 let tensor = Tensor::new(vec![1.0, 2.0, 1.0, 3.0], vec![2, 2]).unwrap();
1352 let args = vec![Value::from("rows")];
1353 let result = issorted_builtin(Value::Tensor(tensor), args).expect("issorted");
1354 assert_eq!(result, Value::Bool(true));
1355 }
1356
1357 #[test]
1358 fn issorted_rows_dimension_error() {
1359 let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2, 1]).unwrap();
1360 let result = issorted_builtin(Value::Tensor(tensor), vec![Value::from("rows")]);
1361 assert!(result.is_err());
1362 }
1363
1364 #[test]
1365 fn issorted_rows_descend_false() {
1366 let tensor = Tensor::new(vec![1.0, 2.0, 4.0, 0.0], vec![2, 2]).unwrap();
1367 let args = vec![Value::from("rows"), Value::from("descend")];
1368 let result = issorted_builtin(Value::Tensor(tensor), args).expect("issorted");
1369 assert_eq!(result, Value::Bool(false));
1370 }
1371
1372 #[test]
1373 fn issorted_string_dimension() {
1374 let array = StringArray::new(
1375 vec![
1376 "pear".into(),
1377 "plum".into(),
1378 "apple".into(),
1379 "banana".into(),
1380 ],
1381 vec![2, 2],
1382 )
1383 .unwrap();
1384 let args = vec![Value::Int(IntValue::I32(2))];
1385 let result =
1386 issorted_builtin(Value::StringArray(array), args).expect("issorted string dim");
1387 assert_eq!(result, Value::Bool(false));
1388 }
1389
1390 #[test]
1391 fn issorted_string_missingplacement_last() {
1392 let array = StringArray::new(
1393 vec!["apple".into(), "banana".into(), "<missing>".into()],
1394 vec![3, 1],
1395 )
1396 .unwrap();
1397 let args = vec![Value::from("MissingPlacement"), Value::from("last")];
1398 let result =
1399 issorted_builtin(Value::StringArray(array), args).expect("issorted string placement");
1400 assert_eq!(result, Value::Bool(true));
1401 }
1402
1403 #[test]
1404 fn issorted_string_missingplacement_last_violation() {
1405 let array = StringArray::new(vec!["<missing>".into(), "apple".into()], vec![2, 1]).unwrap();
1406 let args = vec![Value::from("MissingPlacement"), Value::from("last")];
1407 let result =
1408 issorted_builtin(Value::StringArray(array), args).expect("issorted string placement");
1409 assert_eq!(result, Value::Bool(false));
1410 }
1411
1412 #[test]
1413 fn issorted_string_comparison_method_error() {
1414 let array = StringArray::new(vec!["apple".into(), "berry".into()], vec![2, 1]).unwrap();
1415 let args = vec![Value::from("ComparisonMethod"), Value::from("real")];
1416 let result = issorted_builtin(Value::StringArray(array), args);
1417 assert!(result.is_err());
1418 }
1419
1420 #[test]
1421 fn issorted_char_array_input() {
1422 let chars = CharArray::new(vec!['a', 'c', 'e'], 1, 3).unwrap();
1423 let result = issorted_builtin(Value::CharArray(chars), vec![]).expect("issorted char");
1424 assert_eq!(result, Value::Bool(true));
1425 }
1426
1427 #[test]
1428 fn issorted_duplicate_direction_error() {
1429 let tensor = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
1430 let args = vec![Value::from("ascend"), Value::from("descend")];
1431 let result = issorted_builtin(Value::Tensor(tensor), args);
1432 assert!(result.is_err());
1433 }
1434
1435 #[test]
1436 fn issorted_gpu_roundtrip() {
1437 test_support::with_test_provider(|provider| {
1438 let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
1439 let view = runmat_accelerate_api::HostTensorView {
1440 data: &tensor.data,
1441 shape: &tensor.shape,
1442 };
1443 let handle = provider.upload(&view).expect("upload");
1444 let result = issorted_builtin(Value::GpuTensor(handle), vec![]).expect("issorted gpu");
1445 assert_eq!(result, Value::Bool(true));
1446 });
1447 }
1448
1449 #[test]
1450 #[cfg(feature = "wgpu")]
1451 fn issorted_wgpu_matches_cpu() {
1452 let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
1453 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
1454 );
1455 let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
1456 let cpu = issorted_builtin(Value::Tensor(tensor.clone()), vec![]).expect("cpu issorted");
1457 let view = runmat_accelerate_api::HostTensorView {
1458 data: &tensor.data,
1459 shape: &tensor.shape,
1460 };
1461 let handle = runmat_accelerate_api::provider()
1462 .expect("wgpu provider")
1463 .upload(&view)
1464 .expect("upload");
1465 let gpu = issorted_builtin(Value::GpuTensor(handle), vec![]).expect("gpu issorted");
1466 assert_eq!(gpu, cpu);
1467 }
1468
1469 #[test]
1470 #[cfg(feature = "doc_export")]
1471 fn issorted_doc_examples() {
1472 let blocks = test_support::doc_examples(DOC_MD);
1473 assert!(!blocks.is_empty());
1474 }
1475}