Skip to main content

runmat_runtime/builtins/array/sorting_sets/
unique.rs

1//! MATLAB-compatible `unique` builtin with GPU-aware semantics for RunMat.
2//!
3//! The implementation mirrors MathWorks MATLAB behavioural details for sorted
4//! and stable orderings, row-wise uniqueness, and index outputs. GPU tensors
5//! are gathered to host memory today, but the builtin is registered as a sink
6//! so future providers can add device-side kernels without impacting callers.
7
8use std::cmp::Ordering;
9use std::collections::HashMap;
10
11use runmat_accelerate_api::{
12    GpuTensorHandle, GpuTensorStorage, HostTensorOwned, UniqueOccurrence, UniqueOptions,
13    UniqueOrder, UniqueResult,
14};
15use runmat_builtins::{CharArray, ComplexTensor, StringArray, Tensor, Value};
16use runmat_macros::runtime_builtin;
17
18use super::type_resolvers::set_values_output_type;
19use crate::build_runtime_error;
20use crate::builtins::common::arg_tokens::tokens_from_values;
21use crate::builtins::common::gpu_helpers;
22use crate::builtins::common::random_args::complex_tensor_into_value;
23use crate::builtins::common::spec::{
24    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
25    ProviderHook, ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
26};
27use crate::builtins::common::tensor;
28
29#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::array::sorting_sets::unique")]
30pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
31    name: "unique",
32    op_kind: GpuOpKind::Custom("unique"),
33    supported_precisions: &[ScalarType::F32, ScalarType::F64],
34    broadcast: BroadcastSemantics::None,
35    provider_hooks: &[ProviderHook::Custom("unique")],
36    constant_strategy: ConstantStrategy::InlineLiteral,
37    residency: ResidencyPolicy::GatherImmediately,
38    nan_mode: ReductionNaN::Include,
39    two_pass_threshold: None,
40    workgroup_size: None,
41    accepts_nan_mode: true,
42    notes: "Providers may implement the `unique` hook; default providers download tensors and reuse the CPU implementation.",
43};
44
45#[runmat_macros::register_fusion_spec(
46    builtin_path = "crate::builtins::array::sorting_sets::unique"
47)]
48pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
49    name: "unique",
50    shape: ShapeRequirements::Any,
51    constant_strategy: ConstantStrategy::InlineLiteral,
52    elementwise: None,
53    reduction: None,
54    emits_nan: true,
55    notes: "`unique` terminates fusion chains and materialises results on the host; upstream tensors are gathered when necessary.",
56};
57
58fn unique_error(message: impl Into<String>) -> crate::RuntimeError {
59    build_runtime_error(message).with_builtin("unique").build()
60}
61
62#[runtime_builtin(
63    name = "unique",
64    category = "array/sorting_sets",
65    summary = "Return the unique elements or rows of arrays with optional index outputs.",
66    keywords = "unique,set,distinct,stable,rows,indices,gpu",
67    accel = "array_construct",
68    sink = true,
69    type_resolver(set_values_output_type),
70    builtin_path = "crate::builtins::array::sorting_sets::unique"
71)]
72async fn unique_builtin(value: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
73    let eval = evaluate(value, &rest).await?;
74    if let Some(out_count) = crate::output_count::current_output_count() {
75        if out_count == 0 {
76            return Ok(Value::OutputList(Vec::new()));
77        }
78        if out_count == 1 {
79            return Ok(Value::OutputList(vec![eval.into_values_value()]));
80        }
81        if out_count == 2 {
82            let (values, ia) = eval.into_pair();
83            return Ok(Value::OutputList(vec![values, ia]));
84        }
85        let (values, ia, ic) = eval.into_triple();
86        return Ok(crate::output_count::output_list_with_padding(
87            out_count,
88            vec![values, ia, ic],
89        ));
90    }
91    Ok(eval.into_values_value())
92}
93
94/// Evaluate `unique` once and expose all outputs to the caller.
95pub async fn evaluate(value: Value, rest: &[Value]) -> crate::BuiltinResult<UniqueEvaluation> {
96    let opts = parse_options(rest)?;
97    match value {
98        Value::GpuTensor(handle) => unique_gpu(handle, &opts).await,
99        other => unique_host(other, &opts),
100    }
101}
102
103fn parse_options(rest: &[Value]) -> crate::BuiltinResult<UniqueOptions> {
104    let mut opts = UniqueOptions {
105        rows: false,
106        order: UniqueOrder::Sorted,
107        occurrence: UniqueOccurrence::First,
108    };
109    let mut seen_order: Option<UniqueOrder> = None;
110    let mut seen_occurrence: Option<UniqueOccurrence> = None;
111
112    let tokens = tokens_from_values(rest);
113    for (arg, token) in rest.iter().zip(tokens.iter()) {
114        let text = match token {
115            crate::builtins::common::arg_tokens::ArgToken::String(text) => text.as_str(),
116            _ => {
117                let text = tensor::value_to_string(arg)
118                    .ok_or_else(|| unique_error("unique: expected string option arguments"))?;
119                let lowered = text.trim().to_ascii_lowercase();
120                parse_unique_option(&mut opts, &mut seen_order, &mut seen_occurrence, &lowered)?;
121                continue;
122            }
123        };
124        parse_unique_option(&mut opts, &mut seen_order, &mut seen_occurrence, text)?;
125    }
126
127    Ok(opts)
128}
129
130fn parse_unique_option(
131    opts: &mut UniqueOptions,
132    seen_order: &mut Option<UniqueOrder>,
133    seen_occurrence: &mut Option<UniqueOccurrence>,
134    lowered: &str,
135) -> crate::BuiltinResult<()> {
136    match lowered {
137        "sorted" => {
138            if let Some(prev) = seen_order {
139                if *prev != UniqueOrder::Sorted {
140                    return Err(unique_error(
141                        "unique: cannot combine 'sorted' with 'stable'",
142                    ));
143                }
144            }
145            *seen_order = Some(UniqueOrder::Sorted);
146            opts.order = UniqueOrder::Sorted;
147        }
148        "stable" => {
149            if let Some(prev) = seen_order {
150                if *prev != UniqueOrder::Stable {
151                    return Err(unique_error(
152                        "unique: cannot combine 'sorted' with 'stable'",
153                    ));
154                }
155            }
156            *seen_order = Some(UniqueOrder::Stable);
157            opts.order = UniqueOrder::Stable;
158        }
159        "rows" => {
160            opts.rows = true;
161        }
162        "first" => {
163            if let Some(prev) = seen_occurrence {
164                if *prev != UniqueOccurrence::First {
165                    return Err(unique_error("unique: cannot combine 'first' with 'last'"));
166                }
167            }
168            *seen_occurrence = Some(UniqueOccurrence::First);
169            opts.occurrence = UniqueOccurrence::First;
170        }
171        "last" => {
172            if let Some(prev) = seen_occurrence {
173                if *prev != UniqueOccurrence::Last {
174                    return Err(unique_error("unique: cannot combine 'first' with 'last'"));
175                }
176            }
177            *seen_occurrence = Some(UniqueOccurrence::Last);
178            opts.occurrence = UniqueOccurrence::Last;
179        }
180        "legacy" | "r2012a" => {
181            return Err(unique_error(
182                "unique: the 'legacy' behaviour is not supported",
183            ));
184        }
185        other => {
186            return Err(unique_error(format!(
187                "unique: unrecognised option '{other}'"
188            )));
189        }
190    }
191    Ok(())
192}
193
194async fn unique_gpu(
195    handle: GpuTensorHandle,
196    opts: &UniqueOptions,
197) -> crate::BuiltinResult<UniqueEvaluation> {
198    if let Some(provider) = runmat_accelerate_api::provider() {
199        if let Ok(result) = provider.unique(&handle, opts).await {
200            return UniqueEvaluation::from_unique_result(result);
201        }
202    }
203    let tensor = gpu_helpers::gather_tensor_async(&handle).await?;
204    unique_numeric_from_tensor(tensor, opts)
205}
206
207fn unique_host(value: Value, opts: &UniqueOptions) -> crate::BuiltinResult<UniqueEvaluation> {
208    match value {
209        Value::Tensor(tensor) => unique_numeric_from_tensor(tensor, opts),
210        Value::Num(n) => {
211            let tensor = Tensor::new(vec![n], vec![1, 1]).map_err(|e| unique_error(format!("unique: {e}")))?;
212            unique_numeric_from_tensor(tensor, opts)
213        }
214        Value::Int(i) => {
215            let tensor = Tensor::new(vec![i.to_f64()], vec![1, 1])
216                .map_err(|e| unique_error(format!("unique: {e}")))?;
217            unique_numeric_from_tensor(tensor, opts)
218        }
219        Value::Bool(b) => {
220            let tensor = Tensor::new(vec![if b { 1.0 } else { 0.0 }], vec![1, 1])
221                .map_err(|e| unique_error(format!("unique: {e}")))?;
222            unique_numeric_from_tensor(tensor, opts)
223        }
224        Value::LogicalArray(logical) => {
225            let tensor = tensor::logical_to_tensor(&logical)
226                .map_err(|e| unique_error(e))?;
227            unique_numeric_from_tensor(tensor, opts)
228        }
229        Value::ComplexTensor(tensor) => unique_complex_from_tensor(tensor, opts),
230        Value::Complex(re, im) => {
231            let tensor = ComplexTensor::new(vec![(re, im)], vec![1, 1])
232                .map_err(|e| unique_error(format!("unique: {e}")))?;
233            unique_complex_from_tensor(tensor, opts)
234        }
235        Value::CharArray(array) => unique_char_array(array, opts),
236        Value::StringArray(array) => unique_string_array(array, opts),
237        Value::String(s) => {
238            let array = StringArray::new(vec![s], vec![1, 1]).map_err(|e| unique_error(format!("unique: {e}")))?;
239            unique_string_array(array, opts)
240        }
241        other => Err(unique_error(format!(
242            "unique: unsupported input type {:?}; expected numeric, logical, char, string, or complex values",
243            other
244        ))
245        .into()),
246    }
247}
248
249pub fn unique_numeric_from_tensor(
250    tensor: Tensor,
251    opts: &UniqueOptions,
252) -> crate::BuiltinResult<UniqueEvaluation> {
253    if opts.rows {
254        unique_numeric_rows(tensor, opts)
255    } else {
256        unique_numeric_elements(tensor, opts)
257    }
258}
259
260fn unique_numeric_elements(
261    tensor: Tensor,
262    opts: &UniqueOptions,
263) -> crate::BuiltinResult<UniqueEvaluation> {
264    let len = tensor.data.len();
265    if len == 0 {
266        let values = Tensor::new(Vec::new(), vec![0, 1])
267            .map_err(|e| unique_error(format!("unique: {e}")))?;
268        let ia = Tensor::new(Vec::new(), vec![0, 1])
269            .map_err(|e| unique_error(format!("unique: {e}")))?;
270        let ic = Tensor::new(Vec::new(), vec![0, 1])
271            .map_err(|e| unique_error(format!("unique: {e}")))?;
272        return Ok(UniqueEvaluation::new(
273            tensor::tensor_into_value(values),
274            ia,
275            ic,
276        ));
277    }
278
279    let mut entries = Vec::<NumericElementEntry>::new();
280    let mut map: HashMap<u64, usize> = HashMap::new();
281    let mut element_entry_index = Vec::with_capacity(len);
282
283    for (idx, &value) in tensor.data.iter().enumerate() {
284        let key = canonicalize_f64(value);
285        match map.get(&key) {
286            Some(&entry_idx) => {
287                entries[entry_idx].last = idx;
288                element_entry_index.push(entry_idx);
289            }
290            None => {
291                let entry_idx = entries.len();
292                entries.push(NumericElementEntry {
293                    value,
294                    first: idx,
295                    last: idx,
296                });
297                map.insert(key, entry_idx);
298                element_entry_index.push(entry_idx);
299            }
300        }
301    }
302
303    let mut order: Vec<usize> = (0..entries.len()).collect();
304    if opts.order == UniqueOrder::Sorted {
305        order.sort_by(|&a, &b| compare_f64(entries[a].value, entries[b].value));
306    }
307
308    let mut entry_to_position = vec![0usize; entries.len()];
309    for (pos, &entry_idx) in order.iter().enumerate() {
310        entry_to_position[entry_idx] = pos;
311    }
312
313    let mut values = Vec::with_capacity(order.len());
314    let mut ia = Vec::with_capacity(order.len());
315    for &entry_idx in &order {
316        let entry = &entries[entry_idx];
317        values.push(entry.value);
318        let occurrence = match opts.occurrence {
319            UniqueOccurrence::First => entry.first,
320            UniqueOccurrence::Last => entry.last,
321        };
322        ia.push((occurrence + 1) as f64);
323    }
324
325    let mut ic = Vec::with_capacity(len);
326    for entry_idx in element_entry_index {
327        let pos = entry_to_position[entry_idx];
328        ic.push((pos + 1) as f64);
329    }
330
331    let value_tensor = Tensor::new(values, vec![order.len(), 1])
332        .map_err(|e| unique_error(format!("unique: {e}")))?;
333    let ia_tensor =
334        Tensor::new(ia, vec![order.len(), 1]).map_err(|e| unique_error(format!("unique: {e}")))?;
335    let ic_tensor =
336        Tensor::new(ic, vec![len, 1]).map_err(|e| unique_error(format!("unique: {e}")))?;
337
338    Ok(UniqueEvaluation::new(
339        tensor::tensor_into_value(value_tensor),
340        ia_tensor,
341        ic_tensor,
342    ))
343}
344
345fn unique_numeric_rows(
346    tensor: Tensor,
347    opts: &UniqueOptions,
348) -> crate::BuiltinResult<UniqueEvaluation> {
349    if tensor.shape.len() != 2 {
350        return Err(unique_error(
351            "unique: 'rows' option requires a 2-D matrix input",
352        ));
353    }
354    let rows = tensor.shape[0];
355    let cols = tensor.shape[1];
356
357    if rows == 0 || cols == 0 {
358        let values = Tensor::new(Vec::new(), vec![0, cols])
359            .map_err(|e| unique_error(format!("unique: {e}")))?;
360        let ia = Tensor::new(Vec::new(), vec![0, 1])
361            .map_err(|e| unique_error(format!("unique: {e}")))?;
362        let ic = Tensor::new(Vec::new(), vec![rows, 1])
363            .map_err(|e| unique_error(format!("unique: {e}")))?;
364        return Ok(UniqueEvaluation::new(
365            tensor::tensor_into_value(values),
366            ia,
367            ic,
368        ));
369    }
370
371    let mut entries = Vec::<NumericRowEntry>::new();
372    let mut map: HashMap<NumericRowKey, usize> = HashMap::new();
373    let mut row_entry_index = Vec::with_capacity(rows);
374
375    for r in 0..rows {
376        let mut row_values = Vec::with_capacity(cols);
377        for c in 0..cols {
378            let idx = r + c * rows;
379            row_values.push(tensor.data[idx]);
380        }
381        let key = NumericRowKey::from_slice(&row_values);
382        match map.get(&key) {
383            Some(&entry_idx) => {
384                entries[entry_idx].last = r;
385                row_entry_index.push(entry_idx);
386            }
387            None => {
388                let entry_idx = entries.len();
389                entries.push(NumericRowEntry {
390                    row_data: row_values.clone(),
391                    first: r,
392                    last: r,
393                });
394                map.insert(key, entry_idx);
395                row_entry_index.push(entry_idx);
396            }
397        }
398    }
399
400    let mut order: Vec<usize> = (0..entries.len()).collect();
401    if opts.order == UniqueOrder::Sorted {
402        order.sort_by(|&a, &b| compare_numeric_rows(&entries[a].row_data, &entries[b].row_data));
403    }
404
405    let mut entry_to_position = vec![0usize; entries.len()];
406    for (pos, &entry_idx) in order.iter().enumerate() {
407        entry_to_position[entry_idx] = pos;
408    }
409
410    let unique_rows_count = order.len();
411    let mut values = vec![0.0f64; unique_rows_count * cols];
412    for (row_pos, &entry_idx) in order.iter().enumerate() {
413        let row = &entries[entry_idx].row_data;
414        for (col, value) in row.iter().enumerate().take(cols) {
415            let dest = row_pos + col * unique_rows_count;
416            values[dest] = *value;
417        }
418    }
419
420    let mut ia = Vec::with_capacity(unique_rows_count);
421    for &entry_idx in &order {
422        let entry = &entries[entry_idx];
423        let occurrence = match opts.occurrence {
424            UniqueOccurrence::First => entry.first,
425            UniqueOccurrence::Last => entry.last,
426        };
427        ia.push((occurrence + 1) as f64);
428    }
429
430    let mut ic = Vec::with_capacity(rows);
431    for entry_idx in row_entry_index {
432        let pos = entry_to_position[entry_idx];
433        ic.push((pos + 1) as f64);
434    }
435
436    let value_tensor = Tensor::new(values, vec![unique_rows_count, cols])
437        .map_err(|e| unique_error(format!("unique: {e}")))?;
438    let ia_tensor = Tensor::new(ia, vec![unique_rows_count, 1])
439        .map_err(|e| unique_error(format!("unique: {e}")))?;
440    let ic_tensor =
441        Tensor::new(ic, vec![rows, 1]).map_err(|e| unique_error(format!("unique: {e}")))?;
442
443    Ok(UniqueEvaluation::new(
444        tensor::tensor_into_value(value_tensor),
445        ia_tensor,
446        ic_tensor,
447    ))
448}
449
450fn unique_complex_from_tensor(
451    tensor: ComplexTensor,
452    opts: &UniqueOptions,
453) -> crate::BuiltinResult<UniqueEvaluation> {
454    if opts.rows {
455        unique_complex_rows(tensor, opts)
456    } else {
457        unique_complex_elements(tensor, opts)
458    }
459}
460
461fn unique_complex_elements(
462    tensor: ComplexTensor,
463    opts: &UniqueOptions,
464) -> crate::BuiltinResult<UniqueEvaluation> {
465    let len = tensor.data.len();
466    if len == 0 {
467        let values = ComplexTensor::new(Vec::new(), vec![0, 1])
468            .map_err(|e| unique_error(format!("unique: {e}")))?;
469        let ia = Tensor::new(Vec::new(), vec![0, 1])
470            .map_err(|e| unique_error(format!("unique: {e}")))?;
471        let ic = Tensor::new(Vec::new(), vec![0, 1])
472            .map_err(|e| unique_error(format!("unique: {e}")))?;
473        return Ok(UniqueEvaluation::new(
474            complex_tensor_into_value(values),
475            ia,
476            ic,
477        ));
478    }
479
480    let mut entries = Vec::<ComplexElementEntry>::new();
481    let mut map: HashMap<ComplexKey, usize> = HashMap::new();
482    let mut element_entry_index = Vec::with_capacity(len);
483
484    for (idx, &value) in tensor.data.iter().enumerate() {
485        let key = ComplexKey::new(value);
486        match map.get(&key) {
487            Some(&entry_idx) => {
488                entries[entry_idx].last = idx;
489                element_entry_index.push(entry_idx);
490            }
491            None => {
492                let entry_idx = entries.len();
493                entries.push(ComplexElementEntry {
494                    value,
495                    first: idx,
496                    last: idx,
497                });
498                map.insert(key, entry_idx);
499                element_entry_index.push(entry_idx);
500            }
501        }
502    }
503
504    let mut order: Vec<usize> = (0..entries.len()).collect();
505    if opts.order == UniqueOrder::Sorted {
506        order.sort_by(|&a, &b| compare_complex(entries[a].value, entries[b].value));
507    }
508
509    let mut entry_to_position = vec![0usize; entries.len()];
510    for (pos, &entry_idx) in order.iter().enumerate() {
511        entry_to_position[entry_idx] = pos;
512    }
513
514    let mut values = Vec::with_capacity(order.len());
515    let mut ia = Vec::with_capacity(order.len());
516    for &entry_idx in &order {
517        let entry = &entries[entry_idx];
518        values.push(entry.value);
519        let occurrence = match opts.occurrence {
520            UniqueOccurrence::First => entry.first,
521            UniqueOccurrence::Last => entry.last,
522        };
523        ia.push((occurrence + 1) as f64);
524    }
525
526    let mut ic = Vec::with_capacity(len);
527    for entry_idx in element_entry_index {
528        let pos = entry_to_position[entry_idx];
529        ic.push((pos + 1) as f64);
530    }
531
532    let value_tensor = ComplexTensor::new(values, vec![order.len(), 1])
533        .map_err(|e| unique_error(format!("unique: {e}")))?;
534    let ia_tensor =
535        Tensor::new(ia, vec![order.len(), 1]).map_err(|e| unique_error(format!("unique: {e}")))?;
536    let ic_tensor =
537        Tensor::new(ic, vec![len, 1]).map_err(|e| unique_error(format!("unique: {e}")))?;
538
539    Ok(UniqueEvaluation::new(
540        complex_tensor_into_value(value_tensor),
541        ia_tensor,
542        ic_tensor,
543    ))
544}
545
546fn unique_complex_rows(
547    tensor: ComplexTensor,
548    opts: &UniqueOptions,
549) -> crate::BuiltinResult<UniqueEvaluation> {
550    if tensor.shape.len() != 2 {
551        return Err(unique_error(
552            "unique: 'rows' option requires a 2-D matrix input",
553        ));
554    }
555    let rows = tensor.shape[0];
556    let cols = tensor.shape[1];
557
558    if rows == 0 || cols == 0 {
559        let values = ComplexTensor::new(Vec::new(), vec![rows, cols])
560            .map_err(|e| unique_error(format!("unique: {e}")))?;
561        let ia = Tensor::new(Vec::new(), vec![0, 1])
562            .map_err(|e| unique_error(format!("unique: {e}")))?;
563        let ic = Tensor::new(Vec::new(), vec![rows, 1])
564            .map_err(|e| unique_error(format!("unique: {e}")))?;
565        return Ok(UniqueEvaluation::new(
566            complex_tensor_into_value(values),
567            ia,
568            ic,
569        ));
570    }
571
572    let mut entries = Vec::<ComplexRowEntry>::new();
573    let mut map: HashMap<Vec<ComplexKey>, usize> = HashMap::new();
574    let mut row_entry_index = Vec::with_capacity(rows);
575
576    for r in 0..rows {
577        let mut row_values = Vec::with_capacity(cols);
578        let mut key_row = Vec::with_capacity(cols);
579        for c in 0..cols {
580            let idx = r + c * rows;
581            let value = tensor.data[idx];
582            row_values.push(value);
583            key_row.push(ComplexKey::new(value));
584        }
585        match map.get(&key_row) {
586            Some(&entry_idx) => {
587                entries[entry_idx].last = r;
588                row_entry_index.push(entry_idx);
589            }
590            None => {
591                let entry_idx = entries.len();
592                entries.push(ComplexRowEntry {
593                    row_data: row_values.clone(),
594                    first: r,
595                    last: r,
596                });
597                map.insert(key_row, entry_idx);
598                row_entry_index.push(entry_idx);
599            }
600        }
601    }
602
603    let mut order: Vec<usize> = (0..entries.len()).collect();
604    if opts.order == UniqueOrder::Sorted {
605        order.sort_by(|&a, &b| compare_complex_rows(&entries[a].row_data, &entries[b].row_data));
606    }
607
608    let mut entry_to_position = vec![0usize; entries.len()];
609    for (pos, &entry_idx) in order.iter().enumerate() {
610        entry_to_position[entry_idx] = pos;
611    }
612
613    let unique_rows_count = order.len();
614    let mut values = vec![(0.0, 0.0); unique_rows_count * cols];
615    for (row_pos, &entry_idx) in order.iter().enumerate() {
616        let row = &entries[entry_idx].row_data;
617        for (col, value) in row.iter().enumerate().take(cols) {
618            let dest = row_pos + col * unique_rows_count;
619            values[dest] = *value;
620        }
621    }
622
623    let mut ia = Vec::with_capacity(unique_rows_count);
624    for &entry_idx in &order {
625        let entry = &entries[entry_idx];
626        let occurrence = match opts.occurrence {
627            UniqueOccurrence::First => entry.first,
628            UniqueOccurrence::Last => entry.last,
629        };
630        ia.push((occurrence + 1) as f64);
631    }
632
633    let mut ic = Vec::with_capacity(rows);
634    for entry_idx in row_entry_index {
635        let pos = entry_to_position[entry_idx];
636        ic.push((pos + 1) as f64);
637    }
638
639    let value_tensor = ComplexTensor::new(values, vec![unique_rows_count, cols])
640        .map_err(|e| unique_error(format!("unique: {e}")))?;
641    let ia_tensor = Tensor::new(ia, vec![unique_rows_count, 1])
642        .map_err(|e| unique_error(format!("unique: {e}")))?;
643    let ic_tensor =
644        Tensor::new(ic, vec![rows, 1]).map_err(|e| unique_error(format!("unique: {e}")))?;
645
646    Ok(UniqueEvaluation::new(
647        complex_tensor_into_value(value_tensor),
648        ia_tensor,
649        ic_tensor,
650    ))
651}
652
653fn unique_char_array(
654    array: CharArray,
655    opts: &UniqueOptions,
656) -> crate::BuiltinResult<UniqueEvaluation> {
657    if opts.rows {
658        unique_char_rows(array, opts)
659    } else {
660        unique_char_elements(array, opts)
661    }
662}
663
664fn unique_char_elements(
665    array: CharArray,
666    opts: &UniqueOptions,
667) -> crate::BuiltinResult<UniqueEvaluation> {
668    let rows = array.rows;
669    let cols = array.cols;
670    let total = rows * cols;
671    if total == 0 {
672        let values =
673            CharArray::new(Vec::new(), 0, 0).map_err(|e| unique_error(format!("unique: {e}")))?;
674        let ia = Tensor::new(Vec::new(), vec![0, 1])
675            .map_err(|e| unique_error(format!("unique: {e}")))?;
676        let ic = Tensor::new(Vec::new(), vec![0, 1])
677            .map_err(|e| unique_error(format!("unique: {e}")))?;
678        return Ok(UniqueEvaluation::new(Value::CharArray(values), ia, ic));
679    }
680
681    let mut entries = Vec::<CharElementEntry>::new();
682    let mut map: HashMap<u32, usize> = HashMap::new();
683    let mut element_entry_index = Vec::with_capacity(total);
684
685    for col in 0..cols {
686        for row in 0..rows {
687            let linear_idx = row + col * rows;
688            let data_idx = row * cols + col;
689            let ch = array.data[data_idx];
690            let key = ch as u32;
691            match map.get(&key) {
692                Some(&entry_idx) => {
693                    entries[entry_idx].last = linear_idx;
694                    element_entry_index.push(entry_idx);
695                }
696                None => {
697                    let entry_idx = entries.len();
698                    entries.push(CharElementEntry {
699                        ch,
700                        first: linear_idx,
701                        last: linear_idx,
702                    });
703                    map.insert(key, entry_idx);
704                    element_entry_index.push(entry_idx);
705                }
706            }
707        }
708    }
709
710    let mut order: Vec<usize> = (0..entries.len()).collect();
711    if opts.order == UniqueOrder::Sorted {
712        order.sort_by(|&a, &b| entries[a].ch.cmp(&entries[b].ch));
713    }
714
715    let mut entry_to_position = vec![0usize; entries.len()];
716    for (pos, &entry_idx) in order.iter().enumerate() {
717        entry_to_position[entry_idx] = pos;
718    }
719
720    let mut values = Vec::with_capacity(order.len());
721    let mut ia = Vec::with_capacity(order.len());
722    for &entry_idx in &order {
723        let entry = &entries[entry_idx];
724        values.push(entry.ch);
725        let occurrence = match opts.occurrence {
726            UniqueOccurrence::First => entry.first,
727            UniqueOccurrence::Last => entry.last,
728        };
729        ia.push((occurrence + 1) as f64);
730    }
731
732    let mut ic = Vec::with_capacity(total);
733    for entry_idx in element_entry_index {
734        let pos = entry_to_position[entry_idx];
735        ic.push((pos + 1) as f64);
736    }
737
738    let value_array =
739        CharArray::new(values, order.len(), 1).map_err(|e| unique_error(format!("unique: {e}")))?;
740    let ia_tensor =
741        Tensor::new(ia, vec![order.len(), 1]).map_err(|e| unique_error(format!("unique: {e}")))?;
742    let ic_tensor =
743        Tensor::new(ic, vec![total, 1]).map_err(|e| unique_error(format!("unique: {e}")))?;
744
745    Ok(UniqueEvaluation::new(
746        Value::CharArray(value_array),
747        ia_tensor,
748        ic_tensor,
749    ))
750}
751
752fn unique_char_rows(
753    array: CharArray,
754    opts: &UniqueOptions,
755) -> crate::BuiltinResult<UniqueEvaluation> {
756    let rows = array.rows;
757    let cols = array.cols;
758    if rows == 0 {
759        let values = CharArray::new(Vec::new(), 0, cols)
760            .map_err(|e| unique_error(format!("unique: {e}")))?;
761        let ia = Tensor::new(Vec::new(), vec![0, 1])
762            .map_err(|e| unique_error(format!("unique: {e}")))?;
763        let ic = Tensor::new(Vec::new(), vec![0, 1])
764            .map_err(|e| unique_error(format!("unique: {e}")))?;
765        return Ok(UniqueEvaluation::new(Value::CharArray(values), ia, ic));
766    }
767
768    let mut entries = Vec::<CharRowEntry>::new();
769    let mut map: HashMap<RowCharKey, usize> = HashMap::new();
770    let mut row_entry_index = Vec::with_capacity(rows);
771
772    for r in 0..rows {
773        let start = r * cols;
774        let end = start + cols;
775        let slice = &array.data[start..end];
776        let key = RowCharKey::from_slice(slice);
777        match map.get(&key) {
778            Some(&entry_idx) => {
779                entries[entry_idx].last = r;
780                row_entry_index.push(entry_idx);
781            }
782            None => {
783                let entry_idx = entries.len();
784                entries.push(CharRowEntry {
785                    row_data: slice.to_vec(),
786                    first: r,
787                    last: r,
788                });
789                map.insert(key, entry_idx);
790                row_entry_index.push(entry_idx);
791            }
792        }
793    }
794
795    let mut order: Vec<usize> = (0..entries.len()).collect();
796    if opts.order == UniqueOrder::Sorted {
797        order.sort_by(|&a, &b| compare_char_rows(&entries[a].row_data, &entries[b].row_data));
798    }
799
800    let mut entry_to_position = vec![0usize; entries.len()];
801    for (pos, &entry_idx) in order.iter().enumerate() {
802        entry_to_position[entry_idx] = pos;
803    }
804
805    let unique_rows_count = order.len();
806    let mut values = vec!['\0'; unique_rows_count * cols];
807    for (row_pos, &entry_idx) in order.iter().enumerate() {
808        let row = &entries[entry_idx].row_data;
809        for col in 0..cols {
810            let dest = row_pos * cols + col;
811            if col < row.len() {
812                values[dest] = row[col];
813            }
814        }
815    }
816
817    let mut ia = Vec::with_capacity(unique_rows_count);
818    for &entry_idx in &order {
819        let entry = &entries[entry_idx];
820        let occurrence = match opts.occurrence {
821            UniqueOccurrence::First => entry.first,
822            UniqueOccurrence::Last => entry.last,
823        };
824        ia.push((occurrence + 1) as f64);
825    }
826
827    let mut ic = Vec::with_capacity(rows);
828    for entry_idx in row_entry_index {
829        let pos = entry_to_position[entry_idx];
830        ic.push((pos + 1) as f64);
831    }
832
833    let value_array = CharArray::new(values, unique_rows_count, cols)
834        .map_err(|e| unique_error(format!("unique: {e}")))?;
835    let ia_tensor = Tensor::new(ia, vec![unique_rows_count, 1])
836        .map_err(|e| unique_error(format!("unique: {e}")))?;
837    let ic_tensor =
838        Tensor::new(ic, vec![rows, 1]).map_err(|e| unique_error(format!("unique: {e}")))?;
839
840    Ok(UniqueEvaluation::new(
841        Value::CharArray(value_array),
842        ia_tensor,
843        ic_tensor,
844    ))
845}
846
847fn unique_string_array(
848    array: StringArray,
849    opts: &UniqueOptions,
850) -> crate::BuiltinResult<UniqueEvaluation> {
851    if opts.rows {
852        unique_string_rows(array, opts)
853    } else {
854        unique_string_elements(array, opts)
855    }
856}
857
858fn unique_string_elements(
859    array: StringArray,
860    opts: &UniqueOptions,
861) -> crate::BuiltinResult<UniqueEvaluation> {
862    let len = array.data.len();
863    if len == 0 {
864        let values = StringArray::new(Vec::new(), vec![0, 1])
865            .map_err(|e| unique_error(format!("unique: {e}")))?;
866        let ia = Tensor::new(Vec::new(), vec![0, 1])
867            .map_err(|e| unique_error(format!("unique: {e}")))?;
868        let ic = Tensor::new(Vec::new(), vec![0, 1])
869            .map_err(|e| unique_error(format!("unique: {e}")))?;
870        return Ok(UniqueEvaluation::new(Value::StringArray(values), ia, ic));
871    }
872
873    let mut entries = Vec::<StringElementEntry>::new();
874    let mut map: HashMap<String, usize> = HashMap::new();
875    let mut element_entry_index = Vec::with_capacity(len);
876
877    for (idx, value) in array.data.iter().enumerate() {
878        match map.get(value) {
879            Some(&entry_idx) => {
880                entries[entry_idx].last = idx;
881                element_entry_index.push(entry_idx);
882            }
883            None => {
884                let entry_idx = entries.len();
885                entries.push(StringElementEntry {
886                    value: value.clone(),
887                    first: idx,
888                    last: idx,
889                });
890                map.insert(value.clone(), entry_idx);
891                element_entry_index.push(entry_idx);
892            }
893        }
894    }
895
896    let mut order: Vec<usize> = (0..entries.len()).collect();
897    if opts.order == UniqueOrder::Sorted {
898        order.sort_by(|&a, &b| entries[a].value.cmp(&entries[b].value));
899    }
900
901    let mut entry_to_position = vec![0usize; entries.len()];
902    for (pos, &entry_idx) in order.iter().enumerate() {
903        entry_to_position[entry_idx] = pos;
904    }
905
906    let mut values = Vec::with_capacity(order.len());
907    let mut ia = Vec::with_capacity(order.len());
908    for &entry_idx in &order {
909        let entry = &entries[entry_idx];
910        values.push(entry.value.clone());
911        let occurrence = match opts.occurrence {
912            UniqueOccurrence::First => entry.first,
913            UniqueOccurrence::Last => entry.last,
914        };
915        ia.push((occurrence + 1) as f64);
916    }
917
918    let mut ic = Vec::with_capacity(len);
919    for entry_idx in element_entry_index {
920        let pos = entry_to_position[entry_idx];
921        ic.push((pos + 1) as f64);
922    }
923
924    let value_array = StringArray::new(values, vec![order.len(), 1])
925        .map_err(|e| unique_error(format!("unique: {e}")))?;
926    let ia_tensor =
927        Tensor::new(ia, vec![order.len(), 1]).map_err(|e| unique_error(format!("unique: {e}")))?;
928    let ic_tensor =
929        Tensor::new(ic, vec![len, 1]).map_err(|e| unique_error(format!("unique: {e}")))?;
930
931    Ok(UniqueEvaluation::new(
932        Value::StringArray(value_array),
933        ia_tensor,
934        ic_tensor,
935    ))
936}
937
938fn unique_string_rows(
939    array: StringArray,
940    opts: &UniqueOptions,
941) -> crate::BuiltinResult<UniqueEvaluation> {
942    if array.shape.len() != 2 {
943        return Err(unique_error(
944            "unique: 'rows' option requires a 2-D matrix input",
945        ));
946    }
947    let rows = array.shape[0];
948    let cols = array.shape[1];
949
950    if rows == 0 {
951        let values = StringArray::new(Vec::new(), vec![0, cols])
952            .map_err(|e| unique_error(format!("unique: {e}")))?;
953        let ia = Tensor::new(Vec::new(), vec![0, 1])
954            .map_err(|e| unique_error(format!("unique: {e}")))?;
955        let ic = Tensor::new(Vec::new(), vec![0, 1])
956            .map_err(|e| unique_error(format!("unique: {e}")))?;
957        return Ok(UniqueEvaluation::new(Value::StringArray(values), ia, ic));
958    }
959
960    let mut entries = Vec::<StringRowEntry>::new();
961    let mut map: HashMap<RowStringKey, usize> = HashMap::new();
962    let mut row_entry_index = Vec::with_capacity(rows);
963
964    for r in 0..rows {
965        let mut row_values = Vec::with_capacity(cols);
966        for c in 0..cols {
967            let idx = r + c * rows;
968            row_values.push(array.data[idx].clone());
969        }
970        let key = RowStringKey(row_values.clone());
971        match map.get(&key) {
972            Some(&entry_idx) => {
973                entries[entry_idx].last = r;
974                row_entry_index.push(entry_idx);
975            }
976            None => {
977                let entry_idx = entries.len();
978                entries.push(StringRowEntry {
979                    row_data: row_values.clone(),
980                    first: r,
981                    last: r,
982                });
983                map.insert(key, entry_idx);
984                row_entry_index.push(entry_idx);
985            }
986        }
987    }
988
989    let mut order: Vec<usize> = (0..entries.len()).collect();
990    if opts.order == UniqueOrder::Sorted {
991        order.sort_by(|&a, &b| compare_string_rows(&entries[a].row_data, &entries[b].row_data));
992    }
993
994    let mut entry_to_position = vec![0usize; entries.len()];
995    for (pos, &entry_idx) in order.iter().enumerate() {
996        entry_to_position[entry_idx] = pos;
997    }
998
999    let unique_rows_count = order.len();
1000    let mut values = vec![String::new(); unique_rows_count * cols];
1001    for (row_pos, &entry_idx) in order.iter().enumerate() {
1002        let row = &entries[entry_idx].row_data;
1003        for (col, value) in row.iter().enumerate().take(cols) {
1004            let dest = row_pos + col * unique_rows_count;
1005            values[dest] = value.clone();
1006        }
1007    }
1008
1009    let mut ia = Vec::with_capacity(unique_rows_count);
1010    for &entry_idx in &order {
1011        let entry = &entries[entry_idx];
1012        let occurrence = match opts.occurrence {
1013            UniqueOccurrence::First => entry.first,
1014            UniqueOccurrence::Last => entry.last,
1015        };
1016        ia.push((occurrence + 1) as f64);
1017    }
1018
1019    let mut ic = Vec::with_capacity(rows);
1020    for entry_idx in row_entry_index {
1021        let pos = entry_to_position[entry_idx];
1022        ic.push((pos + 1) as f64);
1023    }
1024
1025    let value_array = StringArray::new(values, vec![unique_rows_count, cols])
1026        .map_err(|e| unique_error(format!("unique: {e}")))?;
1027    let ia_tensor = Tensor::new(ia, vec![unique_rows_count, 1])
1028        .map_err(|e| unique_error(format!("unique: {e}")))?;
1029    let ic_tensor =
1030        Tensor::new(ic, vec![rows, 1]).map_err(|e| unique_error(format!("unique: {e}")))?;
1031
1032    Ok(UniqueEvaluation::new(
1033        Value::StringArray(value_array),
1034        ia_tensor,
1035        ic_tensor,
1036    ))
1037}
1038
1039#[derive(Debug)]
1040struct NumericElementEntry {
1041    value: f64,
1042    first: usize,
1043    last: usize,
1044}
1045
1046#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1047struct NumericRowKey(Vec<u64>);
1048
1049impl NumericRowKey {
1050    fn from_slice(values: &[f64]) -> Self {
1051        NumericRowKey(values.iter().map(|&v| canonicalize_f64(v)).collect())
1052    }
1053}
1054
1055#[derive(Debug, Clone)]
1056struct NumericRowEntry {
1057    row_data: Vec<f64>,
1058    first: usize,
1059    last: usize,
1060}
1061
1062#[derive(Debug)]
1063struct ComplexElementEntry {
1064    value: (f64, f64),
1065    first: usize,
1066    last: usize,
1067}
1068
1069#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1070struct ComplexKey {
1071    re: u64,
1072    im: u64,
1073}
1074
1075impl ComplexKey {
1076    fn new(value: (f64, f64)) -> Self {
1077        Self {
1078            re: canonicalize_f64(value.0),
1079            im: canonicalize_f64(value.1),
1080        }
1081    }
1082}
1083
1084#[derive(Debug, Clone)]
1085struct ComplexRowEntry {
1086    row_data: Vec<(f64, f64)>,
1087    first: usize,
1088    last: usize,
1089}
1090
1091#[derive(Debug)]
1092struct CharElementEntry {
1093    ch: char,
1094    first: usize,
1095    last: usize,
1096}
1097
1098#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1099struct RowCharKey(Vec<u32>);
1100
1101impl RowCharKey {
1102    fn from_slice(values: &[char]) -> Self {
1103        RowCharKey(values.iter().map(|&ch| ch as u32).collect())
1104    }
1105}
1106
1107#[derive(Debug, Clone)]
1108struct CharRowEntry {
1109    row_data: Vec<char>,
1110    first: usize,
1111    last: usize,
1112}
1113
1114#[derive(Debug, Clone)]
1115struct StringElementEntry {
1116    value: String,
1117    first: usize,
1118    last: usize,
1119}
1120
1121#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1122struct RowStringKey(Vec<String>);
1123
1124#[derive(Debug, Clone)]
1125struct StringRowEntry {
1126    row_data: Vec<String>,
1127    first: usize,
1128    last: usize,
1129}
1130
1131fn canonicalize_f64(value: f64) -> u64 {
1132    if value.is_nan() {
1133        0x7ff8_0000_0000_0000u64
1134    } else if value == 0.0 {
1135        0u64
1136    } else {
1137        value.to_bits()
1138    }
1139}
1140
1141fn compare_f64(a: f64, b: f64) -> Ordering {
1142    if a.is_nan() {
1143        if b.is_nan() {
1144            Ordering::Equal
1145        } else {
1146            Ordering::Greater
1147        }
1148    } else if b.is_nan() {
1149        Ordering::Less
1150    } else {
1151        a.partial_cmp(&b).unwrap_or(Ordering::Equal)
1152    }
1153}
1154
1155fn compare_numeric_rows(a: &[f64], b: &[f64]) -> Ordering {
1156    for (lhs, rhs) in a.iter().zip(b.iter()) {
1157        let ord = compare_f64(*lhs, *rhs);
1158        if ord != Ordering::Equal {
1159            return ord;
1160        }
1161    }
1162    Ordering::Equal
1163}
1164
1165fn complex_is_nan(value: (f64, f64)) -> bool {
1166    value.0.is_nan() || value.1.is_nan()
1167}
1168
1169fn compare_complex(a: (f64, f64), b: (f64, f64)) -> Ordering {
1170    match (complex_is_nan(a), complex_is_nan(b)) {
1171        (true, true) => Ordering::Equal,
1172        (true, false) => Ordering::Greater,
1173        (false, true) => Ordering::Less,
1174        (false, false) => {
1175            let mag_a = a.0.hypot(a.1);
1176            let mag_b = b.0.hypot(b.1);
1177            let mag_cmp = compare_f64(mag_a, mag_b);
1178            if mag_cmp != Ordering::Equal {
1179                return mag_cmp;
1180            }
1181            let re_cmp = compare_f64(a.0, b.0);
1182            if re_cmp != Ordering::Equal {
1183                return re_cmp;
1184            }
1185            compare_f64(a.1, b.1)
1186        }
1187    }
1188}
1189
1190fn compare_complex_rows(a: &[(f64, f64)], b: &[(f64, f64)]) -> Ordering {
1191    for (lhs, rhs) in a.iter().zip(b.iter()) {
1192        let ord = compare_complex(*lhs, *rhs);
1193        if ord != Ordering::Equal {
1194            return ord;
1195        }
1196    }
1197    Ordering::Equal
1198}
1199
1200fn compare_char_rows(a: &[char], b: &[char]) -> Ordering {
1201    for (lhs, rhs) in a.iter().zip(b.iter()) {
1202        let ord = lhs.cmp(rhs);
1203        if ord != Ordering::Equal {
1204            return ord;
1205        }
1206    }
1207    Ordering::Equal
1208}
1209
1210fn compare_string_rows(a: &[String], b: &[String]) -> Ordering {
1211    for (lhs, rhs) in a.iter().zip(b.iter()) {
1212        let ord = lhs.cmp(rhs);
1213        if ord != Ordering::Equal {
1214            return ord;
1215        }
1216    }
1217    Ordering::Equal
1218}
1219
1220#[derive(Debug)]
1221pub struct UniqueEvaluation {
1222    values: Value,
1223    ia: Tensor,
1224    ic: Tensor,
1225}
1226
1227impl UniqueEvaluation {
1228    fn new(values: Value, ia: Tensor, ic: Tensor) -> Self {
1229        Self { values, ia, ic }
1230    }
1231
1232    pub fn into_values_value(self) -> Value {
1233        self.values
1234    }
1235
1236    pub fn into_pair(self) -> (Value, Value) {
1237        let ia = tensor::tensor_into_value(self.ia);
1238        (self.values, ia)
1239    }
1240
1241    pub fn into_triple(self) -> (Value, Value, Value) {
1242        let ia = tensor::tensor_into_value(self.ia);
1243        let ic = tensor::tensor_into_value(self.ic);
1244        (self.values, ia, ic)
1245    }
1246
1247    pub fn from_unique_result(result: UniqueResult) -> crate::BuiltinResult<Self> {
1248        let UniqueResult { values, ia, ic } = result;
1249        let values_tensor = Tensor::new(values.data, values.shape)
1250            .map_err(|e| unique_error(format!("unique: {e}")))?;
1251        let ia_tensor =
1252            Tensor::new(ia.data, ia.shape).map_err(|e| unique_error(format!("unique: {e}")))?;
1253        let ic_tensor =
1254            Tensor::new(ic.data, ic.shape).map_err(|e| unique_error(format!("unique: {e}")))?;
1255        Ok(UniqueEvaluation::new(
1256            tensor::tensor_into_value(values_tensor),
1257            ia_tensor,
1258            ic_tensor,
1259        ))
1260    }
1261
1262    pub fn into_numeric_unique_result(self) -> crate::BuiltinResult<UniqueResult> {
1263        let UniqueEvaluation { values, ia, ic } = self;
1264        let values_tensor =
1265            tensor::value_into_tensor_for("unique", values).map_err(|e| unique_error(e))?;
1266        Ok(UniqueResult {
1267            values: HostTensorOwned {
1268                data: values_tensor.data,
1269                shape: values_tensor.shape,
1270                storage: GpuTensorStorage::Real,
1271            },
1272            ia: HostTensorOwned {
1273                data: ia.data,
1274                shape: ia.shape,
1275                storage: GpuTensorStorage::Real,
1276            },
1277            ic: HostTensorOwned {
1278                data: ic.data,
1279                shape: ic.shape,
1280                storage: GpuTensorStorage::Real,
1281            },
1282        })
1283    }
1284
1285    pub fn ia_value(&self) -> Value {
1286        tensor::tensor_into_value(self.ia.clone())
1287    }
1288
1289    pub fn ic_value(&self) -> Value {
1290        tensor::tensor_into_value(self.ic.clone())
1291    }
1292}
1293
1294#[cfg(test)]
1295pub(crate) mod tests {
1296    use super::*;
1297    use crate::builtins::common::test_support;
1298    use runmat_builtins::{
1299        CharArray, IntValue, LogicalArray, ResolveContext, StringArray, Tensor, Type, Value,
1300    };
1301
1302    fn error_message(err: crate::RuntimeError) -> String {
1303        err.message().to_string()
1304    }
1305
1306    fn evaluate_sync(value: Value, rest: &[Value]) -> crate::BuiltinResult<UniqueEvaluation> {
1307        futures::executor::block_on(evaluate(value, rest))
1308    }
1309
1310    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1311    #[test]
1312    fn unique_sorted_default() {
1313        let tensor = Tensor::new(vec![3.0, 1.0, 3.0, 2.0], vec![4, 1]).unwrap();
1314        let eval = evaluate_sync(Value::Tensor(tensor), &[]).expect("unique");
1315        let (values, ia, ic) = eval.into_triple();
1316        match values {
1317            Value::Tensor(t) => {
1318                assert_eq!(t.data, vec![1.0, 2.0, 3.0]);
1319                assert_eq!(t.shape, vec![3, 1]);
1320            }
1321            Value::Num(_) => panic!("expected tensor result"),
1322            other => panic!("unexpected result {other:?}"),
1323        }
1324        match ia {
1325            Value::Tensor(t) => assert_eq!(t.data, vec![2.0, 4.0, 1.0]),
1326            other => panic!("unexpected IA {other:?}"),
1327        }
1328        match ic {
1329            Value::Tensor(t) => assert_eq!(t.data, vec![3.0, 1.0, 3.0, 2.0]),
1330            other => panic!("unexpected IC {other:?}"),
1331        }
1332    }
1333
1334    #[test]
1335    fn unique_type_resolver_numeric() {
1336        assert_eq!(
1337            set_values_output_type(&[Type::tensor()], &ResolveContext::new(Vec::new())),
1338            Type::tensor()
1339        );
1340    }
1341
1342    #[test]
1343    fn unique_type_resolver_string_array() {
1344        assert_eq!(
1345            set_values_output_type(
1346                &[Type::cell_of(Type::String)],
1347                &ResolveContext::new(Vec::new()),
1348            ),
1349            Type::cell_of(Type::String)
1350        );
1351    }
1352
1353    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1354    #[test]
1355    fn unique_sorted_handles_nan() {
1356        let tensor = Tensor::new(vec![f64::NAN, 2.0, f64::NAN, 1.0], vec![4, 1]).unwrap();
1357        let eval = evaluate_sync(Value::Tensor(tensor), &[]).expect("unique");
1358        let (values, ..) = eval.into_triple();
1359        match values {
1360            Value::Tensor(t) => {
1361                assert_eq!(t.data.len(), 3);
1362                assert_eq!(t.data[0], 1.0);
1363                assert_eq!(t.data[1], 2.0);
1364                assert!(t.data[2].is_nan());
1365            }
1366            other => panic!("unexpected values {other:?}"),
1367        }
1368    }
1369
1370    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1371    #[test]
1372    fn unique_stable_with_nan() {
1373        let tensor = Tensor::new(vec![f64::NAN, 2.0, f64::NAN, 1.0], vec![4, 1]).unwrap();
1374        let eval = evaluate_sync(Value::Tensor(tensor), &[Value::from("stable")]).expect("unique");
1375        let (values, ..) = eval.into_triple();
1376        match values {
1377            Value::Tensor(t) => {
1378                assert!(t.data[0].is_nan());
1379                assert_eq!(t.data[1], 2.0);
1380                assert_eq!(t.data[2], 1.0);
1381            }
1382            other => panic!("unexpected values {other:?}"),
1383        }
1384    }
1385
1386    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1387    #[test]
1388    fn unique_stable_preserves_order() {
1389        let tensor = Tensor::new(vec![4.0, 2.0, 4.0, 1.0, 2.0], vec![5, 1]).unwrap();
1390        let eval = evaluate_sync(Value::Tensor(tensor), &[Value::from("stable")]).expect("unique");
1391        let (values, ia) = eval.into_pair();
1392        match values {
1393            Value::Tensor(t) => assert_eq!(t.data, vec![4.0, 2.0, 1.0]),
1394            other => panic!("unexpected values {other:?}"),
1395        }
1396        match ia {
1397            Value::Tensor(t) => assert_eq!(t.data, vec![1.0, 2.0, 4.0]),
1398            other => panic!("unexpected IA {other:?}"),
1399        }
1400    }
1401
1402    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1403    #[test]
1404    fn unique_last_occurrence() {
1405        let tensor = Tensor::new(vec![9.0, 8.0, 9.0, 7.0, 8.0], vec![5, 1]).unwrap();
1406        let eval = evaluate_sync(Value::Tensor(tensor), &[Value::from("last")]).expect("unique");
1407        let (values, ia, ic) = eval.into_triple();
1408        match values {
1409            Value::Tensor(t) => assert_eq!(t.data, vec![7.0, 8.0, 9.0]),
1410            other => panic!("unexpected values {other:?}"),
1411        }
1412        match ia {
1413            Value::Tensor(t) => assert_eq!(t.data, vec![4.0, 5.0, 3.0]),
1414            other => panic!("unexpected IA {other:?}"),
1415        }
1416        match ic {
1417            Value::Tensor(t) => assert_eq!(t.data, vec![3.0, 2.0, 3.0, 1.0, 2.0]),
1418            other => panic!("unexpected IC {other:?}"),
1419        }
1420    }
1421
1422    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1423    #[test]
1424    fn unique_rows_sorted_default() {
1425        let tensor = Tensor::new(vec![1.0, 1.0, 2.0, 1.0, 3.0, 3.0, 4.0, 2.0], vec![4, 2]).unwrap();
1426        let eval = evaluate_sync(Value::Tensor(tensor), &[Value::from("rows")]).expect("unique");
1427        let (values, ia, ic) = eval.into_triple();
1428        match values {
1429            Value::Tensor(t) => {
1430                assert_eq!(t.shape, vec![3, 2]);
1431                assert_eq!(t.data, vec![1.0, 1.0, 2.0, 2.0, 3.0, 4.0]);
1432            }
1433            other => panic!("unexpected values {other:?}"),
1434        }
1435        match ia {
1436            Value::Tensor(t) => assert_eq!(t.data, vec![4.0, 1.0, 3.0]),
1437            other => panic!("unexpected IA {other:?}"),
1438        }
1439        match ic {
1440            Value::Tensor(t) => assert_eq!(t.data, vec![2.0, 2.0, 3.0, 1.0]),
1441            other => panic!("unexpected IC {other:?}"),
1442        }
1443    }
1444
1445    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1446    #[test]
1447    fn unique_rows_stable_last() {
1448        let tensor = Tensor::new(vec![1.0, 1.0, 2.0, 1.0, 1.0, 2.0], vec![3, 2]).unwrap();
1449        let eval = evaluate_sync(
1450            Value::Tensor(tensor),
1451            &[
1452                Value::from("rows"),
1453                Value::from("stable"),
1454                Value::from("last"),
1455            ],
1456        )
1457        .expect("unique");
1458        let (values, ia, ic) = eval.into_triple();
1459        match values {
1460            Value::Tensor(t) => {
1461                assert_eq!(t.shape, vec![2, 2]);
1462                assert_eq!(t.data, vec![1.0, 2.0, 1.0, 2.0]);
1463            }
1464            other => panic!("unexpected values {other:?}"),
1465        }
1466        match ia {
1467            Value::Tensor(t) => assert_eq!(t.data, vec![2.0, 3.0]),
1468            other => panic!("unexpected IA {other:?}"),
1469        }
1470        match ic {
1471            Value::Tensor(t) => assert_eq!(t.data, vec![1.0, 1.0, 2.0]),
1472            other => panic!("unexpected IC {other:?}"),
1473        }
1474    }
1475
1476    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1477    #[test]
1478    fn unique_char_elements_sorted() {
1479        let chars = CharArray::new(vec!['m', 'z', 'm', 'a'], 2, 2).unwrap();
1480        let eval = evaluate_sync(Value::CharArray(chars), &[]).expect("unique");
1481        let (values, ia, ic) = eval.into_triple();
1482        match values {
1483            Value::CharArray(arr) => {
1484                assert_eq!(arr.rows, 3);
1485                assert_eq!(arr.cols, 1);
1486                assert_eq!(arr.data, vec!['a', 'm', 'z']);
1487            }
1488            other => panic!("unexpected values {other:?}"),
1489        }
1490        match ia {
1491            Value::Tensor(t) => assert_eq!(t.data, vec![4.0, 1.0, 3.0]),
1492            other => panic!("unexpected IA {other:?}"),
1493        }
1494        match ic {
1495            Value::Tensor(t) => assert_eq!(t.data, vec![2.0, 2.0, 3.0, 1.0]),
1496            other => panic!("unexpected IC {other:?}"),
1497        }
1498    }
1499
1500    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1501    #[test]
1502    fn unique_char_rows_last() {
1503        let chars = CharArray::new(vec!['a', 'b', 'a', 'b', 'a', 'c'], 3, 2).unwrap();
1504        let eval = evaluate_sync(
1505            Value::CharArray(chars),
1506            &[Value::from("rows"), Value::from("last")],
1507        )
1508        .expect("unique");
1509        let (values, ia, ic) = eval.into_triple();
1510        match values {
1511            Value::CharArray(arr) => {
1512                assert_eq!(arr.rows, 2);
1513                assert_eq!(arr.cols, 2);
1514                assert_eq!(arr.data, vec!['a', 'b', 'a', 'c']);
1515            }
1516            other => panic!("unexpected values {other:?}"),
1517        }
1518        match ia {
1519            Value::Tensor(t) => assert_eq!(t.data, vec![2.0, 3.0]),
1520            other => panic!("unexpected IA {other:?}"),
1521        }
1522        match ic {
1523            Value::Tensor(t) => assert_eq!(t.data, vec![1.0, 1.0, 2.0]),
1524            other => panic!("unexpected IC {other:?}"),
1525        }
1526    }
1527
1528    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1529    #[test]
1530    fn unique_string_elements_stable() {
1531        let array = StringArray::new(
1532            vec!["beta".into(), "alpha".into(), "beta".into()],
1533            vec![3, 1],
1534        )
1535        .unwrap();
1536        let eval =
1537            evaluate_sync(Value::StringArray(array), &[Value::from("stable")]).expect("unique");
1538        let (values, ia, ic) = eval.into_triple();
1539        match values {
1540            Value::StringArray(sa) => {
1541                assert_eq!(sa.data, vec!["beta", "alpha"]);
1542                assert_eq!(sa.shape, vec![2, 1]);
1543            }
1544            other => panic!("unexpected values {other:?}"),
1545        }
1546        match ia {
1547            Value::Tensor(t) => assert_eq!(t.data, vec![1.0, 2.0]),
1548            other => panic!("unexpected IA {other:?}"),
1549        }
1550        match ic {
1551            Value::Tensor(t) => assert_eq!(t.data, vec![1.0, 2.0, 1.0]),
1552            other => panic!("unexpected IC {other:?}"),
1553        }
1554    }
1555
1556    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1557    #[test]
1558    fn unique_string_rows() {
1559        let array = StringArray::new(
1560            vec![
1561                "alpha".into(),
1562                "alpha".into(),
1563                "gamma".into(),
1564                "beta".into(),
1565                "beta".into(),
1566                "beta".into(),
1567            ],
1568            vec![3, 2],
1569        )
1570        .unwrap();
1571        let eval = evaluate_sync(
1572            Value::StringArray(array),
1573            &[Value::from("rows"), Value::from("stable")],
1574        )
1575        .expect("unique");
1576        let (values, ia, ic) = eval.into_triple();
1577        match values {
1578            Value::StringArray(sa) => {
1579                assert_eq!(sa.shape, vec![2, 2]);
1580                assert_eq!(sa.data, vec!["alpha", "gamma", "beta", "beta"]);
1581            }
1582            other => panic!("unexpected values {other:?}"),
1583        }
1584        match ia {
1585            Value::Tensor(t) => assert_eq!(t.data, vec![1.0, 3.0]),
1586            other => panic!("unexpected IA {other:?}"),
1587        }
1588        match ic {
1589            Value::Tensor(t) => assert_eq!(t.data, vec![1.0, 1.0, 2.0]),
1590            other => panic!("unexpected IC {other:?}"),
1591        }
1592    }
1593
1594    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1595    #[test]
1596    fn unique_complex_sorted() {
1597        let tensor = ComplexTensor::new(
1598            vec![(1.0, 1.0), (0.0, 2.0), (1.0, -1.0), (0.0, 2.0)],
1599            vec![4, 1],
1600        )
1601        .unwrap();
1602        let eval = evaluate_sync(Value::ComplexTensor(tensor), &[]).expect("unique");
1603        let (values, ..) = eval.into_triple();
1604        match values {
1605            Value::ComplexTensor(t) => {
1606                assert_eq!(t.data.len(), 3);
1607                assert_eq!(t.data[0], (1.0, -1.0));
1608                assert_eq!(t.data[1], (1.0, 1.0));
1609                assert_eq!(t.data[2], (0.0, 2.0));
1610            }
1611            other => panic!("unexpected values {other:?}"),
1612        }
1613    }
1614
1615    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1616    #[test]
1617    fn unique_handles_logical_arrays() {
1618        let logical = LogicalArray::new(vec![1, 0, 1, 1], vec![4, 1]).unwrap();
1619        let eval = evaluate_sync(Value::LogicalArray(logical), &[]).expect("unique");
1620        let values = eval.into_values_value();
1621        match values {
1622            Value::Tensor(t) => assert_eq!(t.data, vec![0.0, 1.0]),
1623            other => panic!("unexpected values {other:?}"),
1624        }
1625    }
1626
1627    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1628    #[test]
1629    fn unique_gpu_roundtrip() {
1630        test_support::with_test_provider(|provider| {
1631            let tensor = Tensor::new(vec![5.0, 3.0, 5.0, 1.0], vec![4, 1]).unwrap();
1632            let view = runmat_accelerate_api::HostTensorView {
1633                data: &tensor.data,
1634                shape: &tensor.shape,
1635            };
1636            let handle = provider.upload(&view).expect("upload");
1637            let eval =
1638                evaluate_sync(Value::GpuTensor(handle), &[Value::from("stable")]).expect("unique");
1639            let values = eval.into_values_value();
1640            match values {
1641                Value::Tensor(t) => assert_eq!(t.data, vec![5.0, 3.0, 1.0]),
1642                other => panic!("unexpected values {other:?}"),
1643            }
1644        });
1645    }
1646
1647    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1648    #[test]
1649    #[cfg(feature = "wgpu")]
1650    fn unique_wgpu_matches_cpu() {
1651        let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
1652            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
1653        );
1654        let tensor = Tensor::new(vec![5.0, 3.0, 5.0, 1.0, 2.0], vec![5, 1]).unwrap();
1655        let host_eval = evaluate_sync(Value::Tensor(tensor.clone()), &[]).expect("host unique");
1656        let (host_values, host_ia, host_ic) = host_eval.into_triple();
1657
1658        let provider = runmat_accelerate_api::provider().expect("provider registered");
1659        let view = runmat_accelerate_api::HostTensorView {
1660            data: &tensor.data,
1661            shape: &tensor.shape,
1662        };
1663        let handle = provider.upload(&view).expect("upload");
1664        let gpu_eval = evaluate_sync(Value::GpuTensor(handle.clone()), &[]).expect("gpu unique");
1665        let (gpu_values, gpu_ia, gpu_ic) = gpu_eval.into_triple();
1666        let _ = provider.free(&handle);
1667
1668        let host_values = test_support::gather(host_values).expect("gather host values");
1669        let host_ia = test_support::gather(host_ia).expect("gather host ia");
1670        let host_ic = test_support::gather(host_ic).expect("gather host ic");
1671        let gpu_values = test_support::gather(gpu_values).expect("gather gpu values");
1672        let gpu_ia = test_support::gather(gpu_ia).expect("gather gpu ia");
1673        let gpu_ic = test_support::gather(gpu_ic).expect("gather gpu ic");
1674
1675        assert_eq!(gpu_values.shape, host_values.shape);
1676        assert_eq!(gpu_values.data, host_values.data);
1677        assert_eq!(gpu_ia.data, host_ia.data);
1678        assert_eq!(gpu_ic.data, host_ic.data);
1679    }
1680
1681    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1682    #[test]
1683    fn unique_rejects_legacy_option() {
1684        let tensor = Tensor::new(vec![1.0, 1.0], vec![2, 1]).unwrap();
1685        let err = error_message(
1686            evaluate_sync(Value::Tensor(tensor), &[Value::from("legacy")]).unwrap_err(),
1687        );
1688        assert!(err.contains("legacy"));
1689    }
1690
1691    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1692    #[test]
1693    fn unique_conflicting_order_flags() {
1694        let tensor = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
1695        let err = error_message(
1696            evaluate_sync(
1697                Value::Tensor(tensor),
1698                &[Value::from("stable"), Value::from("sorted")],
1699            )
1700            .unwrap_err(),
1701        );
1702        assert!(err.contains("stable"));
1703    }
1704
1705    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1706    #[test]
1707    fn unique_conflicting_occurrence_flags() {
1708        let tensor = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
1709        let err = error_message(
1710            evaluate_sync(
1711                Value::Tensor(tensor),
1712                &[Value::from("first"), Value::from("last")],
1713            )
1714            .unwrap_err(),
1715        );
1716        assert!(err.contains("first"));
1717    }
1718
1719    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1720    #[test]
1721    fn unique_rows_requires_two_dimensional_input() {
1722        let tensor = Tensor::new(vec![1.0, 2.0], vec![2, 1, 1]).unwrap();
1723        let err = error_message(
1724            evaluate_sync(Value::Tensor(tensor), &[Value::from("rows")]).unwrap_err(),
1725        );
1726        assert!(err.contains("2-D matrix"));
1727    }
1728
1729    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1730    #[test]
1731    fn unique_handles_empty_rows() {
1732        let tensor = Tensor::new(Vec::new(), vec![0, 3]).unwrap();
1733        let eval = evaluate_sync(Value::Tensor(tensor), &[Value::from("rows")]).expect("unique");
1734        let (values, ia, ic) = eval.into_triple();
1735        match values {
1736            Value::Tensor(t) => {
1737                assert_eq!(t.shape, vec![0, 3]);
1738                assert!(t.data.is_empty());
1739            }
1740            other => panic!("unexpected values {other:?}"),
1741        }
1742        match ia {
1743            Value::Tensor(t) => assert!(t.data.is_empty()),
1744            other => panic!("unexpected IA {other:?}"),
1745        }
1746        match ic {
1747            Value::Tensor(t) => assert!(t.data.is_empty()),
1748            other => panic!("unexpected IC {other:?}"),
1749        }
1750    }
1751
1752    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1753    #[test]
1754    fn unique_accepts_integer_scalars() {
1755        let eval = evaluate_sync(Value::Int(IntValue::I32(42)), &[]).expect("unique");
1756        let values = eval.into_values_value();
1757        match values {
1758            Value::Num(n) => assert_eq!(n, 42.0),
1759            other => panic!("unexpected values {other:?}"),
1760        }
1761    }
1762}