1use std::cmp::Ordering;
4
5use runmat_builtins::{
6 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
7 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
8 CharArray, ComplexTensor, StringArray, Tensor, Value,
9};
10use runmat_macros::runtime_builtin;
11
12use super::type_resolvers::bool_output_type;
13use crate::build_runtime_error;
14use crate::builtins::common::gpu_helpers;
15use crate::builtins::common::spec::{
16 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
17 ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
18};
19use crate::builtins::common::tensor;
20
21#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::array::sorting_sets::issorted")]
22pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
23 name: "issorted",
24 op_kind: GpuOpKind::Custom("predicate"),
25 supported_precisions: &[ScalarType::F32, ScalarType::F64],
26 broadcast: BroadcastSemantics::None,
27 provider_hooks: &[],
28 constant_strategy: ConstantStrategy::InlineLiteral,
29 residency: ResidencyPolicy::GatherImmediately,
30 nan_mode: ReductionNaN::Include,
31 two_pass_threshold: None,
32 workgroup_size: None,
33 accepts_nan_mode: true,
34 notes: "GPU inputs gather to the host until providers implement dedicated predicate kernels.",
35};
36
37#[runmat_macros::register_fusion_spec(
38 builtin_path = "crate::builtins::array::sorting_sets::issorted"
39)]
40pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
41 name: "issorted",
42 shape: ShapeRequirements::Any,
43 constant_strategy: ConstantStrategy::InlineLiteral,
44 elementwise: None,
45 reduction: None,
46 emits_nan: false,
47 notes: "Predicate builtin evaluated outside fusion; planner prevents kernel generation.",
48};
49
50const BUILTIN_NAME: &str = "issorted";
51
52const ISSORTED_OUTPUT_TF: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
53 name: "tf",
54 ty: BuiltinParamType::LogicalArray,
55 arity: BuiltinParamArity::Required,
56 default: None,
57 description: "True when data is already sorted by requested criteria.",
58}];
59
60const ISSORTED_INPUTS_A: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
61 name: "A",
62 ty: BuiltinParamType::Any,
63 arity: BuiltinParamArity::Required,
64 default: None,
65 description: "Input array.",
66}];
67
68const ISSORTED_INPUTS_A_ARG1: [BuiltinParamDescriptor; 2] = [
69 BuiltinParamDescriptor {
70 name: "A",
71 ty: BuiltinParamType::Any,
72 arity: BuiltinParamArity::Required,
73 default: None,
74 description: "Input array.",
75 },
76 BuiltinParamDescriptor {
77 name: "arg1",
78 ty: BuiltinParamType::Any,
79 arity: BuiltinParamArity::Required,
80 default: None,
81 description: "Dimension selector or direction token (including 'rows').",
82 },
83];
84
85const ISSORTED_INPUTS_A_ARG1_ARG2: [BuiltinParamDescriptor; 3] = [
86 BuiltinParamDescriptor {
87 name: "A",
88 ty: BuiltinParamType::Any,
89 arity: BuiltinParamArity::Required,
90 default: None,
91 description: "Input array.",
92 },
93 BuiltinParamDescriptor {
94 name: "arg1",
95 ty: BuiltinParamType::Any,
96 arity: BuiltinParamArity::Required,
97 default: None,
98 description: "Dimension selector or direction token (including 'rows').",
99 },
100 BuiltinParamDescriptor {
101 name: "arg2",
102 ty: BuiltinParamType::Any,
103 arity: BuiltinParamArity::Required,
104 default: None,
105 description: "Direction token or additional mode selector.",
106 },
107];
108
109const ISSORTED_INPUTS_COMPARISON_METHOD: [BuiltinParamDescriptor; 4] = [
110 BuiltinParamDescriptor {
111 name: "A",
112 ty: BuiltinParamType::Any,
113 arity: BuiltinParamArity::Required,
114 default: None,
115 description: "Input array.",
116 },
117 BuiltinParamDescriptor {
118 name: "arg",
119 ty: BuiltinParamType::Any,
120 arity: BuiltinParamArity::Variadic,
121 default: None,
122 description: "Optional dimension/direction/rows arguments.",
123 },
124 BuiltinParamDescriptor {
125 name: "name",
126 ty: BuiltinParamType::StringScalar,
127 arity: BuiltinParamArity::Required,
128 default: Some("\"ComparisonMethod\""),
129 description: "Name-value option key.",
130 },
131 BuiltinParamDescriptor {
132 name: "method",
133 ty: BuiltinParamType::StringScalar,
134 arity: BuiltinParamArity::Required,
135 default: Some("\"auto\""),
136 description: "Comparison method: 'auto', 'real', or 'abs'.",
137 },
138];
139
140const ISSORTED_INPUTS_MISSING_PLACEMENT: [BuiltinParamDescriptor; 4] = [
141 BuiltinParamDescriptor {
142 name: "A",
143 ty: BuiltinParamType::Any,
144 arity: BuiltinParamArity::Required,
145 default: None,
146 description: "Input array.",
147 },
148 BuiltinParamDescriptor {
149 name: "arg",
150 ty: BuiltinParamType::Any,
151 arity: BuiltinParamArity::Variadic,
152 default: None,
153 description: "Optional dimension/direction/rows arguments.",
154 },
155 BuiltinParamDescriptor {
156 name: "name",
157 ty: BuiltinParamType::StringScalar,
158 arity: BuiltinParamArity::Required,
159 default: Some("\"MissingPlacement\""),
160 description: "Name-value option key.",
161 },
162 BuiltinParamDescriptor {
163 name: "placement",
164 ty: BuiltinParamType::StringScalar,
165 arity: BuiltinParamArity::Required,
166 default: Some("\"auto\""),
167 description: "Missing placement policy: 'auto', 'first', or 'last'.",
168 },
169];
170
171const ISSORTED_SIGNATURES: [BuiltinSignatureDescriptor; 5] = [
172 BuiltinSignatureDescriptor {
173 label: "tf = issorted(A)",
174 inputs: &ISSORTED_INPUTS_A,
175 outputs: &ISSORTED_OUTPUT_TF,
176 },
177 BuiltinSignatureDescriptor {
178 label: "tf = issorted(A, arg1)",
179 inputs: &ISSORTED_INPUTS_A_ARG1,
180 outputs: &ISSORTED_OUTPUT_TF,
181 },
182 BuiltinSignatureDescriptor {
183 label: "tf = issorted(A, arg1, arg2)",
184 inputs: &ISSORTED_INPUTS_A_ARG1_ARG2,
185 outputs: &ISSORTED_OUTPUT_TF,
186 },
187 BuiltinSignatureDescriptor {
188 label: "tf = issorted(A, ..., \"ComparisonMethod\", method)",
189 inputs: &ISSORTED_INPUTS_COMPARISON_METHOD,
190 outputs: &ISSORTED_OUTPUT_TF,
191 },
192 BuiltinSignatureDescriptor {
193 label: "tf = issorted(A, ..., \"MissingPlacement\", placement)",
194 inputs: &ISSORTED_INPUTS_MISSING_PLACEMENT,
195 outputs: &ISSORTED_OUTPUT_TF,
196 },
197];
198
199const ISSORTED_ERROR_ROWS_REQUIRES_2D: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
200 code: "RM.ISSORTED.ROWS_REQUIRES_2D",
201 identifier: Some("RunMat:issorted:RowsRequiresTwoDimensionalInput"),
202 when: "'rows' mode is used with non-2D input.",
203 message: "issorted: 'rows' expects a 2-D matrix",
204};
205
206const ISSORTED_ERROR_STRING_COMPARISON_UNSUPPORTED: BuiltinErrorDescriptor =
207 BuiltinErrorDescriptor {
208 code: "RM.ISSORTED.STRING_COMPARISON_UNSUPPORTED",
209 identifier: Some("RunMat:issorted:StringComparisonMethodUnsupported"),
210 when: "ComparisonMethod is used with string arrays.",
211 message: "issorted: 'ComparisonMethod' is not supported for string arrays",
212 };
213
214const ISSORTED_ERROR_DUPLICATE_DIRECTION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
215 code: "RM.ISSORTED.DUPLICATE_DIRECTION",
216 identifier: Some("RunMat:issorted:DuplicateDirection"),
217 when: "Multiple direction tokens are provided.",
218 message: "issorted: sorting direction specified more than once",
219};
220
221const ISSORTED_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
222 code: "RM.ISSORTED.INVALID_ARGUMENT",
223 identifier: Some("RunMat:issorted:InvalidArgument"),
224 when: "Parser encounters invalid or unrecognized option/value arguments.",
225 message: "issorted: invalid argument sequence",
226};
227
228const ISSORTED_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
229 code: "RM.ISSORTED.INVALID_INPUT",
230 identifier: Some("RunMat:issorted:InvalidInput"),
231 when: "Input type cannot be normalized into a supported sortable representation.",
232 message: "issorted: invalid input type",
233};
234
235const ISSORTED_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
236 code: "RM.ISSORTED.INTERNAL",
237 identifier: Some("RunMat:issorted:Internal"),
238 when: "Internal conversion/allocation paths fail.",
239 message: "issorted: internal operation failed",
240};
241
242const ISSORTED_ERRORS: [BuiltinErrorDescriptor; 6] = [
243 ISSORTED_ERROR_ROWS_REQUIRES_2D,
244 ISSORTED_ERROR_STRING_COMPARISON_UNSUPPORTED,
245 ISSORTED_ERROR_DUPLICATE_DIRECTION,
246 ISSORTED_ERROR_INVALID_ARGUMENT,
247 ISSORTED_ERROR_INVALID_INPUT,
248 ISSORTED_ERROR_INTERNAL,
249];
250
251pub const ISSORTED_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
252 signatures: &ISSORTED_SIGNATURES,
253 output_mode: BuiltinOutputMode::Fixed,
254 completion_policy: BuiltinCompletionPolicy::Public,
255 errors: &ISSORTED_ERRORS,
256};
257
258fn issorted_error(
259 error: &'static BuiltinErrorDescriptor,
260 message: impl Into<String>,
261) -> crate::RuntimeError {
262 let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
263 if let Some(identifier) = error.identifier {
264 builder = builder.with_identifier(identifier);
265 }
266 builder.build()
267}
268
269fn issorted_invalid_argument(message: impl Into<String>) -> crate::RuntimeError {
270 issorted_error(&ISSORTED_ERROR_INVALID_ARGUMENT, message)
271}
272
273fn issorted_invalid_input(message: impl Into<String>) -> crate::RuntimeError {
274 issorted_error(&ISSORTED_ERROR_INVALID_INPUT, message)
275}
276
277fn issorted_internal(message: impl Into<String>) -> crate::RuntimeError {
278 issorted_error(&ISSORTED_ERROR_INTERNAL, message)
279}
280
281#[runtime_builtin(
282 name = "issorted",
283 category = "array/sorting_sets",
284 summary = "Determine whether an array is already sorted.",
285 keywords = "issorted,sorted,monotonic,rows",
286 accel = "sink",
287 sink = true,
288 type_resolver(bool_output_type),
289 descriptor(crate::builtins::array::sorting_sets::issorted::ISSORTED_DESCRIPTOR),
290 builtin_path = "crate::builtins::array::sorting_sets::issorted"
291)]
292async fn issorted_builtin(value: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
293 let input = normalize_input(value).await?;
294 let shape = input.shape();
295 let args = IssortedArgs::parse(&rest, &shape)?;
296
297 let result = match input {
298 InputArray::Real(tensor) => issorted_real(&tensor, &args)?,
299 InputArray::Complex(tensor) => issorted_complex(&tensor, &args)?,
300 InputArray::String(array) => issorted_string(&array, &args)?,
301 };
302
303 Ok(Value::Bool(result))
304}
305
306struct IssortedArgs {
307 mode: CheckMode,
308 direction: Direction,
309 comparison: ComparisonMethod,
310 missing: MissingPlacement,
311}
312
313#[derive(Clone, Copy, Debug, PartialEq, Eq)]
314enum CheckMode {
315 Dimension(usize),
316 Rows,
317}
318
319#[derive(Clone, Copy, Debug, PartialEq, Eq)]
320enum Direction {
321 Ascend,
322 Descend,
323 Monotonic,
324 StrictAscend,
325 StrictDescend,
326 StrictMonotonic,
327}
328
329#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
330enum ComparisonMethod {
331 #[default]
332 Auto,
333 Real,
334 Abs,
335}
336
337#[derive(Clone, Copy, Debug, PartialEq, Eq)]
338enum MissingPlacement {
339 Auto,
340 First,
341 Last,
342}
343
344#[derive(Clone, Copy, Debug, PartialEq, Eq)]
345enum MissingPlacementResolved {
346 First,
347 Last,
348}
349
350impl MissingPlacement {
351 fn resolve(self, direction: SortDirection) -> MissingPlacementResolved {
352 match self {
353 MissingPlacement::First => MissingPlacementResolved::First,
354 MissingPlacement::Last => MissingPlacementResolved::Last,
355 MissingPlacement::Auto => match direction {
356 SortDirection::Ascend => MissingPlacementResolved::Last,
357 SortDirection::Descend => MissingPlacementResolved::First,
358 },
359 }
360 }
361}
362
363#[derive(Clone, Copy, Debug, PartialEq, Eq)]
364enum SortDirection {
365 Ascend,
366 Descend,
367}
368
369#[derive(Clone, Copy)]
370struct OrderSpec {
371 direction: SortDirection,
372 strict: bool,
373}
374
375enum InputArray {
376 Real(Tensor),
377 Complex(ComplexTensor),
378 String(StringArray),
379}
380
381impl InputArray {
382 fn shape(&self) -> Vec<usize> {
383 match self {
384 InputArray::Real(t) => t.shape.clone(),
385 InputArray::Complex(t) => t.shape.clone(),
386 InputArray::String(sa) => sa.shape.clone(),
387 }
388 }
389}
390
391impl IssortedArgs {
392 fn parse(args: &[Value], shape: &[usize]) -> crate::BuiltinResult<Self> {
393 let mut dim_arg: Option<usize> = None;
394 let mut direction: Option<Direction> = None;
395 let mut comparison: ComparisonMethod = ComparisonMethod::Auto;
396 let mut missing: MissingPlacement = MissingPlacement::Auto;
397 let mut mode = CheckMode::Dimension(default_dimension(shape));
398 let mut saw_rows = false;
399
400 let mut idx = 0;
401 while idx < args.len() {
402 let arg = &args[idx];
403 if let Some(token) = value_to_string_lower(arg) {
404 match token.as_str() {
405 "rows" => {
406 if saw_rows {
407 return Err(issorted_invalid_argument(
408 "issorted: 'rows' specified more than once",
409 ));
410 }
411 if dim_arg.is_some() {
412 return Err(issorted_invalid_argument(
413 "issorted: cannot combine 'rows' with a dimension argument",
414 ));
415 }
416 saw_rows = true;
417 mode = CheckMode::Rows;
418 idx += 1;
419 continue;
420 }
421 "ascend" => {
422 ensure_unique_direction(&direction)?;
423 direction = Some(Direction::Ascend);
424 idx += 1;
425 continue;
426 }
427 "descend" => {
428 ensure_unique_direction(&direction)?;
429 direction = Some(Direction::Descend);
430 idx += 1;
431 continue;
432 }
433 "monotonic" => {
434 ensure_unique_direction(&direction)?;
435 direction = Some(Direction::Monotonic);
436 idx += 1;
437 continue;
438 }
439 "strictascend" => {
440 ensure_unique_direction(&direction)?;
441 direction = Some(Direction::StrictAscend);
442 idx += 1;
443 continue;
444 }
445 "strictdescend" => {
446 ensure_unique_direction(&direction)?;
447 direction = Some(Direction::StrictDescend);
448 idx += 1;
449 continue;
450 }
451 "strictmonotonic" => {
452 ensure_unique_direction(&direction)?;
453 direction = Some(Direction::StrictMonotonic);
454 idx += 1;
455 continue;
456 }
457 "comparisonmethod" => {
458 idx += 1;
459 if idx >= args.len() {
460 return Err(issorted_invalid_argument(
461 "issorted: expected a value for 'ComparisonMethod'",
462 ));
463 }
464 let value = value_to_string_lower(&args[idx]).ok_or_else(|| {
465 issorted_invalid_argument(
466 "issorted: 'ComparisonMethod' expects a string value",
467 )
468 })?;
469 comparison = match value.as_str() {
470 "auto" => ComparisonMethod::Auto,
471 "real" => ComparisonMethod::Real,
472 "abs" | "magnitude" => ComparisonMethod::Abs,
473 other => {
474 return Err(issorted_invalid_argument(format!(
475 "issorted: unsupported ComparisonMethod '{other}'"
476 )));
477 }
478 };
479 idx += 1;
480 continue;
481 }
482 "missingplacement" => {
483 idx += 1;
484 if idx >= args.len() {
485 return Err(issorted_invalid_argument(
486 "issorted: expected a value for 'MissingPlacement'",
487 ));
488 }
489 let value = value_to_string_lower(&args[idx]).ok_or_else(|| {
490 issorted_invalid_argument(
491 "issorted: 'MissingPlacement' expects a string value",
492 )
493 })?;
494 missing = match value.as_str() {
495 "auto" => MissingPlacement::Auto,
496 "first" => MissingPlacement::First,
497 "last" => MissingPlacement::Last,
498 other => {
499 return Err(issorted_invalid_argument(format!(
500 "issorted: unsupported MissingPlacement '{other}'"
501 )));
502 }
503 };
504 idx += 1;
505 continue;
506 }
507 _ => {}
508 }
509 }
510
511 if !saw_rows && dim_arg.is_none() {
512 if let Ok(dim) = tensor::parse_dimension(arg, "issorted") {
513 dim_arg = Some(dim);
514 idx += 1;
515 continue;
516 }
517 }
518
519 return Err(issorted_invalid_argument(format!(
520 "issorted: unrecognised argument {:?}",
521 arg
522 )));
523 }
524
525 if let Some(dim) = dim_arg {
526 mode = CheckMode::Dimension(dim);
527 }
528
529 Ok(IssortedArgs {
530 mode,
531 direction: direction.unwrap_or(Direction::Ascend),
532 comparison,
533 missing,
534 })
535 }
536}
537
538fn ensure_unique_direction(direction: &Option<Direction>) -> crate::BuiltinResult<()> {
539 if direction.is_some() {
540 Err(issorted_error(
541 &ISSORTED_ERROR_DUPLICATE_DIRECTION,
542 ISSORTED_ERROR_DUPLICATE_DIRECTION.message,
543 ))
544 } else {
545 Ok(())
546 }
547}
548
549async fn normalize_input(value: Value) -> crate::BuiltinResult<InputArray> {
550 match value {
551 Value::Tensor(tensor) => Ok(InputArray::Real(tensor)),
552 Value::LogicalArray(logical) => {
553 let tensor = tensor::logical_to_tensor(&logical)
554 .map_err(issorted_internal)?;
555 Ok(InputArray::Real(tensor))
556 }
557 Value::Num(_) | Value::Int(_) | Value::Bool(_) => {
558 let tensor = tensor::value_into_tensor_for("issorted", value)
559 .map_err(issorted_internal)?;
560 Ok(InputArray::Real(tensor))
561 }
562 Value::ComplexTensor(ct) => Ok(InputArray::Complex(ct)),
563 Value::Complex(re, im) => {
564 let tensor = ComplexTensor::new(vec![(re, im)], vec![1, 1])
565 .map_err(|e| issorted_internal(format!("issorted: {e}")))?;
566 Ok(InputArray::Complex(tensor))
567 }
568 Value::CharArray(ca) => {
569 let tensor = char_array_to_tensor(&ca)?;
570 Ok(InputArray::Real(tensor))
571 }
572 Value::StringArray(sa) => Ok(InputArray::String(sa)),
573 Value::String(s) => {
574 let array =
575 StringArray::new(vec![s], vec![1, 1])
576 .map_err(|e| issorted_internal(format!("issorted: {e}")))?;
577 Ok(InputArray::String(array))
578 }
579 Value::GpuTensor(handle) => {
580 let tensor = gpu_helpers::gather_tensor_async(&handle).await?;
581 Ok(InputArray::Real(tensor))
582 }
583 other => Err(issorted_invalid_input(format!(
584 "issorted: unsupported input type {:?}; expected numeric, logical, complex, char, or string arrays",
585 other
586 ))),
587 }
588}
589
590fn issorted_real(tensor: &Tensor, args: &IssortedArgs) -> crate::BuiltinResult<bool> {
591 if tensor.data.is_empty() {
592 return Ok(true);
593 }
594 match args.mode {
595 CheckMode::Dimension(dim) => Ok(check_real_dimension(tensor, dim, args)),
596 CheckMode::Rows => check_real_rows(tensor, args),
597 }
598}
599
600fn issorted_complex(tensor: &ComplexTensor, args: &IssortedArgs) -> crate::BuiltinResult<bool> {
601 if tensor.data.is_empty() {
602 return Ok(true);
603 }
604 match args.mode {
605 CheckMode::Dimension(dim) => Ok(check_complex_dimension(tensor, dim, args)),
606 CheckMode::Rows => check_complex_rows(tensor, args),
607 }
608}
609
610fn issorted_string(array: &StringArray, args: &IssortedArgs) -> crate::BuiltinResult<bool> {
611 if array.data.is_empty() {
612 return Ok(true);
613 }
614 if !matches!(args.comparison, ComparisonMethod::Auto) {
615 return Err(issorted_error(
616 &ISSORTED_ERROR_STRING_COMPARISON_UNSUPPORTED,
617 "issorted: 'ComparisonMethod' is not supported for string arrays",
618 ));
619 }
620 match args.mode {
621 CheckMode::Dimension(dim) => Ok(check_string_dimension(array, dim, args)),
622 CheckMode::Rows => check_string_rows(array, args),
623 }
624}
625
626fn check_real_dimension(tensor: &Tensor, dim: usize, args: &IssortedArgs) -> bool {
627 let dim_index = dim.saturating_sub(1);
628 if dim_index >= tensor.shape.len() {
629 return true;
630 }
631 let len_dim = tensor.shape[dim_index];
632 if len_dim <= 1 {
633 return true;
634 }
635
636 let before = product(&tensor.shape[..dim_index]);
637 let after = product(&tensor.shape[dim_index + 1..]);
638 let effective_comp = match args.comparison {
639 ComparisonMethod::Auto => ComparisonMethod::Real,
640 other => other,
641 };
642 let mut slice = Vec::with_capacity(len_dim);
643 for after_idx in 0..after {
644 for before_idx in 0..before {
645 slice.clear();
646 for k in 0..len_dim {
647 let idx = before_idx + k * before + after_idx * before * len_dim;
648 slice.push(tensor.data[idx]);
649 }
650 if !check_real_slice(&slice, args.direction, effective_comp, args.missing) {
651 return false;
652 }
653 }
654 }
655 true
656}
657
658fn check_complex_dimension(tensor: &ComplexTensor, dim: usize, args: &IssortedArgs) -> bool {
659 let dim_index = dim.saturating_sub(1);
660 if dim_index >= tensor.shape.len() {
661 return true;
662 }
663 let len_dim = tensor.shape[dim_index];
664 if len_dim <= 1 {
665 return true;
666 }
667 let before = product(&tensor.shape[..dim_index]);
668 let after = product(&tensor.shape[dim_index + 1..]);
669 let effective_comp = match args.comparison {
670 ComparisonMethod::Auto => ComparisonMethod::Abs,
671 other => other,
672 };
673 let mut slice = Vec::with_capacity(len_dim);
674 for after_idx in 0..after {
675 for before_idx in 0..before {
676 slice.clear();
677 for k in 0..len_dim {
678 let idx = before_idx + k * before + after_idx * before * len_dim;
679 slice.push(tensor.data[idx]);
680 }
681 if !check_complex_slice(&slice, args.direction, effective_comp, args.missing) {
682 return false;
683 }
684 }
685 }
686 true
687}
688
689fn check_string_dimension(array: &StringArray, dim: usize, args: &IssortedArgs) -> bool {
690 let dim_index = dim.saturating_sub(1);
691 if dim_index >= array.shape.len() {
692 return true;
693 }
694 let len_dim = array.shape[dim_index];
695 if len_dim <= 1 {
696 return true;
697 }
698 let before = product(&array.shape[..dim_index]);
699 let after = product(&array.shape[dim_index + 1..]);
700 let mut slice = Vec::with_capacity(len_dim);
701 for after_idx in 0..after {
702 for before_idx in 0..before {
703 slice.clear();
704 for k in 0..len_dim {
705 let idx = before_idx + k * before + after_idx * before * len_dim;
706 slice.push(array.data[idx].as_str());
707 }
708 if !check_string_slice(&slice, args.direction, args.missing) {
709 return false;
710 }
711 }
712 }
713 true
714}
715
716fn check_real_rows(tensor: &Tensor, args: &IssortedArgs) -> crate::BuiltinResult<bool> {
717 if tensor.shape.len() > 2 {
718 return Err(issorted_error(
719 &ISSORTED_ERROR_ROWS_REQUIRES_2D,
720 ISSORTED_ERROR_ROWS_REQUIRES_2D.message,
721 ));
722 }
723 let rows = tensor.rows();
724 let cols = tensor.cols();
725 if rows <= 1 || cols == 0 {
726 return Ok(true);
727 }
728 let effective_comp = match args.comparison {
729 ComparisonMethod::Auto => ComparisonMethod::Real,
730 other => other,
731 };
732 let orders = direction_orders(args.direction);
733 for &order in orders {
734 if real_rows_in_order(tensor, rows, cols, order, effective_comp, args.missing) {
735 return Ok(true);
736 }
737 }
738 Ok(false)
739}
740
741fn check_complex_rows(tensor: &ComplexTensor, args: &IssortedArgs) -> crate::BuiltinResult<bool> {
742 if tensor.shape.len() > 2 {
743 return Err(issorted_error(
744 &ISSORTED_ERROR_ROWS_REQUIRES_2D,
745 ISSORTED_ERROR_ROWS_REQUIRES_2D.message,
746 ));
747 }
748 let rows = tensor.rows;
749 let cols = tensor.cols;
750 if rows <= 1 || cols == 0 {
751 return Ok(true);
752 }
753 let effective_comp = match args.comparison {
754 ComparisonMethod::Auto => ComparisonMethod::Abs,
755 other => other,
756 };
757 let orders = direction_orders(args.direction);
758 for &order in orders {
759 if complex_rows_in_order(tensor, rows, cols, order, effective_comp, args.missing) {
760 return Ok(true);
761 }
762 }
763 Ok(false)
764}
765
766fn check_string_rows(array: &StringArray, args: &IssortedArgs) -> crate::BuiltinResult<bool> {
767 if array.shape.len() > 2 {
768 return Err(issorted_error(
769 &ISSORTED_ERROR_ROWS_REQUIRES_2D,
770 ISSORTED_ERROR_ROWS_REQUIRES_2D.message,
771 ));
772 }
773 let rows = array.rows;
774 let cols = array.cols;
775 if rows <= 1 || cols == 0 {
776 return Ok(true);
777 }
778 let orders = direction_orders(args.direction);
779 for &order in orders {
780 if string_rows_in_order(array, rows, cols, order, args.missing) {
781 return Ok(true);
782 }
783 }
784 Ok(false)
785}
786
787fn real_rows_in_order(
788 tensor: &Tensor,
789 rows: usize,
790 cols: usize,
791 order: OrderSpec,
792 comparison: ComparisonMethod,
793 missing: MissingPlacement,
794) -> bool {
795 if order.strict && tensor.data.iter().any(|v| v.is_nan()) {
796 return false;
797 }
798 let missing_resolved = missing.resolve(order.direction);
799 for row in 0..rows - 1 {
800 let ord = compare_real_row_pair(
801 tensor,
802 rows,
803 cols,
804 row,
805 row + 1,
806 order.direction,
807 comparison,
808 missing_resolved,
809 );
810 if !order_satisfied(ord, order) {
811 return false;
812 }
813 }
814 true
815}
816
817fn complex_rows_in_order(
818 tensor: &ComplexTensor,
819 rows: usize,
820 cols: usize,
821 order: OrderSpec,
822 comparison: ComparisonMethod,
823 missing: MissingPlacement,
824) -> bool {
825 if order.strict && tensor.data.iter().any(|v| complex_is_nan(*v)) {
826 return false;
827 }
828 let missing_resolved = missing.resolve(order.direction);
829 for row in 0..rows - 1 {
830 let ord = compare_complex_row_pair(
831 tensor,
832 rows,
833 cols,
834 row,
835 row + 1,
836 order.direction,
837 comparison,
838 missing_resolved,
839 );
840 if !order_satisfied(ord, order) {
841 return false;
842 }
843 }
844 true
845}
846
847fn string_rows_in_order(
848 array: &StringArray,
849 rows: usize,
850 cols: usize,
851 order: OrderSpec,
852 missing: MissingPlacement,
853) -> bool {
854 if order.strict && array.data.iter().any(|s| is_string_missing(s)) {
855 return false;
856 }
857 let missing_resolved = missing.resolve(order.direction);
858 for row in 0..rows - 1 {
859 let ord = compare_string_row_pair(
860 array,
861 rows,
862 cols,
863 row,
864 row + 1,
865 order.direction,
866 missing_resolved,
867 );
868 if !order_satisfied(ord, order) {
869 return false;
870 }
871 }
872 true
873}
874
875#[allow(clippy::too_many_arguments)]
876fn compare_real_row_pair(
877 tensor: &Tensor,
878 rows: usize,
879 cols: usize,
880 a: usize,
881 b: usize,
882 direction: SortDirection,
883 comparison: ComparisonMethod,
884 missing: MissingPlacementResolved,
885) -> Ordering {
886 for col in 0..cols {
887 let idx_a = a + col * rows;
888 let idx_b = b + col * rows;
889 let ord = compare_real_scalars(
890 tensor.data[idx_a],
891 tensor.data[idx_b],
892 direction,
893 comparison,
894 missing,
895 );
896 if ord != Ordering::Equal {
897 return ord;
898 }
899 }
900 Ordering::Equal
901}
902
903#[allow(clippy::too_many_arguments)]
904fn compare_complex_row_pair(
905 tensor: &ComplexTensor,
906 rows: usize,
907 cols: usize,
908 a: usize,
909 b: usize,
910 direction: SortDirection,
911 comparison: ComparisonMethod,
912 missing: MissingPlacementResolved,
913) -> Ordering {
914 for col in 0..cols {
915 let idx_a = a + col * rows;
916 let idx_b = b + col * rows;
917 let ord = compare_complex_scalars(
918 tensor.data[idx_a],
919 tensor.data[idx_b],
920 direction,
921 comparison,
922 missing,
923 );
924 if ord != Ordering::Equal {
925 return ord;
926 }
927 }
928 Ordering::Equal
929}
930
931fn compare_string_row_pair(
932 array: &StringArray,
933 rows: usize,
934 cols: usize,
935 a: usize,
936 b: usize,
937 direction: SortDirection,
938 missing: MissingPlacementResolved,
939) -> Ordering {
940 for col in 0..cols {
941 let idx_a = a + col * rows;
942 let idx_b = b + col * rows;
943 let ord = compare_string_scalars(
944 array.data[idx_a].as_str(),
945 array.data[idx_b].as_str(),
946 direction,
947 missing,
948 );
949 if ord != Ordering::Equal {
950 return ord;
951 }
952 }
953 Ordering::Equal
954}
955
956fn order_satisfied(ord: Ordering, order: OrderSpec) -> bool {
957 match order.direction {
958 SortDirection::Ascend => match ord {
959 Ordering::Greater => false,
960 Ordering::Equal => !order.strict,
961 Ordering::Less => true,
962 },
963 SortDirection::Descend => match ord {
964 Ordering::Less => true,
965 Ordering::Equal => !order.strict,
966 Ordering::Greater => false,
967 },
968 }
969}
970
971fn check_real_slice(
972 slice: &[f64],
973 direction: Direction,
974 comparison: ComparisonMethod,
975 missing: MissingPlacement,
976) -> bool {
977 if slice.len() <= 1 {
978 return true;
979 }
980 let orders = direction_orders(direction);
981 for &order in orders {
982 if order.strict && slice.iter().any(|v| v.is_nan()) {
983 continue;
984 }
985 let missing_resolved = missing.resolve(order.direction);
986 if real_slice_in_order(slice, order, comparison, missing_resolved) {
987 return true;
988 }
989 }
990 false
991}
992
993fn check_complex_slice(
994 slice: &[(f64, f64)],
995 direction: Direction,
996 comparison: ComparisonMethod,
997 missing: MissingPlacement,
998) -> bool {
999 if slice.len() <= 1 {
1000 return true;
1001 }
1002 let orders = direction_orders(direction);
1003 for &order in orders {
1004 if order.strict && slice.iter().any(|v| complex_is_nan(*v)) {
1005 continue;
1006 }
1007 let missing_resolved = missing.resolve(order.direction);
1008 if complex_slice_in_order(slice, order, comparison, missing_resolved) {
1009 return true;
1010 }
1011 }
1012 false
1013}
1014
1015fn check_string_slice(slice: &[&str], direction: Direction, missing: MissingPlacement) -> bool {
1016 if slice.len() <= 1 {
1017 return true;
1018 }
1019 let orders = direction_orders(direction);
1020 for &order in orders {
1021 if order.strict && slice.iter().any(|s| is_string_missing(s)) {
1022 continue;
1023 }
1024 let missing_resolved = missing.resolve(order.direction);
1025 if string_slice_in_order(slice, order, missing_resolved) {
1026 return true;
1027 }
1028 }
1029 false
1030}
1031
1032fn real_slice_in_order(
1033 slice: &[f64],
1034 order: OrderSpec,
1035 comparison: ComparisonMethod,
1036 missing: MissingPlacementResolved,
1037) -> bool {
1038 for pair in slice.windows(2) {
1039 let ord = compare_real_scalars(pair[0], pair[1], order.direction, comparison, missing);
1040 if !order_satisfied(ord, order) {
1041 return false;
1042 }
1043 }
1044 true
1045}
1046
1047fn complex_slice_in_order(
1048 slice: &[(f64, f64)],
1049 order: OrderSpec,
1050 comparison: ComparisonMethod,
1051 missing: MissingPlacementResolved,
1052) -> bool {
1053 for pair in slice.windows(2) {
1054 let ord = compare_complex_scalars(pair[0], pair[1], order.direction, comparison, missing);
1055 if !order_satisfied(ord, order) {
1056 return false;
1057 }
1058 }
1059 true
1060}
1061
1062fn string_slice_in_order(
1063 slice: &[&str],
1064 order: OrderSpec,
1065 missing: MissingPlacementResolved,
1066) -> bool {
1067 for pair in slice.windows(2) {
1068 let ord = compare_string_scalars(pair[0], pair[1], order.direction, missing);
1069 if !order_satisfied(ord, order) {
1070 return false;
1071 }
1072 }
1073 true
1074}
1075
1076fn compare_real_scalars(
1077 a: f64,
1078 b: f64,
1079 direction: SortDirection,
1080 comparison: ComparisonMethod,
1081 missing: MissingPlacementResolved,
1082) -> Ordering {
1083 match (a.is_nan(), b.is_nan()) {
1084 (true, true) => Ordering::Equal,
1085 (true, false) => match missing {
1086 MissingPlacementResolved::First => Ordering::Less,
1087 MissingPlacementResolved::Last => Ordering::Greater,
1088 },
1089 (false, true) => match missing {
1090 MissingPlacementResolved::First => Ordering::Greater,
1091 MissingPlacementResolved::Last => Ordering::Less,
1092 },
1093 (false, false) => compare_real_finite_scalars(a, b, direction, comparison),
1094 }
1095}
1096
1097fn compare_real_finite_scalars(
1098 a: f64,
1099 b: f64,
1100 direction: SortDirection,
1101 comparison: ComparisonMethod,
1102) -> Ordering {
1103 if matches!(comparison, ComparisonMethod::Abs) {
1104 let abs_cmp = a.abs().partial_cmp(&b.abs()).unwrap_or(Ordering::Equal);
1105 if abs_cmp != Ordering::Equal {
1106 return match direction {
1107 SortDirection::Ascend => abs_cmp,
1108 SortDirection::Descend => abs_cmp.reverse(),
1109 };
1110 }
1111 }
1112 match direction {
1113 SortDirection::Ascend => a.partial_cmp(&b).unwrap_or(Ordering::Equal),
1114 SortDirection::Descend => b.partial_cmp(&a).unwrap_or(Ordering::Equal),
1115 }
1116}
1117
1118fn compare_complex_scalars(
1119 a: (f64, f64),
1120 b: (f64, f64),
1121 direction: SortDirection,
1122 comparison: ComparisonMethod,
1123 missing: MissingPlacementResolved,
1124) -> Ordering {
1125 match (complex_is_nan(a), complex_is_nan(b)) {
1126 (true, true) => Ordering::Equal,
1127 (true, false) => match missing {
1128 MissingPlacementResolved::First => Ordering::Less,
1129 MissingPlacementResolved::Last => Ordering::Greater,
1130 },
1131 (false, true) => match missing {
1132 MissingPlacementResolved::First => Ordering::Greater,
1133 MissingPlacementResolved::Last => Ordering::Less,
1134 },
1135 (false, false) => compare_complex_finite_scalars(a, b, direction, comparison),
1136 }
1137}
1138
1139fn compare_complex_finite_scalars(
1140 a: (f64, f64),
1141 b: (f64, f64),
1142 direction: SortDirection,
1143 comparison: ComparisonMethod,
1144) -> Ordering {
1145 match comparison {
1146 ComparisonMethod::Real => compare_complex_real_first(a, b, direction),
1147 ComparisonMethod::Abs | ComparisonMethod::Auto => {
1148 let abs_cmp = complex_abs(a)
1149 .partial_cmp(&complex_abs(b))
1150 .unwrap_or(Ordering::Equal);
1151 if abs_cmp != Ordering::Equal {
1152 return match direction {
1153 SortDirection::Ascend => abs_cmp,
1154 SortDirection::Descend => abs_cmp.reverse(),
1155 };
1156 }
1157 compare_complex_real_first(a, b, direction)
1158 }
1159 }
1160}
1161
1162fn compare_complex_real_first(a: (f64, f64), b: (f64, f64), direction: SortDirection) -> Ordering {
1163 let real_cmp = match direction {
1164 SortDirection::Ascend => a.0.partial_cmp(&b.0),
1165 SortDirection::Descend => b.0.partial_cmp(&a.0),
1166 }
1167 .unwrap_or(Ordering::Equal);
1168 if real_cmp != Ordering::Equal {
1169 return real_cmp;
1170 }
1171 match direction {
1172 SortDirection::Ascend => a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal),
1173 SortDirection::Descend => b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal),
1174 }
1175}
1176
1177fn compare_string_scalars(
1178 a: &str,
1179 b: &str,
1180 direction: SortDirection,
1181 missing: MissingPlacementResolved,
1182) -> Ordering {
1183 let missing_a = is_string_missing(a);
1184 let missing_b = is_string_missing(b);
1185 match (missing_a, missing_b) {
1186 (true, true) => Ordering::Equal,
1187 (true, false) => match missing {
1188 MissingPlacementResolved::First => Ordering::Less,
1189 MissingPlacementResolved::Last => Ordering::Greater,
1190 },
1191 (false, true) => match missing {
1192 MissingPlacementResolved::First => Ordering::Greater,
1193 MissingPlacementResolved::Last => Ordering::Less,
1194 },
1195 (false, false) => match direction {
1196 SortDirection::Ascend => a.cmp(b),
1197 SortDirection::Descend => b.cmp(a),
1198 },
1199 }
1200}
1201
1202fn complex_is_nan(value: (f64, f64)) -> bool {
1203 value.0.is_nan() || value.1.is_nan()
1204}
1205
1206fn complex_abs(value: (f64, f64)) -> f64 {
1207 value.0.hypot(value.1)
1208}
1209
1210fn is_string_missing(value: &str) -> bool {
1211 value.eq_ignore_ascii_case("<missing>")
1212}
1213
1214fn direction_orders(direction: Direction) -> &'static [OrderSpec] {
1215 match direction {
1216 Direction::Ascend => &[OrderSpec {
1217 direction: SortDirection::Ascend,
1218 strict: false,
1219 }],
1220 Direction::Descend => &[OrderSpec {
1221 direction: SortDirection::Descend,
1222 strict: false,
1223 }],
1224 Direction::Monotonic => &[
1225 OrderSpec {
1226 direction: SortDirection::Ascend,
1227 strict: false,
1228 },
1229 OrderSpec {
1230 direction: SortDirection::Descend,
1231 strict: false,
1232 },
1233 ],
1234 Direction::StrictAscend => &[OrderSpec {
1235 direction: SortDirection::Ascend,
1236 strict: true,
1237 }],
1238 Direction::StrictDescend => &[OrderSpec {
1239 direction: SortDirection::Descend,
1240 strict: true,
1241 }],
1242 Direction::StrictMonotonic => &[
1243 OrderSpec {
1244 direction: SortDirection::Ascend,
1245 strict: true,
1246 },
1247 OrderSpec {
1248 direction: SortDirection::Descend,
1249 strict: true,
1250 },
1251 ],
1252 }
1253}
1254
1255fn default_dimension(shape: &[usize]) -> usize {
1256 if shape.is_empty() {
1257 return 1;
1258 }
1259 shape
1260 .iter()
1261 .position(|&extent| extent > 1)
1262 .map(|idx| idx + 1)
1263 .unwrap_or(1)
1264}
1265
1266fn product(slice: &[usize]) -> usize {
1267 slice
1268 .iter()
1269 .copied()
1270 .fold(1usize, |acc, value| acc.saturating_mul(value.max(1)))
1271}
1272
1273fn value_to_string_lower(value: &Value) -> Option<String> {
1274 match String::try_from(value) {
1275 Ok(text) => Some(text.trim().to_ascii_lowercase()),
1276 Err(_) => None,
1277 }
1278}
1279
1280fn char_array_to_tensor(array: &CharArray) -> crate::BuiltinResult<Tensor> {
1281 let rows = array.rows;
1282 let cols = array.cols;
1283 let mut data = vec![0.0f64; rows * cols];
1284 for r in 0..rows {
1285 for c in 0..cols {
1286 let ch = array.data[r * cols + c];
1287 let idx = r + c * rows;
1288 data[idx] = ch as u32 as f64;
1289 }
1290 }
1291 Tensor::new(data, vec![rows, cols]).map_err(|e| issorted_internal(format!("issorted: {e}")))
1292}
1293
1294#[cfg(test)]
1295pub(crate) mod tests {
1296 use super::*;
1297 use crate::builtins::common::test_support;
1298 use futures::executor::block_on;
1299 use runmat_builtins::{IntValue, LogicalArray, ResolveContext, Type, Value};
1300
1301 fn issorted_builtin(value: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
1302 block_on(super::issorted_builtin(value, rest))
1303 }
1304
1305 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1306 #[test]
1307 fn issorted_numeric_vector_true() {
1308 let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
1309 let result = issorted_builtin(Value::Tensor(tensor), vec![]).expect("issorted");
1310 assert_eq!(result, Value::Bool(true));
1311 }
1312
1313 #[test]
1314 fn issorted_type_resolver_bool() {
1315 assert_eq!(
1316 bool_output_type(&[Type::tensor()], &ResolveContext::new(Vec::new())),
1317 Type::Bool
1318 );
1319 }
1320
1321 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1322 #[test]
1323 fn issorted_numeric_vector_false() {
1324 let tensor = Tensor::new(vec![3.0, 2.0, 1.0], vec![3, 1]).unwrap();
1325 let result = issorted_builtin(Value::Tensor(tensor), vec![]).expect("issorted");
1326 assert_eq!(result, Value::Bool(false));
1327 }
1328
1329 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1330 #[test]
1331 fn issorted_logical_vector() {
1332 let logical = LogicalArray::new(vec![0, 1, 1], vec![3, 1]).unwrap();
1333 let result =
1334 issorted_builtin(Value::LogicalArray(logical), vec![]).expect("issorted logical");
1335 assert_eq!(result, Value::Bool(true));
1336 }
1337
1338 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1339 #[test]
1340 fn issorted_dimension_argument() {
1341 let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 3.0], vec![2, 2]).unwrap();
1342 let args = vec![Value::Int(IntValue::I32(2))];
1343 let result = issorted_builtin(Value::Tensor(tensor), args).expect("issorted");
1344 assert_eq!(result, Value::Bool(true));
1345 }
1346
1347 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1348 #[test]
1349 fn issorted_strictascend_rejects_duplicates() {
1350 let tensor = Tensor::new(vec![1.0, 1.0, 2.0], vec![3, 1]).unwrap();
1351 let args = vec![Value::from("strictascend")];
1352 let result = issorted_builtin(Value::Tensor(tensor), args).expect("issorted");
1353 assert_eq!(result, Value::Bool(false));
1354 }
1355
1356 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1357 #[test]
1358 fn issorted_strictmonotonic_true_with_descend() {
1359 let tensor = Tensor::new(vec![9.0, 4.0, 1.0], vec![3, 1]).unwrap();
1360 let args = vec![Value::from("strictmonotonic")];
1361 let result = issorted_builtin(Value::Tensor(tensor), args).expect("issorted");
1362 assert_eq!(result, Value::Bool(true));
1363 }
1364
1365 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1366 #[test]
1367 fn issorted_strictmonotonic_rejects_plateaus() {
1368 let tensor = Tensor::new(vec![4.0, 4.0, 2.0, 1.0], vec![4, 1]).unwrap();
1369 let args = vec![Value::from("strictmonotonic")];
1370 let result = issorted_builtin(Value::Tensor(tensor), args).expect("issorted");
1371 assert_eq!(result, Value::Bool(false));
1372 }
1373
1374 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1375 #[test]
1376 fn issorted_monotonic_accepts_descending() {
1377 let tensor = Tensor::new(vec![5.0, 4.0, 4.0, 1.0], vec![4, 1]).unwrap();
1378 let args = vec![Value::from("monotonic")];
1379 let result = issorted_builtin(Value::Tensor(tensor), args).expect("issorted");
1380 assert_eq!(result, Value::Bool(true));
1381 }
1382
1383 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1384 #[test]
1385 fn issorted_monotonic_rejects_unsorted_data() {
1386 let tensor = Tensor::new(vec![1.0, 3.0, 2.0], vec![3, 1]).unwrap();
1387 let args = vec![Value::from("monotonic")];
1388 let result = issorted_builtin(Value::Tensor(tensor), args).expect("issorted");
1389 assert_eq!(result, Value::Bool(false));
1390 }
1391
1392 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1393 #[test]
1394 fn issorted_missingplacement_first() {
1395 let tensor = Tensor::new(vec![f64::NAN, 2.0, 3.0], vec![3, 1]).unwrap();
1396 let args = vec![Value::from("MissingPlacement"), Value::from("first")];
1397 let result = issorted_builtin(Value::Tensor(tensor), args).expect("issorted");
1398 assert_eq!(result, Value::Bool(true));
1399 }
1400
1401 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1402 #[test]
1403 fn issorted_missingplacement_first_violation() {
1404 let tensor = Tensor::new(vec![2.0, f64::NAN, 3.0], vec![3, 1]).unwrap();
1405 let args = vec![Value::from("MissingPlacement"), Value::from("first")];
1406 let result = issorted_builtin(Value::Tensor(tensor), args).expect("issorted");
1407 assert_eq!(result, Value::Bool(false));
1408 }
1409
1410 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1411 #[test]
1412 fn issorted_missingplacement_auto_descend_prefers_front() {
1413 let tensor = Tensor::new(vec![f64::NAN, 5.0, 3.0], vec![3, 1]).unwrap();
1414 let args = vec![Value::from("descend")];
1415 let result = issorted_builtin(Value::Tensor(tensor), args).expect("issorted");
1416 assert_eq!(result, Value::Bool(true));
1417 }
1418
1419 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1420 #[test]
1421 fn issorted_comparison_abs() {
1422 let tensor = Tensor::new(vec![-1.0, 1.5, -2.0], vec![3, 1]).unwrap();
1423 let args = vec![Value::from("ComparisonMethod"), Value::from("abs")];
1424 let result = issorted_builtin(Value::Tensor(tensor), args).expect("issorted");
1425 assert_eq!(result, Value::Bool(true));
1426 }
1427
1428 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1429 #[test]
1430 fn issorted_complex_abs_method() {
1431 let tensor =
1432 ComplexTensor::new(vec![(1.0, 1.0), (2.0, 0.0), (2.0, 3.0)], vec![3, 1]).unwrap();
1433 let args = vec![Value::from("ComparisonMethod"), Value::from("abs")];
1434 let result = issorted_builtin(Value::ComplexTensor(tensor), args).expect("issorted");
1435 assert_eq!(result, Value::Bool(true));
1436 }
1437
1438 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1439 #[test]
1440 fn issorted_complex_real_method() {
1441 let tensor =
1442 ComplexTensor::new(vec![(1.0, 1.0), (1.0, 1.0), (2.0, 0.0)], vec![3, 1]).unwrap();
1443 let args = vec![
1444 Value::from("ComparisonMethod"),
1445 Value::from("real"),
1446 Value::from("strictascend"),
1447 ];
1448 let result = issorted_builtin(Value::ComplexTensor(tensor), args).expect("issorted");
1449 assert_eq!(result, Value::Bool(false));
1450 }
1451
1452 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1453 #[test]
1454 fn issorted_rows_true() {
1455 let tensor = Tensor::new(vec![1.0, 2.0, 1.0, 3.0], vec![2, 2]).unwrap();
1456 let args = vec![Value::from("rows")];
1457 let result = issorted_builtin(Value::Tensor(tensor), args).expect("issorted");
1458 assert_eq!(result, Value::Bool(true));
1459 }
1460
1461 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1462 #[test]
1463 fn issorted_rows_dimension_error() {
1464 let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2, 1]).unwrap();
1465 let err = issorted_builtin(Value::Tensor(tensor), vec![Value::from("rows")]).unwrap_err();
1466 assert_eq!(err.identifier(), ISSORTED_ERROR_ROWS_REQUIRES_2D.identifier);
1467 }
1468
1469 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1470 #[test]
1471 fn issorted_rows_descend_false() {
1472 let tensor = Tensor::new(vec![1.0, 2.0, 4.0, 0.0], vec![2, 2]).unwrap();
1473 let args = vec![Value::from("rows"), Value::from("descend")];
1474 let result = issorted_builtin(Value::Tensor(tensor), args).expect("issorted");
1475 assert_eq!(result, Value::Bool(false));
1476 }
1477
1478 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1479 #[test]
1480 fn issorted_string_dimension() {
1481 let array = StringArray::new(
1482 vec![
1483 "pear".into(),
1484 "plum".into(),
1485 "apple".into(),
1486 "banana".into(),
1487 ],
1488 vec![2, 2],
1489 )
1490 .unwrap();
1491 let args = vec![Value::Int(IntValue::I32(2))];
1492 let result =
1493 issorted_builtin(Value::StringArray(array), args).expect("issorted string dim");
1494 assert_eq!(result, Value::Bool(false));
1495 }
1496
1497 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1498 #[test]
1499 fn issorted_string_missingplacement_last() {
1500 let array = StringArray::new(
1501 vec!["apple".into(), "banana".into(), "<missing>".into()],
1502 vec![3, 1],
1503 )
1504 .unwrap();
1505 let args = vec![Value::from("MissingPlacement"), Value::from("last")];
1506 let result =
1507 issorted_builtin(Value::StringArray(array), args).expect("issorted string placement");
1508 assert_eq!(result, Value::Bool(true));
1509 }
1510
1511 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1512 #[test]
1513 fn issorted_string_missingplacement_last_violation() {
1514 let array = StringArray::new(vec!["<missing>".into(), "apple".into()], vec![2, 1]).unwrap();
1515 let args = vec![Value::from("MissingPlacement"), Value::from("last")];
1516 let result =
1517 issorted_builtin(Value::StringArray(array), args).expect("issorted string placement");
1518 assert_eq!(result, Value::Bool(false));
1519 }
1520
1521 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1522 #[test]
1523 fn issorted_string_comparison_method_error() {
1524 let array = StringArray::new(vec!["apple".into(), "berry".into()], vec![2, 1]).unwrap();
1525 let args = vec![Value::from("ComparisonMethod"), Value::from("real")];
1526 let err = issorted_builtin(Value::StringArray(array), args).unwrap_err();
1527 assert_eq!(
1528 err.identifier(),
1529 ISSORTED_ERROR_STRING_COMPARISON_UNSUPPORTED.identifier
1530 );
1531 }
1532
1533 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1534 #[test]
1535 fn issorted_char_array_input() {
1536 let chars = CharArray::new(vec!['a', 'c', 'e'], 1, 3).unwrap();
1537 let result = issorted_builtin(Value::CharArray(chars), vec![]).expect("issorted char");
1538 assert_eq!(result, Value::Bool(true));
1539 }
1540
1541 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1542 #[test]
1543 fn issorted_duplicate_direction_error() {
1544 let tensor = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
1545 let args = vec![Value::from("ascend"), Value::from("descend")];
1546 let err = issorted_builtin(Value::Tensor(tensor), args).unwrap_err();
1547 assert_eq!(
1548 err.identifier(),
1549 ISSORTED_ERROR_DUPLICATE_DIRECTION.identifier
1550 );
1551 }
1552
1553 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1554 #[test]
1555 fn issorted_gpu_roundtrip() {
1556 test_support::with_test_provider(|provider| {
1557 let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
1558 let view = runmat_accelerate_api::HostTensorView {
1559 data: &tensor.data,
1560 shape: &tensor.shape,
1561 };
1562 let handle = provider.upload(&view).expect("upload");
1563 let result = issorted_builtin(Value::GpuTensor(handle), vec![]).expect("issorted gpu");
1564 assert_eq!(result, Value::Bool(true));
1565 });
1566 }
1567
1568 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1569 #[test]
1570 #[cfg(feature = "wgpu")]
1571 fn issorted_wgpu_matches_cpu() {
1572 let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
1573 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
1574 );
1575 let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
1576 let cpu = issorted_builtin(Value::Tensor(tensor.clone()), vec![]).expect("cpu issorted");
1577 let view = runmat_accelerate_api::HostTensorView {
1578 data: &tensor.data,
1579 shape: &tensor.shape,
1580 };
1581 let handle = runmat_accelerate_api::provider()
1582 .expect("wgpu provider")
1583 .upload(&view)
1584 .expect("upload");
1585 let gpu = issorted_builtin(Value::GpuTensor(handle), vec![]).expect("gpu issorted");
1586 assert_eq!(gpu, cpu);
1587 }
1588}