Skip to main content

runmat_runtime/builtins/array/sorting_sets/
issorted.rs

1//! MATLAB-compatible `issorted` builtin with GPU-aware semantics.
2
3use 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}