Skip to main content

runmat_runtime/builtins/array/sorting_sets/
union.rs

1//! MATLAB-compatible `union` builtin with GPU-aware semantics for RunMat.
2//!
3//! Handles element-wise and row-wise unions with optional stable ordering and
4//! index outputs that mirror MathWorks MATLAB semantics. GPU tensors use a
5//! provider hook or typed host fallback, then public outputs are restored to the
6//! owning provider.
7
8use std::cmp::Ordering;
9use std::collections::{hash_map::Entry, HashMap};
10
11use runmat_accelerate_api::{GpuTensorHandle, UnionOptions, UnionOrder, UnionResult};
12use runmat_builtins::{
13    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinIntegerBackendRule,
14    BuiltinIntegerCapabilityDescriptor, BuiltinIntegerComputationDomain,
15    BuiltinIntegerOutputClassRule, BuiltinIntegerOverflowRule, BuiltinIntegerOverloadKind,
16    BuiltinOutputMode, BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType,
17    BuiltinSignatureDescriptor,
18};
19use runmat_macros::runtime_builtin;
20use runmat_value::{
21    CharArray, ComplexStorage, ComplexTensor, IntValue, IntegerStorage, NumericDType,
22    NumericStorage, StringArray, Tensor, Value,
23};
24
25use super::{float_order::SetFloat, integer_order, type_resolvers::set_values_output_type};
26use crate::build_runtime_error;
27use crate::builtins::common::arg_tokens::tokens_from_values;
28use crate::builtins::common::gpu_helpers;
29use crate::builtins::common::random_args::complex_tensor_into_value;
30use crate::builtins::common::spec::{
31    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
32    ProviderHook, ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
33};
34use crate::builtins::common::tensor;
35use crate::builtins::math::elementwise::integer_cast::IntegerTarget;
36
37#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::array::sorting_sets::union")]
38pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
39    name: "union",
40    op_kind: GpuOpKind::Custom("union"),
41    supported_precisions: &[ScalarType::F32, ScalarType::F64],
42    broadcast: BroadcastSemantics::None,
43    provider_hooks: &[ProviderHook::Custom("union")],
44    constant_strategy: ConstantStrategy::InlineLiteral,
45    residency: ResidencyPolicy::NewHandle,
46    nan_mode: ReductionNaN::Include,
47    two_pass_threshold: None,
48    workgroup_size: None,
49    accepts_nan_mode: true,
50    notes: "Providers may expose a dedicated union hook; exact typed fallback gathers when needed and restores union values plus double indices to the input owner.",
51};
52
53#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::array::sorting_sets::union")]
54pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
55    name: "union",
56    shape: ShapeRequirements::Any,
57    constant_strategy: ConstantStrategy::InlineLiteral,
58    elementwise: None,
59    reduction: None,
60    emits_nan: true,
61    notes: "`union` terminates fusion chains and materialises results on the host; upstream tensors are gathered when necessary.",
62};
63
64const BUILTIN_NAME: &str = "union";
65
66const UNION_OUTPUT_C: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
67    name: "C",
68    ty: BuiltinParamType::Any,
69    arity: BuiltinParamArity::Required,
70    default: None,
71    description: "Union values or rows.",
72}];
73
74const UNION_OUTPUT_C_IA: [BuiltinParamDescriptor; 2] = [
75    BuiltinParamDescriptor {
76        name: "C",
77        ty: BuiltinParamType::Any,
78        arity: BuiltinParamArity::Required,
79        default: None,
80        description: "Union values or rows.",
81    },
82    BuiltinParamDescriptor {
83        name: "ia",
84        ty: BuiltinParamType::NumericArray,
85        arity: BuiltinParamArity::Required,
86        default: None,
87        description: "Indices selecting contributions from A.",
88    },
89];
90
91const UNION_OUTPUT_C_IA_IB: [BuiltinParamDescriptor; 3] = [
92    BuiltinParamDescriptor {
93        name: "C",
94        ty: BuiltinParamType::Any,
95        arity: BuiltinParamArity::Required,
96        default: None,
97        description: "Union values or rows.",
98    },
99    BuiltinParamDescriptor {
100        name: "ia",
101        ty: BuiltinParamType::NumericArray,
102        arity: BuiltinParamArity::Required,
103        default: None,
104        description: "Indices selecting contributions from A.",
105    },
106    BuiltinParamDescriptor {
107        name: "ib",
108        ty: BuiltinParamType::NumericArray,
109        arity: BuiltinParamArity::Required,
110        default: None,
111        description: "Indices selecting contributions from B.",
112    },
113];
114
115const UNION_INPUTS_A_B: [BuiltinParamDescriptor; 2] = [
116    BuiltinParamDescriptor {
117        name: "A",
118        ty: BuiltinParamType::Any,
119        arity: BuiltinParamArity::Required,
120        default: None,
121        description: "First input array.",
122    },
123    BuiltinParamDescriptor {
124        name: "B",
125        ty: BuiltinParamType::Any,
126        arity: BuiltinParamArity::Required,
127        default: None,
128        description: "Second input array.",
129    },
130];
131
132const UNION_INPUTS_A_B_OPTIONS: [BuiltinParamDescriptor; 3] = [
133    BuiltinParamDescriptor {
134        name: "A",
135        ty: BuiltinParamType::Any,
136        arity: BuiltinParamArity::Required,
137        default: None,
138        description: "First input array.",
139    },
140    BuiltinParamDescriptor {
141        name: "B",
142        ty: BuiltinParamType::Any,
143        arity: BuiltinParamArity::Required,
144        default: None,
145        description: "Second input array.",
146    },
147    BuiltinParamDescriptor {
148        name: "option",
149        ty: BuiltinParamType::StringScalar,
150        arity: BuiltinParamArity::Variadic,
151        default: None,
152        description: "Option tokens: 'rows'|'sorted'|'stable'.",
153    },
154];
155
156const UNION_SIGNATURES: [BuiltinSignatureDescriptor; 6] = [
157    BuiltinSignatureDescriptor {
158        label: "C = union(A, B)",
159        inputs: &UNION_INPUTS_A_B,
160        outputs: &UNION_OUTPUT_C,
161    },
162    BuiltinSignatureDescriptor {
163        label: "C = union(A, B, option...)",
164        inputs: &UNION_INPUTS_A_B_OPTIONS,
165        outputs: &UNION_OUTPUT_C,
166    },
167    BuiltinSignatureDescriptor {
168        label: "[C, ia] = union(A, B)",
169        inputs: &UNION_INPUTS_A_B,
170        outputs: &UNION_OUTPUT_C_IA,
171    },
172    BuiltinSignatureDescriptor {
173        label: "[C, ia] = union(A, B, option...)",
174        inputs: &UNION_INPUTS_A_B_OPTIONS,
175        outputs: &UNION_OUTPUT_C_IA,
176    },
177    BuiltinSignatureDescriptor {
178        label: "[C, ia, ib] = union(A, B)",
179        inputs: &UNION_INPUTS_A_B,
180        outputs: &UNION_OUTPUT_C_IA_IB,
181    },
182    BuiltinSignatureDescriptor {
183        label: "[C, ia, ib] = union(A, B, option...)",
184        inputs: &UNION_INPUTS_A_B_OPTIONS,
185        outputs: &UNION_OUTPUT_C_IA_IB,
186    },
187];
188
189const UNION_ERROR_LEGACY_OPTION_UNSUPPORTED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
190    code: "RM.UNION.LEGACY_OPTION_UNSUPPORTED",
191    identifier: Some("RunMat:union:LegacyOptionUnsupported"),
192    when: "Legacy compatibility options are requested.",
193    message: "union: the 'legacy' behaviour is not supported",
194};
195
196const UNION_ERROR_CONFLICTING_ORDER_OPTIONS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
197    code: "RM.UNION.CONFLICTING_ORDER_OPTIONS",
198    identifier: Some("RunMat:union:ConflictingOrderOptions"),
199    when: "Both 'sorted' and 'stable' options are provided.",
200    message: "union: cannot combine 'sorted' with 'stable'",
201};
202
203const UNION_ERROR_UNKNOWN_OPTION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
204    code: "RM.UNION.UNKNOWN_OPTION",
205    identifier: Some("RunMat:union:UnknownOption"),
206    when: "An unsupported option token is provided.",
207    message: "union: unrecognised option",
208};
209
210const UNION_ERROR_ROWS_COLUMN_MISMATCH: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
211    code: "RM.UNION.ROWS_COLUMN_MISMATCH",
212    identifier: Some("RunMat:union:RowsColumnMismatch"),
213    when: "'rows' mode is used and column counts differ.",
214    message: "union: inputs must have the same number of columns when using 'rows'",
215};
216
217const UNION_ERROR_UNSUPPORTED_INPUT_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
218    code: "RM.UNION.UNSUPPORTED_INPUT_TYPE",
219    identifier: Some("RunMat:union:UnsupportedInputType"),
220    when: "Input values cannot be converted into supported union domains.",
221    message: "union: unsupported input type",
222};
223
224const UNION_ERROR_NUMERIC_CLASS_MISMATCH: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
225    code: "RM.UNION.NUMERIC_CLASS_MISMATCH",
226    identifier: Some("RunMat:union:NumericClassMismatch"),
227    when: "Numeric inputs have incompatible nondouble classes.",
228    message: "union: numeric inputs must have the same class, except double may be combined with one nondouble class",
229};
230
231const UNION_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
232    code: "RM.UNION.INVALID_ARGUMENT",
233    identifier: Some("RunMat:union:InvalidArgument"),
234    when: "Option arguments are not string-like where required.",
235    message: "union: expected string option arguments",
236};
237
238const UNION_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
239    code: "RM.UNION.INTERNAL",
240    identifier: Some("RunMat:union:Internal"),
241    when: "Internal conversion/allocation/provider decode fails.",
242    message: "union: internal operation failed",
243};
244
245const UNION_ERRORS: [BuiltinErrorDescriptor; 8] = [
246    UNION_ERROR_LEGACY_OPTION_UNSUPPORTED,
247    UNION_ERROR_CONFLICTING_ORDER_OPTIONS,
248    UNION_ERROR_UNKNOWN_OPTION,
249    UNION_ERROR_ROWS_COLUMN_MISMATCH,
250    UNION_ERROR_UNSUPPORTED_INPUT_TYPE,
251    UNION_ERROR_NUMERIC_CLASS_MISMATCH,
252    UNION_ERROR_INVALID_ARGUMENT,
253    UNION_ERROR_INTERNAL,
254];
255
256const UNION_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 1] =
257    [BuiltinIntegerCapabilityDescriptor {
258        form: "[C, ia, ib] = union(integer_A, integer_B, options)",
259        inputs: &super::BINARY_SET_INTEGER_INPUTS,
260        computation_domain: BuiltinIntegerComputationDomain::ExactInteger,
261        output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
262        overflow: BuiltinIntegerOverflowRule::NotApplicable,
263        backend: BuiltinIntegerBackendRule::GpuRestricted,
264        overload: BuiltinIntegerOverloadKind::Multiple,
265        notes: "C preserves the common nondouble integer class, including when paired with double; ia and ib are one-based double. GPU supports integer classes through 32 bits and restores outputs after typed fallback.",
266    }];
267
268pub const UNION_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
269    signatures: &UNION_SIGNATURES,
270    output_mode: BuiltinOutputMode::ByRequestedOutputCount,
271    completion_policy: BuiltinCompletionPolicy::Public,
272    errors: &UNION_ERRORS,
273};
274
275fn union_error_with(
276    error: &'static BuiltinErrorDescriptor,
277    message: impl Into<String>,
278) -> crate::RuntimeError {
279    let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
280    if let Some(identifier) = error.identifier {
281        builder = builder.with_identifier(identifier);
282    }
283    builder.build()
284}
285
286fn union_error(error: &'static BuiltinErrorDescriptor) -> crate::RuntimeError {
287    union_error_with(error, error.message)
288}
289
290fn union_internal_error(message: impl Into<String>) -> crate::RuntimeError {
291    union_error_with(&UNION_ERROR_INTERNAL, message)
292}
293
294#[runtime_builtin(
295    name = "union",
296    category = "array/sorting_sets",
297    summary = "Return unions of input arrays with ordering and index-output controls.",
298    keywords = "union,set,stable,rows,indices,gpu",
299    accel = "array_construct",
300    sink = true,
301    type_resolver(set_values_output_type),
302    descriptor(crate::builtins::array::sorting_sets::union::UNION_DESCRIPTOR),
303    integer_capabilities(UNION_INTEGER_CAPABILITIES),
304    builtin_path = "crate::builtins::array::sorting_sets::union"
305)]
306async fn union_builtin(a: Value, b: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
307    if matches!(crate::output_count::current_output_count(), Some(n) if n > 3) {
308        return Err(union_error_with(
309            &UNION_ERROR_INVALID_ARGUMENT,
310            "union: too many output arguments; maximum is 3",
311        ));
312    }
313    let provider = super::set_output_provider(&a, &b);
314    let eval = evaluate(a, b, &rest).await?;
315    if let Some(out_count) = crate::output_count::current_output_count() {
316        if out_count == 0 {
317            return Ok(Value::OutputList(Vec::new()));
318        }
319        if out_count == 1 {
320            let outputs = super::restore_set_outputs(
321                provider,
322                BUILTIN_NAME,
323                vec![eval.into_values_value()],
324                union_internal_error,
325            )?;
326            return Ok(Value::OutputList(outputs));
327        }
328        if out_count == 2 {
329            let (values, ia) = eval.into_pair();
330            let outputs = super::restore_set_outputs(
331                provider,
332                BUILTIN_NAME,
333                vec![values, ia],
334                union_internal_error,
335            )?;
336            return Ok(Value::OutputList(outputs));
337        }
338        let (values, ia, ib) = eval.into_triple();
339        let outputs = super::restore_set_outputs(
340            provider,
341            BUILTIN_NAME,
342            vec![values, ia, ib],
343            union_internal_error,
344        )?;
345        return Ok(Value::OutputList(outputs));
346    }
347    let mut outputs = super::restore_set_outputs(
348        provider,
349        BUILTIN_NAME,
350        vec![eval.into_values_value()],
351        union_internal_error,
352    )?;
353    Ok(outputs.pop().expect("union output"))
354}
355
356/// Evaluate the `union` builtin once and expose all outputs.
357pub async fn evaluate(a: Value, b: Value, rest: &[Value]) -> crate::BuiltinResult<UnionEvaluation> {
358    crate::builtins::common::validation::reject_typed_complex_integer(&a, "union")?;
359    crate::builtins::common::validation::reject_typed_complex_integer(&b, "union")?;
360    let opts = parse_options(rest)?;
361    for value in [&a, &b] {
362        if let Value::GpuTensor(handle) = value {
363            if super::is_unsupported_set_gpu_integer(handle) {
364                return Err(union_error_with(
365                    &UNION_ERROR_UNSUPPORTED_INPUT_TYPE,
366                    "union: resident 64-bit integer inputs are not supported",
367                ));
368            }
369        }
370    }
371    match (a, b) {
372        (Value::GpuTensor(handle_a), Value::GpuTensor(handle_b)) => {
373            union_gpu_pair(handle_a, handle_b, &opts).await
374        }
375        (Value::GpuTensor(handle_a), other) => union_gpu_mixed(handle_a, other, &opts, true).await,
376        (other, Value::GpuTensor(handle_b)) => union_gpu_mixed(handle_b, other, &opts, false).await,
377        (left, right) => union_host(left, right, &opts),
378    }
379}
380
381fn parse_options(rest: &[Value]) -> crate::BuiltinResult<UnionOptions> {
382    let mut opts = UnionOptions {
383        rows: false,
384        order: UnionOrder::Sorted,
385    };
386    let mut seen_order: Option<UnionOrder> = None;
387
388    let tokens = tokens_from_values(rest);
389    for (arg, token) in rest.iter().zip(tokens.iter()) {
390        let text = match token {
391            crate::builtins::common::arg_tokens::ArgToken::String(text) => text.as_str(),
392            _ => {
393                let text = tensor::value_to_string(arg)
394                    .ok_or_else(|| union_error(&UNION_ERROR_INVALID_ARGUMENT))?;
395                let lowered = text.trim().to_ascii_lowercase();
396                parse_union_option(&mut opts, &mut seen_order, &lowered)?;
397                continue;
398            }
399        };
400        parse_union_option(&mut opts, &mut seen_order, text)?;
401    }
402
403    Ok(opts)
404}
405
406fn parse_union_option(
407    opts: &mut UnionOptions,
408    seen_order: &mut Option<UnionOrder>,
409    lowered: &str,
410) -> crate::BuiltinResult<()> {
411    match lowered {
412        "rows" => opts.rows = true,
413        "sorted" => {
414            if let Some(prev) = seen_order {
415                if *prev != UnionOrder::Sorted {
416                    return Err(union_error(&UNION_ERROR_CONFLICTING_ORDER_OPTIONS));
417                }
418            }
419            *seen_order = Some(UnionOrder::Sorted);
420            opts.order = UnionOrder::Sorted;
421        }
422        "stable" => {
423            if let Some(prev) = seen_order {
424                if *prev != UnionOrder::Stable {
425                    return Err(union_error(&UNION_ERROR_CONFLICTING_ORDER_OPTIONS));
426                }
427            }
428            *seen_order = Some(UnionOrder::Stable);
429            opts.order = UnionOrder::Stable;
430        }
431        "legacy" | "r2012a" => {
432            return Err(union_error(&UNION_ERROR_LEGACY_OPTION_UNSUPPORTED));
433        }
434        other => {
435            return Err(union_error_with(
436                &UNION_ERROR_UNKNOWN_OPTION,
437                format!("union: unrecognised option '{other}'"),
438            ))
439        }
440    }
441    Ok(())
442}
443
444async fn union_gpu_pair(
445    handle_a: GpuTensorHandle,
446    handle_b: GpuTensorHandle,
447    opts: &UnionOptions,
448) -> crate::BuiltinResult<UnionEvaluation> {
449    if let Some(provider) = runmat_accelerate_api::provider_for_handle(&handle_a)
450        .or_else(runmat_accelerate_api::provider)
451    {
452        match provider.union(&handle_a, &handle_b, opts).await {
453            Ok(result) => return UnionEvaluation::from_union_result(result),
454            Err(_) => {
455                // Fall back to host gather when provider union is unavailable.
456            }
457        }
458    }
459    let tensor_a = gpu_helpers::gather_tensor_async(&handle_a).await?;
460    let tensor_b = gpu_helpers::gather_tensor_async(&handle_b).await?;
461    union_numeric(tensor_a, tensor_b, opts)
462}
463
464async fn union_gpu_mixed(
465    handle_gpu: GpuTensorHandle,
466    other: Value,
467    opts: &UnionOptions,
468    gpu_is_a: bool,
469) -> crate::BuiltinResult<UnionEvaluation> {
470    let tensor_gpu = gpu_helpers::gather_tensor_async(&handle_gpu).await?;
471    let tensor_other =
472        tensor::value_into_tensor_for("union", other).map_err(|e| union_internal_error(e))?;
473    if gpu_is_a {
474        union_numeric(tensor_gpu, tensor_other, opts)
475    } else {
476        union_numeric(tensor_other, tensor_gpu, opts)
477    }
478}
479
480fn union_host(a: Value, b: Value, opts: &UnionOptions) -> crate::BuiltinResult<UnionEvaluation> {
481    match (a, b) {
482        // Complex cases
483        (Value::ComplexTensor(at), Value::ComplexTensor(bt)) => union_complex(at, bt, opts),
484        (Value::ComplexTensor(at), Value::Complex(re, im)) => {
485            let bt = ComplexTensor::new(vec![(re, im)], vec![1, 1])
486                .map_err(|e| union_internal_error(format!("union: {e}")))?;
487            union_complex(at, bt, opts)
488        }
489        (Value::Complex(re, im), Value::ComplexTensor(bt)) => {
490            let at = ComplexTensor::new(vec![(re, im)], vec![1, 1])
491                .map_err(|e| union_internal_error(format!("union: {e}")))?;
492            union_complex(at, bt, opts)
493        }
494        (Value::Complex(a_re, a_im), Value::Complex(b_re, b_im)) => {
495            let at = ComplexTensor::new(vec![(a_re, a_im)], vec![1, 1])
496                .map_err(|e| union_internal_error(format!("union: {e}")))?;
497            let bt = ComplexTensor::new(vec![(b_re, b_im)], vec![1, 1])
498                .map_err(|e| union_internal_error(format!("union: {e}")))?;
499            union_complex(at, bt, opts)
500        }
501
502        // Character arrays
503        (Value::CharArray(ac), Value::CharArray(bc)) => union_char(ac, bc, opts),
504
505        // String arrays / scalars
506        (Value::StringArray(astring), Value::StringArray(bstring)) => {
507            union_string(astring, bstring, opts)
508        }
509        (Value::StringArray(astring), Value::String(b)) => {
510            let bstring = StringArray::new(vec![b], vec![1, 1])
511                .map_err(|e| union_internal_error(format!("union: {e}")))?;
512            union_string(astring, bstring, opts)
513        }
514        (Value::String(a), Value::StringArray(bstring)) => {
515            let astring = StringArray::new(vec![a], vec![1, 1])
516                .map_err(|e| union_internal_error(format!("union: {e}")))?;
517            union_string(astring, bstring, opts)
518        }
519        (Value::String(a), Value::String(b)) => {
520            let astring = StringArray::new(vec![a], vec![1, 1])
521                .map_err(|e| union_internal_error(format!("union: {e}")))?;
522            let bstring = StringArray::new(vec![b], vec![1, 1])
523                .map_err(|e| union_internal_error(format!("union: {e}")))?;
524            union_string(astring, bstring, opts)
525        }
526
527        // Fallback to numeric (includes tensors, logical arrays, ints, bools, doubles)
528        (left, right) => {
529            let tensor_a = tensor::value_into_tensor_for("union", left)
530                .map_err(|e| union_error_with(&UNION_ERROR_UNSUPPORTED_INPUT_TYPE, e))?;
531            let tensor_b = tensor::value_into_tensor_for("union", right)
532                .map_err(|e| union_error_with(&UNION_ERROR_UNSUPPORTED_INPUT_TYPE, e))?;
533            union_numeric(tensor_a, tensor_b, opts)
534        }
535    }
536}
537
538fn union_numeric(
539    a: Tensor,
540    b: Tensor,
541    opts: &UnionOptions,
542) -> crate::BuiltinResult<UnionEvaluation> {
543    let a_dtype = a.numeric_dtype();
544    let b_dtype = b.numeric_dtype();
545    if let (Some(a_storage), Some(b_storage)) = (a.integer_storage(), b.integer_storage()) {
546        if a_storage.class_name() == b_storage.class_name() {
547            return if opts.rows {
548                union_integer_rows(a_storage, a.shape.clone(), b_storage, b.shape.clone(), opts)
549            } else {
550                union_integer_elements(a_storage, b_storage, opts)
551            };
552        }
553        return Err(union_error(&UNION_ERROR_NUMERIC_CLASS_MISMATCH));
554    }
555    match (a.integer_storage(), b.integer_storage()) {
556        (Some(storage), None) if b_dtype == NumericDType::F64 => {
557            let target = IntegerTarget::from_storage(storage);
558            let b = target.cast_tensor(b).map_err(union_internal_error)?;
559            return union_numeric(a, b, opts);
560        }
561        (None, Some(storage)) if a_dtype == NumericDType::F64 => {
562            let target = IntegerTarget::from_storage(storage);
563            let a = target.cast_tensor(a).map_err(union_internal_error)?;
564            return union_numeric(a, b, opts);
565        }
566        _ => {}
567    }
568    if a_dtype != b_dtype && a_dtype != NumericDType::F64 && b_dtype != NumericDType::F64 {
569        return Err(union_error(&UNION_ERROR_NUMERIC_CLASS_MISMATCH));
570    }
571    let a_shape = a.shape.clone();
572    let b_shape = b.shape.clone();
573    let a_storage = a
574        .into_numeric_storage()
575        .map_err(|e| union_internal_error(format!("union: {e}")))?;
576    let b_storage = b
577        .into_numeric_storage()
578        .map_err(|e| union_internal_error(format!("union: {e}")))?;
579    match (a_storage, b_storage) {
580        (NumericStorage::F64(a), NumericStorage::F64(b)) => {
581            union_floating(a, a_shape, b, b_shape, opts)
582        }
583        (NumericStorage::F32(a), NumericStorage::F32(b)) => {
584            union_floating(a, a_shape, b, b_shape, opts)
585        }
586        (a, b) => union_promoted_f64(a, a_shape, b, b_shape, opts),
587    }
588}
589
590fn union_promoted_f64(
591    a: NumericStorage,
592    a_shape: Vec<usize>,
593    b: NumericStorage,
594    b_shape: Vec<usize>,
595    opts: &UnionOptions,
596) -> crate::BuiltinResult<UnionEvaluation> {
597    union_floating(
598        a.materialize_f64(),
599        a_shape,
600        b.materialize_f64(),
601        b_shape,
602        opts,
603    )
604}
605
606fn union_floating<T: SetFloat>(
607    a: Vec<T>,
608    a_shape: Vec<usize>,
609    b: Vec<T>,
610    b_shape: Vec<usize>,
611    opts: &UnionOptions,
612) -> crate::BuiltinResult<UnionEvaluation> {
613    if opts.rows {
614        union_floating_rows(a, a_shape, b, b_shape, opts)
615    } else {
616        union_floating_elements(a, b, opts)
617    }
618}
619
620/// Helper exposed for acceleration providers handling numeric tensors entirely on the host.
621pub fn union_numeric_from_tensors(
622    a: Tensor,
623    b: Tensor,
624    opts: &UnionOptions,
625) -> crate::BuiltinResult<UnionEvaluation> {
626    union_numeric(a, b, opts)
627}
628
629fn union_integer_elements(
630    a: &IntegerStorage,
631    b: &IntegerStorage,
632    opts: &UnionOptions,
633) -> crate::BuiltinResult<UnionEvaluation> {
634    let mut entries = Vec::<IntegerUnionEntry>::new();
635    let mut map = HashMap::<IntValue, usize>::new();
636    for (index, value) in a.exact_values().into_iter().enumerate() {
637        if map.contains_key(&value) {
638            continue;
639        }
640        let entry_index = entries.len();
641        entries.push(IntegerUnionEntry {
642            value: value.clone(),
643            a_index: Some(index),
644            b_index: None,
645            order_rank: entry_index,
646        });
647        map.insert(value, entry_index);
648    }
649    for (index, value) in b.exact_values().into_iter().enumerate() {
650        if map.contains_key(&value) {
651            continue;
652        }
653        let entry_index = entries.len();
654        entries.push(IntegerUnionEntry {
655            value: value.clone(),
656            a_index: None,
657            b_index: Some(index),
658            order_rank: entry_index,
659        });
660        map.insert(value, entry_index);
661    }
662    assemble_integer_union(entries, a, opts)
663}
664
665fn union_integer_rows(
666    a_storage: &IntegerStorage,
667    a_shape: Vec<usize>,
668    b_storage: &IntegerStorage,
669    b_shape: Vec<usize>,
670    opts: &UnionOptions,
671) -> crate::BuiltinResult<UnionEvaluation> {
672    if a_shape.len() != 2 || b_shape.len() != 2 {
673        return Err(union_internal_error(
674            "union: 'rows' option requires 2-D numeric matrices",
675        ));
676    }
677    if a_shape[1] != b_shape[1] {
678        return Err(union_error(&UNION_ERROR_ROWS_COLUMN_MISMATCH));
679    }
680    let (rows_a, rows_b, cols) = (a_shape[0], b_shape[0], a_shape[1]);
681    let a_values = a_storage.exact_values();
682    let b_values = b_storage.exact_values();
683    let mut entries = Vec::<IntegerRowUnionEntry>::new();
684    let mut map = HashMap::<Vec<IntValue>, usize>::new();
685    for row in 0..rows_a {
686        let row_data: Vec<_> = (0..cols)
687            .map(|col| a_values[row + col * rows_a].clone())
688            .collect();
689        if map.contains_key(&row_data) {
690            continue;
691        }
692        let entry_index = entries.len();
693        entries.push(IntegerRowUnionEntry {
694            row_data: row_data.clone(),
695            a_row: Some(row),
696            b_row: None,
697            order_rank: entry_index,
698        });
699        map.insert(row_data, entry_index);
700    }
701    for row in 0..rows_b {
702        let row_data: Vec<_> = (0..cols)
703            .map(|col| b_values[row + col * rows_b].clone())
704            .collect();
705        if map.contains_key(&row_data) {
706            continue;
707        }
708        let entry_index = entries.len();
709        entries.push(IntegerRowUnionEntry {
710            row_data: row_data.clone(),
711            a_row: None,
712            b_row: Some(row),
713            order_rank: entry_index,
714        });
715        map.insert(row_data, entry_index);
716    }
717    assemble_integer_row_union(entries, a_storage, opts, cols)
718}
719
720fn union_floating_elements<T: SetFloat>(
721    a_values: Vec<T>,
722    b_values: Vec<T>,
723    opts: &UnionOptions,
724) -> crate::BuiltinResult<UnionEvaluation> {
725    let mut entries = Vec::<FloatingUnionEntry<T>>::new();
726    let mut map: HashMap<u64, usize> = HashMap::new();
727    let mut order_counter = 0usize;
728
729    for (idx, &value) in a_values.iter().enumerate() {
730        let key = value.canonical_key();
731        match map.entry(key) {
732            Entry::Occupied(_) => {
733                // Already recorded from A; keep first occurrence only.
734            }
735            Entry::Vacant(v) => {
736                let entry_idx = entries.len();
737                entries.push(FloatingUnionEntry {
738                    value,
739                    a_index: Some(idx),
740                    b_index: None,
741                    order_rank: order_counter,
742                });
743                v.insert(entry_idx);
744                order_counter += 1;
745            }
746        }
747    }
748
749    for (idx, &value) in b_values.iter().enumerate() {
750        let key = value.canonical_key();
751        match map.entry(key) {
752            Entry::Occupied(occ) => {
753                let entry = &mut entries[*occ.get()];
754                if entry.a_index.is_none() && entry.b_index.is_none() {
755                    entry.b_index = Some(idx);
756                }
757            }
758            Entry::Vacant(v) => {
759                let entry_idx = entries.len();
760                entries.push(FloatingUnionEntry {
761                    value,
762                    a_index: None,
763                    b_index: Some(idx),
764                    order_rank: order_counter,
765                });
766                v.insert(entry_idx);
767                order_counter += 1;
768            }
769        }
770    }
771
772    assemble_floating_union(entries, opts)
773}
774
775fn union_floating_rows<T: SetFloat>(
776    a_values: Vec<T>,
777    a_shape: Vec<usize>,
778    b_values: Vec<T>,
779    b_shape: Vec<usize>,
780    opts: &UnionOptions,
781) -> crate::BuiltinResult<UnionEvaluation> {
782    if a_shape.len() != 2 || b_shape.len() != 2 {
783        return Err(union_internal_error(
784            "union: 'rows' option requires 2-D numeric matrices",
785        ));
786    }
787    if a_shape[1] != b_shape[1] {
788        return Err(union_error_with(
789            &UNION_ERROR_ROWS_COLUMN_MISMATCH,
790            UNION_ERROR_ROWS_COLUMN_MISMATCH.message,
791        ));
792    }
793    let rows_a = a_shape[0];
794    let cols = a_shape[1];
795    let rows_b = b_shape[0];
796
797    let mut entries = Vec::<FloatingRowUnionEntry<T>>::new();
798    let mut map: HashMap<FloatingRowKey, usize> = HashMap::new();
799    let mut order_counter = 0usize;
800
801    for r in 0..rows_a {
802        let mut row_values = Vec::with_capacity(cols);
803        for c in 0..cols {
804            let idx = r + c * rows_a;
805            row_values.push(a_values[idx]);
806        }
807        let key = FloatingRowKey::from_slice(&row_values);
808        match map.entry(key) {
809            Entry::Occupied(_) => {}
810            Entry::Vacant(v) => {
811                let entry_idx = entries.len();
812                entries.push(FloatingRowUnionEntry {
813                    row_data: row_values,
814                    a_row: Some(r),
815                    b_row: None,
816                    order_rank: order_counter,
817                });
818                v.insert(entry_idx);
819                order_counter += 1;
820            }
821        }
822    }
823
824    for r in 0..rows_b {
825        let mut row_values = Vec::with_capacity(cols);
826        for c in 0..cols {
827            let idx = r + c * rows_b;
828            row_values.push(b_values[idx]);
829        }
830        let key = FloatingRowKey::from_slice(&row_values);
831        match map.entry(key) {
832            Entry::Occupied(occ) => {
833                let entry = &mut entries[*occ.get()];
834                if entry.a_row.is_none() && entry.b_row.is_none() {
835                    entry.b_row = Some(r);
836                }
837            }
838            Entry::Vacant(v) => {
839                let entry_idx = entries.len();
840                entries.push(FloatingRowUnionEntry {
841                    row_data: row_values,
842                    a_row: None,
843                    b_row: Some(r),
844                    order_rank: order_counter,
845                });
846                v.insert(entry_idx);
847                order_counter += 1;
848            }
849        }
850    }
851
852    assemble_floating_row_union(entries, opts, cols)
853}
854
855fn union_complex(
856    a: ComplexTensor,
857    b: ComplexTensor,
858    opts: &UnionOptions,
859) -> crate::BuiltinResult<UnionEvaluation> {
860    let a_shape = a.shape.clone();
861    let b_shape = b.shape.clone();
862    match (a.into_complex_storage(), b.into_complex_storage()) {
863        (ComplexStorage::F64(a), ComplexStorage::F64(b)) => {
864            union_floating_complex(a, a_shape, b, b_shape, opts)
865        }
866        (ComplexStorage::F32(a), ComplexStorage::F32(b)) => {
867            union_floating_complex(a, a_shape, b, b_shape, opts)
868        }
869        (a, b) => union_promoted_complex_f64(a, a_shape, b, b_shape, opts),
870    }
871}
872
873fn union_promoted_complex_f64(
874    a: ComplexStorage,
875    a_shape: Vec<usize>,
876    b: ComplexStorage,
877    b_shape: Vec<usize>,
878    opts: &UnionOptions,
879) -> crate::BuiltinResult<UnionEvaluation> {
880    union_floating_complex(
881        a.materialize_f64(),
882        a_shape,
883        b.materialize_f64(),
884        b_shape,
885        opts,
886    )
887}
888
889fn union_floating_complex<T: SetFloat>(
890    a: Vec<(T, T)>,
891    a_shape: Vec<usize>,
892    b: Vec<(T, T)>,
893    b_shape: Vec<usize>,
894    opts: &UnionOptions,
895) -> crate::BuiltinResult<UnionEvaluation> {
896    if opts.rows {
897        union_complex_rows(a, a_shape, b, b_shape, opts)
898    } else {
899        union_complex_elements(a, b, opts)
900    }
901}
902
903fn union_complex_elements<T: SetFloat>(
904    a: Vec<(T, T)>,
905    b: Vec<(T, T)>,
906    opts: &UnionOptions,
907) -> crate::BuiltinResult<UnionEvaluation> {
908    let mut entries = Vec::<ComplexUnionEntry<T>>::new();
909    let mut map: HashMap<ComplexKey, usize> = HashMap::new();
910    let mut order_counter = 0usize;
911
912    for (idx, &value) in a.iter().enumerate() {
913        let key = ComplexKey::new(value);
914        match map.entry(key) {
915            Entry::Occupied(_) => {}
916            Entry::Vacant(v) => {
917                let entry_idx = entries.len();
918                entries.push(ComplexUnionEntry {
919                    value,
920                    a_index: Some(idx),
921                    b_index: None,
922                    order_rank: order_counter,
923                });
924                v.insert(entry_idx);
925                order_counter += 1;
926            }
927        }
928    }
929
930    for (idx, &value) in b.iter().enumerate() {
931        let key = ComplexKey::new(value);
932        match map.entry(key) {
933            Entry::Occupied(occ) => {
934                let entry = &mut entries[*occ.get()];
935                if entry.a_index.is_none() && entry.b_index.is_none() {
936                    entry.b_index = Some(idx);
937                }
938            }
939            Entry::Vacant(v) => {
940                let entry_idx = entries.len();
941                entries.push(ComplexUnionEntry {
942                    value,
943                    a_index: None,
944                    b_index: Some(idx),
945                    order_rank: order_counter,
946                });
947                v.insert(entry_idx);
948                order_counter += 1;
949            }
950        }
951    }
952
953    assemble_complex_union(entries, opts)
954}
955
956fn union_complex_rows<T: SetFloat>(
957    a: Vec<(T, T)>,
958    a_shape: Vec<usize>,
959    b: Vec<(T, T)>,
960    b_shape: Vec<usize>,
961    opts: &UnionOptions,
962) -> crate::BuiltinResult<UnionEvaluation> {
963    if a_shape.len() != 2 || b_shape.len() != 2 {
964        return Err(union_internal_error(
965            "union: 'rows' option requires 2-D complex matrices",
966        ));
967    }
968    if a_shape[1] != b_shape[1] {
969        return Err(union_error_with(
970            &UNION_ERROR_ROWS_COLUMN_MISMATCH,
971            UNION_ERROR_ROWS_COLUMN_MISMATCH.message,
972        ));
973    }
974    let rows_a = a_shape[0];
975    let cols = a_shape[1];
976    let rows_b = b_shape[0];
977
978    let mut entries = Vec::<ComplexRowUnionEntry<T>>::new();
979    let mut map: HashMap<Vec<ComplexKey>, usize> = HashMap::new();
980    let mut order_counter = 0usize;
981
982    for r in 0..rows_a {
983        let mut row_values = Vec::with_capacity(cols);
984        let mut key_row = Vec::with_capacity(cols);
985        for c in 0..cols {
986            let idx = r + c * rows_a;
987            let value = a[idx];
988            row_values.push(value);
989            key_row.push(ComplexKey::new(value));
990        }
991        match map.entry(key_row) {
992            Entry::Occupied(_) => {}
993            Entry::Vacant(v) => {
994                let entry_idx = entries.len();
995                entries.push(ComplexRowUnionEntry {
996                    row_data: row_values,
997                    a_row: Some(r),
998                    b_row: None,
999                    order_rank: order_counter,
1000                });
1001                v.insert(entry_idx);
1002                order_counter += 1;
1003            }
1004        }
1005    }
1006
1007    for r in 0..rows_b {
1008        let mut row_values = Vec::with_capacity(cols);
1009        let mut key_row = Vec::with_capacity(cols);
1010        for c in 0..cols {
1011            let idx = r + c * rows_b;
1012            let value = b[idx];
1013            row_values.push(value);
1014            key_row.push(ComplexKey::new(value));
1015        }
1016        match map.entry(key_row) {
1017            Entry::Occupied(occ) => {
1018                let entry = &mut entries[*occ.get()];
1019                if entry.a_row.is_none() && entry.b_row.is_none() {
1020                    entry.b_row = Some(r);
1021                }
1022            }
1023            Entry::Vacant(v) => {
1024                let entry_idx = entries.len();
1025                entries.push(ComplexRowUnionEntry {
1026                    row_data: row_values,
1027                    a_row: None,
1028                    b_row: Some(r),
1029                    order_rank: order_counter,
1030                });
1031                v.insert(entry_idx);
1032                order_counter += 1;
1033            }
1034        }
1035    }
1036
1037    assemble_complex_row_union(entries, opts, cols)
1038}
1039
1040fn union_char(
1041    a: CharArray,
1042    b: CharArray,
1043    opts: &UnionOptions,
1044) -> crate::BuiltinResult<UnionEvaluation> {
1045    if opts.rows {
1046        union_char_rows(a, b, opts)
1047    } else {
1048        union_char_elements(a, b, opts)
1049    }
1050}
1051
1052fn union_char_elements(
1053    a: CharArray,
1054    b: CharArray,
1055    opts: &UnionOptions,
1056) -> crate::BuiltinResult<UnionEvaluation> {
1057    let mut entries = Vec::<CharUnionEntry>::new();
1058    let mut map: HashMap<u32, usize> = HashMap::new();
1059    let mut order_counter = 0usize;
1060
1061    for col in 0..a.cols {
1062        for row in 0..a.rows {
1063            let linear_idx = row + col * a.rows;
1064            let data_idx = row * a.cols + col;
1065            let ch = a.data[data_idx];
1066            let key = ch as u32;
1067            match map.entry(key) {
1068                Entry::Occupied(_) => {}
1069                Entry::Vacant(v) => {
1070                    let entry_idx = entries.len();
1071                    entries.push(CharUnionEntry {
1072                        ch,
1073                        a_index: Some(linear_idx),
1074                        b_index: None,
1075                        order_rank: order_counter,
1076                    });
1077                    v.insert(entry_idx);
1078                    order_counter += 1;
1079                }
1080            }
1081        }
1082    }
1083
1084    for col in 0..b.cols {
1085        for row in 0..b.rows {
1086            let linear_idx = row + col * b.rows;
1087            let data_idx = row * b.cols + col;
1088            let ch = b.data[data_idx];
1089            let key = ch as u32;
1090            match map.entry(key) {
1091                Entry::Occupied(occ) => {
1092                    let entry = &mut entries[*occ.get()];
1093                    if entry.a_index.is_none() && entry.b_index.is_none() {
1094                        entry.b_index = Some(linear_idx);
1095                    }
1096                }
1097                Entry::Vacant(v) => {
1098                    let entry_idx = entries.len();
1099                    entries.push(CharUnionEntry {
1100                        ch,
1101                        a_index: None,
1102                        b_index: Some(linear_idx),
1103                        order_rank: order_counter,
1104                    });
1105                    v.insert(entry_idx);
1106                    order_counter += 1;
1107                }
1108            }
1109        }
1110    }
1111
1112    assemble_char_union(entries, opts)
1113}
1114
1115fn union_char_rows(
1116    a: CharArray,
1117    b: CharArray,
1118    opts: &UnionOptions,
1119) -> crate::BuiltinResult<UnionEvaluation> {
1120    if a.cols != b.cols {
1121        return Err(union_error_with(
1122            &UNION_ERROR_ROWS_COLUMN_MISMATCH,
1123            UNION_ERROR_ROWS_COLUMN_MISMATCH.message,
1124        ));
1125    }
1126    let rows_a = a.rows;
1127    let rows_b = b.rows;
1128    let cols = a.cols;
1129
1130    let mut entries = Vec::<CharRowUnionEntry>::new();
1131    let mut map: HashMap<RowCharKey, usize> = HashMap::new();
1132    let mut order_counter = 0usize;
1133
1134    for r in 0..rows_a {
1135        let mut row_values = Vec::with_capacity(cols);
1136        for c in 0..cols {
1137            let idx = r * cols + c;
1138            row_values.push(a.data[idx]);
1139        }
1140        let key = RowCharKey::from_slice(&row_values);
1141        match map.entry(key) {
1142            Entry::Occupied(_) => {}
1143            Entry::Vacant(v) => {
1144                let entry_idx = entries.len();
1145                entries.push(CharRowUnionEntry {
1146                    row_data: row_values,
1147                    a_row: Some(r),
1148                    b_row: None,
1149                    order_rank: order_counter,
1150                });
1151                v.insert(entry_idx);
1152                order_counter += 1;
1153            }
1154        }
1155    }
1156
1157    for r in 0..rows_b {
1158        let mut row_values = Vec::with_capacity(cols);
1159        for c in 0..cols {
1160            let idx = r * cols + c;
1161            row_values.push(b.data[idx]);
1162        }
1163        let key = RowCharKey::from_slice(&row_values);
1164        match map.entry(key) {
1165            Entry::Occupied(occ) => {
1166                let entry = &mut entries[*occ.get()];
1167                if entry.a_row.is_none() && entry.b_row.is_none() {
1168                    entry.b_row = Some(r);
1169                }
1170            }
1171            Entry::Vacant(v) => {
1172                let entry_idx = entries.len();
1173                entries.push(CharRowUnionEntry {
1174                    row_data: row_values,
1175                    a_row: None,
1176                    b_row: Some(r),
1177                    order_rank: order_counter,
1178                });
1179                v.insert(entry_idx);
1180                order_counter += 1;
1181            }
1182        }
1183    }
1184
1185    assemble_char_row_union(entries, opts, cols)
1186}
1187
1188fn union_string(
1189    a: StringArray,
1190    b: StringArray,
1191    opts: &UnionOptions,
1192) -> crate::BuiltinResult<UnionEvaluation> {
1193    if opts.rows {
1194        union_string_rows(a, b, opts)
1195    } else {
1196        union_string_elements(a, b, opts)
1197    }
1198}
1199
1200fn union_string_elements(
1201    a: StringArray,
1202    b: StringArray,
1203    opts: &UnionOptions,
1204) -> crate::BuiltinResult<UnionEvaluation> {
1205    let mut entries = Vec::<StringUnionEntry>::new();
1206    let mut map: HashMap<String, usize> = HashMap::new();
1207    let mut order_counter = 0usize;
1208
1209    for (idx, value) in a.data.iter().enumerate() {
1210        match map.entry(value.clone()) {
1211            Entry::Occupied(_) => {}
1212            Entry::Vacant(v) => {
1213                let entry_idx = entries.len();
1214                entries.push(StringUnionEntry {
1215                    value: value.clone(),
1216                    a_index: Some(idx),
1217                    b_index: None,
1218                    order_rank: order_counter,
1219                });
1220                v.insert(entry_idx);
1221                order_counter += 1;
1222            }
1223        }
1224    }
1225
1226    for (idx, value) in b.data.iter().enumerate() {
1227        match map.entry(value.clone()) {
1228            Entry::Occupied(occ) => {
1229                let entry = &mut entries[*occ.get()];
1230                if entry.a_index.is_none() && entry.b_index.is_none() {
1231                    entry.b_index = Some(idx);
1232                }
1233            }
1234            Entry::Vacant(v) => {
1235                let entry_idx = entries.len();
1236                entries.push(StringUnionEntry {
1237                    value: value.clone(),
1238                    a_index: None,
1239                    b_index: Some(idx),
1240                    order_rank: order_counter,
1241                });
1242                v.insert(entry_idx);
1243                order_counter += 1;
1244            }
1245        }
1246    }
1247
1248    assemble_string_union(entries, opts)
1249}
1250
1251fn union_string_rows(
1252    a: StringArray,
1253    b: StringArray,
1254    opts: &UnionOptions,
1255) -> crate::BuiltinResult<UnionEvaluation> {
1256    if a.shape.len() != 2 || b.shape.len() != 2 {
1257        return Err(union_internal_error(
1258            "union: 'rows' option requires 2-D string arrays",
1259        ));
1260    }
1261    if a.shape[1] != b.shape[1] {
1262        return Err(union_error_with(
1263            &UNION_ERROR_ROWS_COLUMN_MISMATCH,
1264            UNION_ERROR_ROWS_COLUMN_MISMATCH.message,
1265        ));
1266    }
1267    let rows_a = a.shape[0];
1268    let cols = a.shape[1];
1269    let rows_b = b.shape[0];
1270
1271    let mut entries = Vec::<StringRowUnionEntry>::new();
1272    let mut map: HashMap<RowStringKey, usize> = HashMap::new();
1273    let mut order_counter = 0usize;
1274
1275    for r in 0..rows_a {
1276        let mut row_values = Vec::with_capacity(cols);
1277        for c in 0..cols {
1278            let idx = r + c * rows_a;
1279            row_values.push(a.data[idx].clone());
1280        }
1281        let key = RowStringKey(row_values.clone());
1282        match map.entry(key) {
1283            Entry::Occupied(_) => {}
1284            Entry::Vacant(v) => {
1285                let entry_idx = entries.len();
1286                entries.push(StringRowUnionEntry {
1287                    row_data: row_values,
1288                    a_row: Some(r),
1289                    b_row: None,
1290                    order_rank: order_counter,
1291                });
1292                v.insert(entry_idx);
1293                order_counter += 1;
1294            }
1295        }
1296    }
1297
1298    for r in 0..rows_b {
1299        let mut row_values = Vec::with_capacity(cols);
1300        for c in 0..cols {
1301            let idx = r + c * rows_b;
1302            row_values.push(b.data[idx].clone());
1303        }
1304        let key = RowStringKey(row_values.clone());
1305        match map.entry(key) {
1306            Entry::Occupied(occ) => {
1307                let entry = &mut entries[*occ.get()];
1308                if entry.a_row.is_none() && entry.b_row.is_none() {
1309                    entry.b_row = Some(r);
1310                }
1311            }
1312            Entry::Vacant(v) => {
1313                let entry_idx = entries.len();
1314                entries.push(StringRowUnionEntry {
1315                    row_data: row_values,
1316                    a_row: None,
1317                    b_row: Some(r),
1318                    order_rank: order_counter,
1319                });
1320                v.insert(entry_idx);
1321                order_counter += 1;
1322            }
1323        }
1324    }
1325
1326    assemble_string_row_union(entries, opts, cols)
1327}
1328
1329#[derive(Debug, Clone)]
1330pub struct UnionEvaluation {
1331    values: Value,
1332    ia: Tensor,
1333    ib: Tensor,
1334}
1335
1336impl UnionEvaluation {
1337    fn new(values: Value, ia: Tensor, ib: Tensor) -> Self {
1338        Self { values, ia, ib }
1339    }
1340
1341    pub fn from_union_result(result: UnionResult) -> crate::BuiltinResult<Self> {
1342        let UnionResult { values, ia, ib } = result;
1343        let values_tensor = Tensor::new(values.data, values.shape)
1344            .map_err(|e| union_internal_error(format!("union: {e}")))?;
1345        let ia_tensor = Tensor::new(ia.data, ia.shape)
1346            .map_err(|e| union_internal_error(format!("union: {e}")))?;
1347        let ib_tensor = Tensor::new(ib.data, ib.shape)
1348            .map_err(|e| union_internal_error(format!("union: {e}")))?;
1349        Ok(UnionEvaluation::new(
1350            tensor::tensor_into_value(values_tensor),
1351            ia_tensor,
1352            ib_tensor,
1353        ))
1354    }
1355
1356    pub fn into_numeric_union_result(self) -> crate::BuiltinResult<UnionResult> {
1357        let UnionEvaluation { values, ia, ib } = self;
1358        let values_tensor =
1359            tensor::value_into_tensor_for("union", values).map_err(|e| union_internal_error(e))?;
1360        Ok(UnionResult {
1361            values: tensor::tensor_into_host_f64_owned(values_tensor),
1362            ia: tensor::tensor_into_host_f64_owned(ia),
1363            ib: tensor::tensor_into_host_f64_owned(ib),
1364        })
1365    }
1366
1367    pub fn into_values_value(self) -> Value {
1368        self.values
1369    }
1370
1371    pub fn into_pair(self) -> (Value, Value) {
1372        let ia = tensor::tensor_into_value(self.ia);
1373        (self.values, ia)
1374    }
1375
1376    pub fn into_triple(self) -> (Value, Value, Value) {
1377        let ia = tensor::tensor_into_value(self.ia);
1378        let ib = tensor::tensor_into_value(self.ib);
1379        (self.values, ia, ib)
1380    }
1381
1382    pub fn values_value(&self) -> Value {
1383        self.values.clone()
1384    }
1385
1386    pub fn ia_value(&self) -> Value {
1387        tensor::tensor_into_value(self.ia.clone())
1388    }
1389
1390    pub fn ib_value(&self) -> Value {
1391        tensor::tensor_into_value(self.ib.clone())
1392    }
1393}
1394
1395#[derive(Debug)]
1396struct FloatingUnionEntry<T> {
1397    value: T,
1398    a_index: Option<usize>,
1399    b_index: Option<usize>,
1400    order_rank: usize,
1401}
1402
1403#[derive(Debug)]
1404struct IntegerUnionEntry {
1405    value: IntValue,
1406    a_index: Option<usize>,
1407    b_index: Option<usize>,
1408    order_rank: usize,
1409}
1410
1411#[derive(Debug)]
1412struct FloatingRowUnionEntry<T> {
1413    row_data: Vec<T>,
1414    a_row: Option<usize>,
1415    b_row: Option<usize>,
1416    order_rank: usize,
1417}
1418
1419#[derive(Debug)]
1420struct IntegerRowUnionEntry {
1421    row_data: Vec<IntValue>,
1422    a_row: Option<usize>,
1423    b_row: Option<usize>,
1424    order_rank: usize,
1425}
1426
1427#[derive(Debug)]
1428struct ComplexUnionEntry<T> {
1429    value: (T, T),
1430    a_index: Option<usize>,
1431    b_index: Option<usize>,
1432    order_rank: usize,
1433}
1434
1435#[derive(Debug)]
1436struct ComplexRowUnionEntry<T> {
1437    row_data: Vec<(T, T)>,
1438    a_row: Option<usize>,
1439    b_row: Option<usize>,
1440    order_rank: usize,
1441}
1442
1443#[derive(Debug)]
1444struct CharUnionEntry {
1445    ch: char,
1446    a_index: Option<usize>,
1447    b_index: Option<usize>,
1448    order_rank: usize,
1449}
1450
1451#[derive(Debug)]
1452struct CharRowUnionEntry {
1453    row_data: Vec<char>,
1454    a_row: Option<usize>,
1455    b_row: Option<usize>,
1456    order_rank: usize,
1457}
1458
1459#[derive(Debug)]
1460struct StringUnionEntry {
1461    value: String,
1462    a_index: Option<usize>,
1463    b_index: Option<usize>,
1464    order_rank: usize,
1465}
1466
1467#[derive(Debug)]
1468struct StringRowUnionEntry {
1469    row_data: Vec<String>,
1470    a_row: Option<usize>,
1471    b_row: Option<usize>,
1472    order_rank: usize,
1473}
1474
1475#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1476struct FloatingRowKey(Vec<u64>);
1477
1478impl FloatingRowKey {
1479    fn from_slice<T: SetFloat>(values: &[T]) -> Self {
1480        Self(values.iter().map(|&value| value.canonical_key()).collect())
1481    }
1482}
1483
1484#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1485struct ComplexKey {
1486    re: u64,
1487    im: u64,
1488}
1489
1490impl ComplexKey {
1491    fn new<T: SetFloat>(value: (T, T)) -> Self {
1492        Self {
1493            re: value.0.canonical_key(),
1494            im: value.1.canonical_key(),
1495        }
1496    }
1497}
1498
1499#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1500struct RowCharKey(Vec<u32>);
1501
1502impl RowCharKey {
1503    fn from_slice(values: &[char]) -> Self {
1504        RowCharKey(values.iter().map(|&ch| ch as u32).collect())
1505    }
1506}
1507
1508#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1509struct RowStringKey(Vec<String>);
1510
1511fn assemble_floating_union<T: SetFloat>(
1512    entries: Vec<FloatingUnionEntry<T>>,
1513    opts: &UnionOptions,
1514) -> crate::BuiltinResult<UnionEvaluation> {
1515    let mut order: Vec<usize> = (0..entries.len()).collect();
1516    match opts.order {
1517        UnionOrder::Sorted => {
1518            order.sort_by(|&lhs, &rhs| entries[lhs].value.compare(entries[rhs].value));
1519        }
1520        UnionOrder::Stable => {
1521            order.sort_by_key(|&idx| entries[idx].order_rank);
1522        }
1523    }
1524
1525    let mut values = Vec::with_capacity(order.len());
1526    let mut ia = Vec::new();
1527    let mut ib = Vec::new();
1528    for &idx in &order {
1529        let entry = &entries[idx];
1530        values.push(entry.value);
1531        if let Some(a_idx) = entry.a_index {
1532            ia.push((a_idx + 1) as f64);
1533        } else if let Some(b_idx) = entry.b_index {
1534            ib.push((b_idx + 1) as f64);
1535        }
1536    }
1537
1538    let value_tensor =
1539        Tensor::from_numeric_storage(T::numeric_storage(values), vec![order.len(), 1])
1540            .map_err(|e| union_internal_error(format!("union: {e}")))?;
1541    let ia_len = ia.len();
1542    let ib_len = ib.len();
1543    let ia_tensor = Tensor::new(ia, vec![ia_len, 1])
1544        .map_err(|e| union_internal_error(format!("union: {e}")))?;
1545    let ib_tensor = Tensor::new(ib, vec![ib_len, 1])
1546        .map_err(|e| union_internal_error(format!("union: {e}")))?;
1547
1548    Ok(UnionEvaluation::new(
1549        tensor::tensor_into_value(value_tensor),
1550        ia_tensor,
1551        ib_tensor,
1552    ))
1553}
1554
1555fn assemble_integer_union(
1556    entries: Vec<IntegerUnionEntry>,
1557    storage: &IntegerStorage,
1558    opts: &UnionOptions,
1559) -> crate::BuiltinResult<UnionEvaluation> {
1560    let mut order: Vec<_> = (0..entries.len()).collect();
1561    match opts.order {
1562        UnionOrder::Sorted => order.sort_by(|&a, &b| {
1563            integer_order::compare(&entries[a].value, &entries[b].value, false, false)
1564        }),
1565        UnionOrder::Stable => order.sort_by_key(|&index| entries[index].order_rank),
1566    }
1567    let values: Vec<_> = order
1568        .iter()
1569        .map(|&index| entries[index].value.clone())
1570        .collect();
1571    let mut ia = Vec::new();
1572    let mut ib = Vec::new();
1573    for &index in &order {
1574        let entry = &entries[index];
1575        if let Some(a_index) = entry.a_index {
1576            ia.push((a_index + 1) as f64);
1577        } else if let Some(b_index) = entry.b_index {
1578            ib.push((b_index + 1) as f64);
1579        }
1580    }
1581    let values = Tensor::new_integer(
1582        storage
1583            .from_exact_values_like(values)
1584            .map_err(|e| union_internal_error(format!("union: {e}")))?,
1585        vec![order.len(), 1],
1586    )
1587    .map_err(|e| union_internal_error(format!("union: {e}")))?;
1588    let ia_len = ia.len();
1589    let ib_len = ib.len();
1590    let ia = Tensor::new(ia, vec![ia_len, 1])
1591        .map_err(|e| union_internal_error(format!("union: {e}")))?;
1592    let ib = Tensor::new(ib, vec![ib_len, 1])
1593        .map_err(|e| union_internal_error(format!("union: {e}")))?;
1594    Ok(UnionEvaluation::new(Value::Tensor(values), ia, ib))
1595}
1596
1597fn assemble_floating_row_union<T: SetFloat>(
1598    entries: Vec<FloatingRowUnionEntry<T>>,
1599    opts: &UnionOptions,
1600    cols: usize,
1601) -> crate::BuiltinResult<UnionEvaluation> {
1602    let mut order: Vec<usize> = (0..entries.len()).collect();
1603    match opts.order {
1604        UnionOrder::Sorted => {
1605            order.sort_by(|&lhs, &rhs| {
1606                compare_floating_rows(&entries[lhs].row_data, &entries[rhs].row_data)
1607            });
1608        }
1609        UnionOrder::Stable => {
1610            order.sort_by_key(|&idx| entries[idx].order_rank);
1611        }
1612    }
1613
1614    let unique_rows = order.len();
1615    let mut values = vec![T::default(); unique_rows * cols];
1616    let mut ia = Vec::new();
1617    let mut ib = Vec::new();
1618
1619    for (row_pos, &entry_idx) in order.iter().enumerate() {
1620        let entry = &entries[entry_idx];
1621        for col in 0..cols {
1622            let dest = row_pos + col * unique_rows;
1623            values[dest] = entry.row_data[col];
1624        }
1625        if let Some(a_row) = entry.a_row {
1626            ia.push((a_row + 1) as f64);
1627        } else if let Some(b_row) = entry.b_row {
1628            ib.push((b_row + 1) as f64);
1629        }
1630    }
1631
1632    let value_tensor =
1633        Tensor::from_numeric_storage(T::numeric_storage(values), vec![unique_rows, cols])
1634            .map_err(|e| union_internal_error(format!("union: {e}")))?;
1635    let ia_len = ia.len();
1636    let ib_len = ib.len();
1637    let ia_tensor = Tensor::new(ia, vec![ia_len, 1])
1638        .map_err(|e| union_internal_error(format!("union: {e}")))?;
1639    let ib_tensor = Tensor::new(ib, vec![ib_len, 1])
1640        .map_err(|e| union_internal_error(format!("union: {e}")))?;
1641
1642    Ok(UnionEvaluation::new(
1643        tensor::tensor_into_value(value_tensor),
1644        ia_tensor,
1645        ib_tensor,
1646    ))
1647}
1648
1649fn assemble_integer_row_union(
1650    entries: Vec<IntegerRowUnionEntry>,
1651    storage: &IntegerStorage,
1652    opts: &UnionOptions,
1653    cols: usize,
1654) -> crate::BuiltinResult<UnionEvaluation> {
1655    let mut order: Vec<_> = (0..entries.len()).collect();
1656    match opts.order {
1657        UnionOrder::Sorted => order.sort_by(|&a, &b| {
1658            for (left, right) in entries[a].row_data.iter().zip(&entries[b].row_data) {
1659                let ordering = integer_order::compare(left, right, false, false);
1660                if ordering != Ordering::Equal {
1661                    return ordering;
1662                }
1663            }
1664            Ordering::Equal
1665        }),
1666        UnionOrder::Stable => order.sort_by_key(|&index| entries[index].order_rank),
1667    }
1668    let rows = order.len();
1669    let mut values = Vec::with_capacity(rows * cols);
1670    for col in 0..cols {
1671        for &index in &order {
1672            values.push(entries[index].row_data[col].clone());
1673        }
1674    }
1675    let mut ia = Vec::new();
1676    let mut ib = Vec::new();
1677    for &index in &order {
1678        let entry = &entries[index];
1679        if let Some(a_row) = entry.a_row {
1680            ia.push((a_row + 1) as f64);
1681        } else if let Some(b_row) = entry.b_row {
1682            ib.push((b_row + 1) as f64);
1683        }
1684    }
1685    let values = Tensor::new_integer(
1686        storage
1687            .from_exact_values_like(values)
1688            .map_err(|e| union_internal_error(format!("union: {e}")))?,
1689        vec![rows, cols],
1690    )
1691    .map_err(|e| union_internal_error(format!("union: {e}")))?;
1692    let ia_len = ia.len();
1693    let ib_len = ib.len();
1694    let ia = Tensor::new(ia, vec![ia_len, 1])
1695        .map_err(|e| union_internal_error(format!("union: {e}")))?;
1696    let ib = Tensor::new(ib, vec![ib_len, 1])
1697        .map_err(|e| union_internal_error(format!("union: {e}")))?;
1698    Ok(UnionEvaluation::new(Value::Tensor(values), ia, ib))
1699}
1700
1701fn assemble_complex_union<T: SetFloat>(
1702    entries: Vec<ComplexUnionEntry<T>>,
1703    opts: &UnionOptions,
1704) -> crate::BuiltinResult<UnionEvaluation> {
1705    let mut order: Vec<usize> = (0..entries.len()).collect();
1706    match opts.order {
1707        UnionOrder::Sorted => {
1708            order.sort_by(|&lhs, &rhs| compare_complex(entries[lhs].value, entries[rhs].value));
1709        }
1710        UnionOrder::Stable => {
1711            order.sort_by_key(|&idx| entries[idx].order_rank);
1712        }
1713    }
1714
1715    let mut values = Vec::with_capacity(order.len());
1716    let mut ia = Vec::new();
1717    let mut ib = Vec::new();
1718    for &idx in &order {
1719        let entry = &entries[idx];
1720        values.push(entry.value);
1721        if let Some(a_idx) = entry.a_index {
1722            ia.push((a_idx + 1) as f64);
1723        } else if let Some(b_idx) = entry.b_index {
1724            ib.push((b_idx + 1) as f64);
1725        }
1726    }
1727
1728    let value_tensor =
1729        ComplexTensor::from_complex_storage(T::complex_storage(values), vec![order.len(), 1])
1730            .map_err(|e| union_internal_error(format!("union: {e}")))?;
1731    let ia_len = ia.len();
1732    let ib_len = ib.len();
1733    let ia_tensor = Tensor::new(ia, vec![ia_len, 1])
1734        .map_err(|e| union_internal_error(format!("union: {e}")))?;
1735    let ib_tensor = Tensor::new(ib, vec![ib_len, 1])
1736        .map_err(|e| union_internal_error(format!("union: {e}")))?;
1737
1738    let value = if value_tensor.as_f32_slice().is_some() {
1739        Value::ComplexTensor(value_tensor)
1740    } else {
1741        complex_tensor_into_value(value_tensor)
1742    };
1743    Ok(UnionEvaluation::new(value, ia_tensor, ib_tensor))
1744}
1745
1746fn assemble_complex_row_union<T: SetFloat>(
1747    entries: Vec<ComplexRowUnionEntry<T>>,
1748    opts: &UnionOptions,
1749    cols: usize,
1750) -> crate::BuiltinResult<UnionEvaluation> {
1751    let mut order: Vec<usize> = (0..entries.len()).collect();
1752    match opts.order {
1753        UnionOrder::Sorted => {
1754            order.sort_by(|&lhs, &rhs| {
1755                compare_complex_rows(&entries[lhs].row_data, &entries[rhs].row_data)
1756            });
1757        }
1758        UnionOrder::Stable => {
1759            order.sort_by_key(|&idx| entries[idx].order_rank);
1760        }
1761    }
1762
1763    let unique_rows = order.len();
1764    let mut values = vec![(T::default(), T::default()); unique_rows * cols];
1765    let mut ia = Vec::new();
1766    let mut ib = Vec::new();
1767
1768    for (row_pos, &entry_idx) in order.iter().enumerate() {
1769        let entry = &entries[entry_idx];
1770        for col in 0..cols {
1771            let dest = row_pos + col * unique_rows;
1772            values[dest] = entry.row_data[col];
1773        }
1774        if let Some(a_row) = entry.a_row {
1775            ia.push((a_row + 1) as f64);
1776        } else if let Some(b_row) = entry.b_row {
1777            ib.push((b_row + 1) as f64);
1778        }
1779    }
1780
1781    let value_tensor =
1782        ComplexTensor::from_complex_storage(T::complex_storage(values), vec![unique_rows, cols])
1783            .map_err(|e| union_internal_error(format!("union: {e}")))?;
1784    let ia_len = ia.len();
1785    let ib_len = ib.len();
1786    let ia_tensor = Tensor::new(ia, vec![ia_len, 1])
1787        .map_err(|e| union_internal_error(format!("union: {e}")))?;
1788    let ib_tensor = Tensor::new(ib, vec![ib_len, 1])
1789        .map_err(|e| union_internal_error(format!("union: {e}")))?;
1790
1791    let value = if value_tensor.as_f32_slice().is_some() {
1792        Value::ComplexTensor(value_tensor)
1793    } else {
1794        complex_tensor_into_value(value_tensor)
1795    };
1796    Ok(UnionEvaluation::new(value, ia_tensor, ib_tensor))
1797}
1798
1799fn assemble_char_union(
1800    entries: Vec<CharUnionEntry>,
1801    opts: &UnionOptions,
1802) -> crate::BuiltinResult<UnionEvaluation> {
1803    let mut order: Vec<usize> = (0..entries.len()).collect();
1804    match opts.order {
1805        UnionOrder::Sorted => {
1806            order.sort_by(|&lhs, &rhs| entries[lhs].ch.cmp(&entries[rhs].ch));
1807        }
1808        UnionOrder::Stable => {
1809            order.sort_by_key(|&idx| entries[idx].order_rank);
1810        }
1811    }
1812
1813    let mut values = Vec::with_capacity(order.len());
1814    let mut ia = Vec::new();
1815    let mut ib = Vec::new();
1816    for &idx in &order {
1817        let entry = &entries[idx];
1818        values.push(entry.ch);
1819        if let Some(a_idx) = entry.a_index {
1820            ia.push((a_idx + 1) as f64);
1821        } else if let Some(b_idx) = entry.b_index {
1822            ib.push((b_idx + 1) as f64);
1823        }
1824    }
1825
1826    let value_array = CharArray::new(values, order.len(), 1)
1827        .map_err(|e| union_internal_error(format!("union: {e}")))?;
1828    let ia_len = ia.len();
1829    let ib_len = ib.len();
1830    let ia_tensor = Tensor::new(ia, vec![ia_len, 1])
1831        .map_err(|e| union_internal_error(format!("union: {e}")))?;
1832    let ib_tensor = Tensor::new(ib, vec![ib_len, 1])
1833        .map_err(|e| union_internal_error(format!("union: {e}")))?;
1834
1835    Ok(UnionEvaluation::new(
1836        Value::CharArray(value_array),
1837        ia_tensor,
1838        ib_tensor,
1839    ))
1840}
1841
1842fn assemble_char_row_union(
1843    entries: Vec<CharRowUnionEntry>,
1844    opts: &UnionOptions,
1845    cols: usize,
1846) -> crate::BuiltinResult<UnionEvaluation> {
1847    let mut order: Vec<usize> = (0..entries.len()).collect();
1848    match opts.order {
1849        UnionOrder::Sorted => {
1850            order.sort_by(|&lhs, &rhs| {
1851                compare_char_rows(&entries[lhs].row_data, &entries[rhs].row_data)
1852            });
1853        }
1854        UnionOrder::Stable => {
1855            order.sort_by_key(|&idx| entries[idx].order_rank);
1856        }
1857    }
1858
1859    let unique_rows = order.len();
1860    let mut values = vec!['\0'; unique_rows * cols];
1861    let mut ia = Vec::new();
1862    let mut ib = Vec::new();
1863
1864    for (row_pos, &entry_idx) in order.iter().enumerate() {
1865        let entry = &entries[entry_idx];
1866        for col in 0..cols {
1867            let dest = row_pos * cols + col;
1868            values[dest] = entry.row_data[col];
1869        }
1870        if let Some(a_row) = entry.a_row {
1871            ia.push((a_row + 1) as f64);
1872        } else if let Some(b_row) = entry.b_row {
1873            ib.push((b_row + 1) as f64);
1874        }
1875    }
1876
1877    let value_array = CharArray::new(values, unique_rows, cols)
1878        .map_err(|e| union_internal_error(format!("union: {e}")))?;
1879    let ia_len = ia.len();
1880    let ib_len = ib.len();
1881    let ia_tensor = Tensor::new(ia, vec![ia_len, 1])
1882        .map_err(|e| union_internal_error(format!("union: {e}")))?;
1883    let ib_tensor = Tensor::new(ib, vec![ib_len, 1])
1884        .map_err(|e| union_internal_error(format!("union: {e}")))?;
1885
1886    Ok(UnionEvaluation::new(
1887        Value::CharArray(value_array),
1888        ia_tensor,
1889        ib_tensor,
1890    ))
1891}
1892
1893fn assemble_string_union(
1894    entries: Vec<StringUnionEntry>,
1895    opts: &UnionOptions,
1896) -> crate::BuiltinResult<UnionEvaluation> {
1897    let mut order: Vec<usize> = (0..entries.len()).collect();
1898    match opts.order {
1899        UnionOrder::Sorted => {
1900            order.sort_by(|&lhs, &rhs| entries[lhs].value.cmp(&entries[rhs].value));
1901        }
1902        UnionOrder::Stable => {
1903            order.sort_by_key(|&idx| entries[idx].order_rank);
1904        }
1905    }
1906
1907    let mut values = Vec::with_capacity(order.len());
1908    let mut ia = Vec::new();
1909    let mut ib = Vec::new();
1910    for &idx in &order {
1911        let entry = &entries[idx];
1912        values.push(entry.value.clone());
1913        if let Some(a_idx) = entry.a_index {
1914            ia.push((a_idx + 1) as f64);
1915        } else if let Some(b_idx) = entry.b_index {
1916            ib.push((b_idx + 1) as f64);
1917        }
1918    }
1919
1920    let value_array = StringArray::new(values, vec![order.len(), 1])
1921        .map_err(|e| union_internal_error(format!("union: {e}")))?;
1922    let ia_len = ia.len();
1923    let ib_len = ib.len();
1924    let ia_tensor = Tensor::new(ia, vec![ia_len, 1])
1925        .map_err(|e| union_internal_error(format!("union: {e}")))?;
1926    let ib_tensor = Tensor::new(ib, vec![ib_len, 1])
1927        .map_err(|e| union_internal_error(format!("union: {e}")))?;
1928
1929    Ok(UnionEvaluation::new(
1930        Value::StringArray(value_array),
1931        ia_tensor,
1932        ib_tensor,
1933    ))
1934}
1935
1936fn assemble_string_row_union(
1937    entries: Vec<StringRowUnionEntry>,
1938    opts: &UnionOptions,
1939    cols: usize,
1940) -> crate::BuiltinResult<UnionEvaluation> {
1941    let mut order: Vec<usize> = (0..entries.len()).collect();
1942    match opts.order {
1943        UnionOrder::Sorted => {
1944            order.sort_by(|&lhs, &rhs| {
1945                compare_string_rows(&entries[lhs].row_data, &entries[rhs].row_data)
1946            });
1947        }
1948        UnionOrder::Stable => {
1949            order.sort_by_key(|&idx| entries[idx].order_rank);
1950        }
1951    }
1952
1953    let unique_rows = order.len();
1954    let mut values = vec![String::new(); unique_rows * cols];
1955    let mut ia = Vec::new();
1956    let mut ib = Vec::new();
1957
1958    for (row_pos, &entry_idx) in order.iter().enumerate() {
1959        let entry = &entries[entry_idx];
1960        for col in 0..cols {
1961            let dest = row_pos + col * unique_rows;
1962            values[dest] = entry.row_data[col].clone();
1963        }
1964        if let Some(a_row) = entry.a_row {
1965            ia.push((a_row + 1) as f64);
1966        } else if let Some(b_row) = entry.b_row {
1967            ib.push((b_row + 1) as f64);
1968        }
1969    }
1970
1971    let value_array = StringArray::new(values, vec![unique_rows, cols])
1972        .map_err(|e| union_internal_error(format!("union: {e}")))?;
1973    let ia_len = ia.len();
1974    let ib_len = ib.len();
1975    let ia_tensor = Tensor::new(ia, vec![ia_len, 1])
1976        .map_err(|e| union_internal_error(format!("union: {e}")))?;
1977    let ib_tensor = Tensor::new(ib, vec![ib_len, 1])
1978        .map_err(|e| union_internal_error(format!("union: {e}")))?;
1979
1980    Ok(UnionEvaluation::new(
1981        Value::StringArray(value_array),
1982        ia_tensor,
1983        ib_tensor,
1984    ))
1985}
1986
1987fn compare_floating_rows<T: SetFloat>(a: &[T], b: &[T]) -> Ordering {
1988    for (lhs, rhs) in a.iter().zip(b.iter()) {
1989        let ord = lhs.compare(*rhs);
1990        if ord != Ordering::Equal {
1991            return ord;
1992        }
1993    }
1994    Ordering::Equal
1995}
1996
1997fn complex_is_nan<T: SetFloat>(value: (T, T)) -> bool {
1998    value.0.is_nan() || value.1.is_nan()
1999}
2000
2001fn compare_complex<T: SetFloat>(a: (T, T), b: (T, T)) -> Ordering {
2002    match (complex_is_nan(a), complex_is_nan(b)) {
2003        (true, true) => Ordering::Equal,
2004        (true, false) => Ordering::Greater,
2005        (false, true) => Ordering::Less,
2006        (false, false) => {
2007            let mag_a = a.0.hypot(a.1);
2008            let mag_b = b.0.hypot(b.1);
2009            let mag_cmp = mag_a.compare(mag_b);
2010            if mag_cmp != Ordering::Equal {
2011                return mag_cmp;
2012            }
2013            let re_cmp = a.0.compare(b.0);
2014            if re_cmp != Ordering::Equal {
2015                return re_cmp;
2016            }
2017            a.1.compare(b.1)
2018        }
2019    }
2020}
2021
2022fn compare_complex_rows<T: SetFloat>(a: &[(T, T)], b: &[(T, T)]) -> Ordering {
2023    for (lhs, rhs) in a.iter().zip(b.iter()) {
2024        let ord = compare_complex(*lhs, *rhs);
2025        if ord != Ordering::Equal {
2026            return ord;
2027        }
2028    }
2029    Ordering::Equal
2030}
2031
2032fn compare_char_rows(a: &[char], b: &[char]) -> Ordering {
2033    for (lhs, rhs) in a.iter().zip(b.iter()) {
2034        let ord = lhs.cmp(rhs);
2035        if ord != Ordering::Equal {
2036            return ord;
2037        }
2038    }
2039    Ordering::Equal
2040}
2041
2042fn compare_string_rows(a: &[String], b: &[String]) -> Ordering {
2043    for (lhs, rhs) in a.iter().zip(b.iter()) {
2044        let ord = lhs.cmp(rhs);
2045        if ord != Ordering::Equal {
2046            return ord;
2047        }
2048    }
2049    Ordering::Equal
2050}
2051
2052#[cfg(test)]
2053pub(crate) mod tests {
2054    use super::*;
2055    use crate::builtins::common::test_support;
2056    use runmat_accelerate_api::HostTensorView;
2057    use runmat_builtins::{ResolveContext, Type};
2058    use runmat_value::{IntValue, Tensor, Value};
2059
2060    fn evaluate_sync(a: Value, b: Value, rest: &[Value]) -> crate::BuiltinResult<UnionEvaluation> {
2061        futures::executor::block_on(evaluate(a, b, rest))
2062    }
2063
2064    fn builtin_sync(a: Value, b: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
2065        futures::executor::block_on(union_builtin(a, b, rest))
2066    }
2067
2068    #[test]
2069    fn registered_builtin_restores_resident_outputs_and_rejects_excess_arity() {
2070        test_support::with_test_provider(|provider| {
2071            let left = Tensor::new_integer(IntegerStorage::I32(vec![7, 2, 9]), vec![3, 1]).unwrap();
2072            let right = Tensor::new_integer(IntegerStorage::I32(vec![2, 7]), vec![2, 1]).unwrap();
2073            let left =
2074                Value::GpuTensor(gpu_helpers::upload_tensor(provider, &left).expect("upload left"));
2075            let right = Value::GpuTensor(
2076                gpu_helpers::upload_tensor(provider, &right).expect("upload right"),
2077            );
2078
2079            {
2080                let _guard = crate::output_count::push_output_count(Some(3));
2081                let Value::OutputList(outputs) =
2082                    builtin_sync(left, right, Vec::new()).expect("resident union")
2083                else {
2084                    panic!("expected output list");
2085                };
2086                assert_eq!(outputs.len(), 3);
2087                assert!(outputs
2088                    .iter()
2089                    .all(|output| matches!(output, Value::GpuTensor(_))));
2090                assert_eq!(
2091                    test_support::gather(outputs[0].clone())
2092                        .expect("gather values")
2093                        .integer_storage(),
2094                    Some(&IntegerStorage::I32(vec![2, 7, 9]))
2095                );
2096            }
2097
2098            let _guard = crate::output_count::push_output_count(Some(4));
2099            let err = builtin_sync(Value::Num(1.0), Value::Num(1.0), Vec::new())
2100                .expect_err("excess outputs must fail");
2101            assert_eq!(err.identifier(), UNION_ERROR_INVALID_ARGUMENT.identifier);
2102        });
2103    }
2104
2105    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2106    #[test]
2107    fn union_numeric_sorted_default() {
2108        let a = Tensor::new(vec![5.0, 7.0, 1.0], vec![3, 1]).unwrap();
2109        let b = Tensor::new(vec![3.0, 1.0, 1.0], vec![3, 1]).unwrap();
2110        let eval = evaluate_sync(Value::Tensor(a), Value::Tensor(b), &[]).expect("union");
2111        match eval.values_value() {
2112            Value::Tensor(t) => {
2113                assert_eq!(t.materialize_f64(), vec![1.0, 3.0, 5.0, 7.0]);
2114                assert_eq!(t.shape, vec![4, 1]);
2115            }
2116            other => panic!("expected tensor result, got {other:?}"),
2117        }
2118        let ia = tensor::value_into_tensor_for("union", eval.ia_value()).expect("ia tensor");
2119        assert_eq!(ia.materialize_f64(), vec![3.0, 1.0, 2.0]);
2120        assert_eq!(ia.shape, vec![3, 1]);
2121        let ib = tensor::value_into_tensor_for("union", eval.ib_value()).expect("ib tensor");
2122        assert_eq!(ib.materialize_f64(), vec![1.0]);
2123        assert_eq!(ib.shape, vec![1, 1]);
2124    }
2125
2126    #[test]
2127    fn union_preserves_native_single_elements_and_rows() {
2128        let a = Tensor::from_f32(vec![5.0, 7.0, 1.0], vec![3, 1]).unwrap();
2129        let b = Tensor::from_f32(vec![3.0, 1.0, 1.0], vec![3, 1]).unwrap();
2130        let values = evaluate_sync(Value::Tensor(a), Value::Tensor(b), &[])
2131            .expect("single union")
2132            .into_values_value();
2133        let Value::Tensor(values) = values else {
2134            panic!("expected native single values");
2135        };
2136        assert_eq!(
2137            values.into_numeric_storage().unwrap(),
2138            NumericStorage::F32(vec![1.0, 3.0, 5.0, 7.0])
2139        );
2140
2141        let a = Tensor::from_f32(vec![1.0, 3.0, 1.0, 2.0, 4.0, 2.0], vec![3, 2]).unwrap();
2142        let b = Tensor::from_f32(vec![3.0, 5.0, 4.0, 6.0], vec![2, 2]).unwrap();
2143        let values = evaluate_sync(Value::Tensor(a), Value::Tensor(b), &[Value::from("rows")])
2144            .expect("single row union")
2145            .into_values_value();
2146        let Value::Tensor(values) = values else {
2147            panic!("expected native single rows");
2148        };
2149        assert_eq!(values.shape, vec![3, 2]);
2150        assert_eq!(
2151            values.into_numeric_storage().unwrap(),
2152            NumericStorage::F32(vec![1.0, 3.0, 5.0, 2.0, 4.0, 6.0])
2153        );
2154    }
2155
2156    #[test]
2157    fn union_preserves_native_complex_single_elements_and_rows() {
2158        let a = ComplexTensor::from_f32(vec![(1.0, 1.0)], vec![1, 1]).unwrap();
2159        let b = ComplexTensor::from_f32(vec![(1.0, 1.0)], vec![1, 1]).unwrap();
2160        let values = evaluate_sync(Value::ComplexTensor(a), Value::ComplexTensor(b), &[])
2161            .expect("complex single union")
2162            .into_values_value();
2163        let Value::ComplexTensor(values) = values else {
2164            panic!("expected native complex single value");
2165        };
2166        assert_eq!(values.as_f32_slice(), Some(&[(1.0, 1.0)][..]));
2167
2168        let a = ComplexTensor::from_f32(
2169            vec![
2170                (1.0, 0.0),
2171                (3.0, 0.0),
2172                (1.0, 0.0),
2173                (2.0, 1.0),
2174                (4.0, 1.0),
2175                (2.0, 1.0),
2176            ],
2177            vec![3, 2],
2178        )
2179        .unwrap();
2180        let b = ComplexTensor::from_f32(
2181            vec![(3.0, 0.0), (5.0, 0.0), (4.0, 1.0), (6.0, 1.0)],
2182            vec![2, 2],
2183        )
2184        .unwrap();
2185        let values = evaluate_sync(
2186            Value::ComplexTensor(a),
2187            Value::ComplexTensor(b),
2188            &[Value::from("rows")],
2189        )
2190        .expect("complex single row union")
2191        .into_values_value();
2192        let Value::ComplexTensor(values) = values else {
2193            panic!("expected native complex single rows");
2194        };
2195        assert_eq!(values.shape, vec![3, 2]);
2196        assert_eq!(
2197            values.as_f32_slice(),
2198            Some(
2199                &[
2200                    (1.0, 0.0),
2201                    (3.0, 0.0),
2202                    (5.0, 0.0),
2203                    (2.0, 1.0),
2204                    (4.0, 1.0),
2205                    (6.0, 1.0),
2206                ][..]
2207            )
2208        );
2209    }
2210
2211    #[test]
2212    fn union_preserves_exact_integer_elements_and_rows() {
2213        let a = Tensor::new_integer(
2214            runmat_value::IntegerStorage::U64(vec![u64::MAX, 0, 9_007_199_254_740_993]),
2215            vec![3, 1],
2216        )
2217        .expect("input");
2218        let b = Tensor::new_integer(runmat_value::IntegerStorage::U64(vec![0, 7]), vec![2, 1])
2219            .expect("input");
2220        let (values, ia, ib) = evaluate_sync(Value::Tensor(a), Value::Tensor(b), &[])
2221            .expect("union")
2222            .into_triple();
2223        let Value::Tensor(values) = values else {
2224            panic!("exact values");
2225        };
2226        assert_eq!(
2227            values.integer_storage(),
2228            Some(&runmat_value::IntegerStorage::U64(vec![
2229                0,
2230                7,
2231                9_007_199_254_740_993,
2232                u64::MAX
2233            ]))
2234        );
2235        let ia = tensor::value_into_tensor_for("union", ia).expect("indices");
2236        assert_eq!(ia.materialize_f64(), vec![2.0, 3.0, 1.0]);
2237        let ib = tensor::value_into_tensor_for("union", ib).expect("indices");
2238        assert_eq!(ib.materialize_f64(), vec![2.0]);
2239
2240        let a = Tensor::new_integer(
2241            runmat_value::IntegerStorage::U64(vec![u64::MAX, 9_007_199_254_740_993, 0, 1]),
2242            vec![2, 2],
2243        )
2244        .expect("rows input");
2245        let b = Tensor::new_integer(
2246            runmat_value::IntegerStorage::U64(vec![9_007_199_254_740_993, 4, 1, 2]),
2247            vec![2, 2],
2248        )
2249        .expect("rows input");
2250        let (values, ia, ib) =
2251            evaluate_sync(Value::Tensor(a), Value::Tensor(b), &[Value::from("rows")])
2252                .expect("union rows")
2253                .into_triple();
2254        let Value::Tensor(values) = values else {
2255            panic!("exact row values");
2256        };
2257        assert_eq!(
2258            values.integer_storage(),
2259            Some(&runmat_value::IntegerStorage::U64(vec![
2260                4,
2261                9_007_199_254_740_993,
2262                u64::MAX,
2263                2,
2264                1,
2265                0,
2266            ]))
2267        );
2268        let ia = tensor::value_into_tensor_for("union", ia).expect("row indices");
2269        assert_eq!(ia.materialize_f64(), vec![2.0, 1.0]);
2270        let ib = tensor::value_into_tensor_for("union", ib).expect("row indices");
2271        assert_eq!(ib.materialize_f64(), vec![2.0]);
2272    }
2273
2274    #[test]
2275    fn union_rejects_mixed_nondouble_integer_classes() {
2276        let a = Tensor::new_integer(runmat_value::IntegerStorage::U16(vec![7, 2]), vec![2, 1])
2277            .expect("input");
2278        let b = Tensor::new_integer(runmat_value::IntegerStorage::I32(vec![2, 9]), vec![2, 1])
2279            .expect("input");
2280        let error = evaluate_sync(Value::Tensor(a), Value::Tensor(b), &[])
2281            .expect_err("mixed integer classes must reject");
2282        assert_eq!(
2283            error.identifier(),
2284            UNION_ERROR_NUMERIC_CLASS_MISMATCH.identifier
2285        );
2286    }
2287
2288    #[test]
2289    fn union_preserves_every_exact_integer_class() {
2290        let cases = [
2291            runmat_value::IntegerStorage::I8(vec![i8::MAX, 0]),
2292            runmat_value::IntegerStorage::I16(vec![i16::MAX, 0]),
2293            runmat_value::IntegerStorage::I32(vec![i32::MAX, 0]),
2294            runmat_value::IntegerStorage::I64(vec![i64::MAX, 0]),
2295            runmat_value::IntegerStorage::U8(vec![u8::MAX, 0]),
2296            runmat_value::IntegerStorage::U16(vec![u16::MAX, 0]),
2297            runmat_value::IntegerStorage::U32(vec![u32::MAX, 0]),
2298            runmat_value::IntegerStorage::U64(vec![u64::MAX, 0]),
2299        ];
2300        for storage in cases {
2301            let expected = storage.clone();
2302            let a = Tensor::new_integer(storage, vec![2, 1]).expect("input");
2303            let b = Tensor::new_integer(expected.zeros_like(1), vec![1, 1]).expect("input");
2304            let values =
2305                evaluate_sync(Value::Tensor(a), Value::Tensor(b), &[Value::from("stable")])
2306                    .expect("union")
2307                    .into_values_value();
2308            let Value::Tensor(values) = values else {
2309                panic!("exact values");
2310            };
2311            assert_eq!(values.integer_storage(), Some(&expected));
2312        }
2313    }
2314
2315    #[test]
2316    fn union_type_resolver_numeric() {
2317        assert_eq!(
2318            set_values_output_type(
2319                &[Type::tensor(), Type::tensor()],
2320                &ResolveContext::new(Vec::new()),
2321            ),
2322            Type::tensor()
2323        );
2324    }
2325
2326    #[test]
2327    fn union_type_resolver_string_array() {
2328        assert_eq!(
2329            set_values_output_type(
2330                &[Type::cell_of(Type::String), Type::String],
2331                &ResolveContext::new(Vec::new()),
2332            ),
2333            Type::cell_of(Type::String)
2334        );
2335    }
2336
2337    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2338    #[test]
2339    fn union_numeric_stable_order() {
2340        let a = Tensor::new(vec![5.0, 7.0, 1.0], vec![3, 1]).unwrap();
2341        let b = Tensor::new(vec![3.0, 2.0, 4.0], vec![3, 1]).unwrap();
2342        let eval = evaluate_sync(Value::Tensor(a), Value::Tensor(b), &[Value::from("stable")])
2343            .expect("union");
2344        match eval.values_value() {
2345            Value::Tensor(t) => {
2346                assert_eq!(t.materialize_f64(), vec![5.0, 7.0, 1.0, 3.0, 2.0, 4.0]);
2347                assert_eq!(t.shape, vec![6, 1]);
2348            }
2349            other => panic!("expected tensor result, got {other:?}"),
2350        }
2351        let ia = tensor::value_into_tensor_for("union", eval.ia_value()).expect("ia tensor");
2352        assert_eq!(ia.materialize_f64(), vec![1.0, 2.0, 3.0]);
2353        let ib = tensor::value_into_tensor_for("union", eval.ib_value()).expect("ib tensor");
2354        assert_eq!(ib.materialize_f64(), vec![1.0, 2.0, 3.0]);
2355    }
2356
2357    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2358    #[test]
2359    fn union_numeric_sorted_places_nan_last() {
2360        let a = Tensor::new(vec![f64::NAN, 1.0], vec![2, 1]).unwrap();
2361        let b = Tensor::new(vec![2.0, f64::NAN], vec![2, 1]).unwrap();
2362        let eval = evaluate_sync(Value::Tensor(a), Value::Tensor(b), &[]).expect("union");
2363        let values = tensor::value_into_tensor_for("union", eval.values_value()).expect("values");
2364        assert_eq!(values.shape, vec![3, 1]);
2365        assert_eq!(values.materialize_f64()[0], 1.0);
2366        assert_eq!(values.materialize_f64()[1], 2.0);
2367        assert!(values.materialize_f64()[2].is_nan());
2368        let ia = tensor::value_into_tensor_for("union", eval.ia_value()).expect("ia tensor");
2369        assert_eq!(ia.materialize_f64(), vec![2.0, 1.0]);
2370        let ib = tensor::value_into_tensor_for("union", eval.ib_value()).expect("ib tensor");
2371        assert_eq!(ib.materialize_f64(), vec![1.0]);
2372    }
2373
2374    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2375    #[test]
2376    fn union_numeric_rows_sorted() {
2377        let a = Tensor::new(vec![1.0, 3.0, 1.0, 2.0, 4.0, 2.0], vec![3, 2]).unwrap();
2378        let b = Tensor::new(vec![3.0, 5.0, 4.0, 6.0], vec![2, 2]).unwrap();
2379        let eval = evaluate_sync(Value::Tensor(a), Value::Tensor(b), &[Value::from("rows")])
2380            .expect("union");
2381        match eval.values_value() {
2382            Value::Tensor(t) => {
2383                assert_eq!(t.shape, vec![3, 2]);
2384                assert_eq!(t.materialize_f64(), vec![1.0, 3.0, 5.0, 2.0, 4.0, 6.0]);
2385            }
2386            other => panic!("expected tensor result, got {other:?}"),
2387        }
2388        let ia = tensor::value_into_tensor_for("union", eval.ia_value()).expect("ia tensor");
2389        assert_eq!(ia.materialize_f64(), vec![1.0, 2.0]);
2390        let ib = tensor::value_into_tensor_for("union", eval.ib_value()).expect("ib tensor");
2391        assert_eq!(ib.materialize_f64(), vec![2.0]);
2392    }
2393
2394    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2395    #[test]
2396    fn union_numeric_rows_stable_preserves_first_occurrence() {
2397        let a = Tensor::new(vec![1.0, 3.0, 1.0, 2.0, 4.0, 2.0], vec![3, 2]).unwrap();
2398        let b = Tensor::new(vec![3.0, 5.0, 1.0, 4.0, 6.0, 2.0], vec![3, 2]).unwrap();
2399        let eval = evaluate_sync(
2400            Value::Tensor(a),
2401            Value::Tensor(b),
2402            &[Value::from("rows"), Value::from("stable")],
2403        )
2404        .expect("union");
2405        let (values, ia, ib) = eval.into_triple();
2406        match values {
2407            Value::Tensor(t) => {
2408                assert_eq!(t.shape, vec![3, 2]);
2409                assert_eq!(t.materialize_f64(), vec![1.0, 3.0, 5.0, 2.0, 4.0, 6.0]);
2410            }
2411            other => panic!("expected tensor result, got {other:?}"),
2412        }
2413        let ia_tensor = tensor::value_into_tensor_for("union", ia).expect("ia tensor");
2414        assert_eq!(ia_tensor.materialize_f64(), vec![1.0, 2.0]);
2415        let ib_tensor = tensor::value_into_tensor_for("union", ib).expect("ib tensor");
2416        assert_eq!(ib_tensor.materialize_f64(), vec![2.0]);
2417    }
2418
2419    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2420    #[test]
2421    fn union_char_elements() {
2422        let a = CharArray::new(vec!['m', 'z', 'm', 'a'], 2, 2).unwrap();
2423        let b = CharArray::new(vec!['a', 'x', 'm', 'a'], 2, 2).unwrap();
2424        let eval = evaluate_sync(Value::CharArray(a), Value::CharArray(b), &[]).expect("union");
2425        match eval.values_value() {
2426            Value::CharArray(arr) => {
2427                assert_eq!(arr.rows, 4);
2428                assert_eq!(arr.cols, 1);
2429                assert_eq!(arr.data, vec!['a', 'm', 'x', 'z']);
2430            }
2431            other => panic!("expected char array, got {other:?}"),
2432        }
2433        let ia = tensor::value_into_tensor_for("union", eval.ia_value()).expect("ia tensor");
2434        assert_eq!(ia.materialize_f64(), vec![4.0, 1.0, 3.0]);
2435        let ib = tensor::value_into_tensor_for("union", eval.ib_value()).expect("ib tensor");
2436        assert_eq!(ib.materialize_f64(), vec![3.0]);
2437    }
2438
2439    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2440    #[test]
2441    fn union_string_rows_stable() {
2442        let a = StringArray::new(
2443            vec![
2444                "alpha".to_string(),
2445                "gamma".to_string(),
2446                "beta".to_string(),
2447                "beta".to_string(),
2448            ],
2449            vec![2, 2],
2450        )
2451        .unwrap();
2452        let b = StringArray::new(
2453            vec![
2454                "gamma".to_string(),
2455                "delta".to_string(),
2456                "beta".to_string(),
2457                "beta".to_string(),
2458            ],
2459            vec![2, 2],
2460        )
2461        .unwrap();
2462        let eval = evaluate_sync(
2463            Value::StringArray(a),
2464            Value::StringArray(b),
2465            &[Value::from("rows"), Value::from("stable")],
2466        )
2467        .expect("union");
2468        match eval.values_value() {
2469            Value::StringArray(arr) => {
2470                assert_eq!(arr.shape, vec![3, 2]);
2471                assert_eq!(
2472                    arr.data,
2473                    vec![
2474                        "alpha".to_string(),
2475                        "gamma".to_string(),
2476                        "delta".to_string(),
2477                        "beta".to_string(),
2478                        "beta".to_string(),
2479                        "beta".to_string()
2480                    ]
2481                );
2482            }
2483            other => panic!("expected string array, got {other:?}"),
2484        }
2485        let ia = tensor::value_into_tensor_for("union", eval.ia_value()).expect("ia tensor");
2486        assert_eq!(ia.materialize_f64(), vec![1.0, 2.0]);
2487        let ib = tensor::value_into_tensor_for("union", eval.ib_value()).expect("ib tensor");
2488        assert_eq!(ib.materialize_f64(), vec![2.0]);
2489    }
2490
2491    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2492    #[test]
2493    fn union_gpu_roundtrip() {
2494        test_support::with_test_provider(|provider| {
2495            let a = Tensor::new(vec![4.0, 1.0, 2.0], vec![3, 1]).unwrap();
2496            let b = Tensor::new(vec![2.0, 5.0], vec![2, 1]).unwrap();
2497            let view_a = HostTensorView {
2498                data: &a.materialize_f64(),
2499                shape: &a.shape,
2500            };
2501            let view_b = HostTensorView {
2502                data: &b.materialize_f64(),
2503                shape: &b.shape,
2504            };
2505            let handle_a = provider.upload(&view_a).expect("upload A");
2506            let handle_b = provider.upload(&view_b).expect("upload B");
2507            let eval = evaluate_sync(
2508                Value::GpuTensor(handle_a),
2509                Value::GpuTensor(handle_b),
2510                &[Value::from("stable")],
2511            )
2512            .expect("union");
2513            let values = tensor::value_into_tensor_for("union", eval.values_value()).unwrap();
2514            assert_eq!(values.materialize_f64(), vec![4.0, 1.0, 2.0, 5.0]);
2515            let ia = tensor::value_into_tensor_for("union", eval.ia_value()).unwrap();
2516            assert_eq!(ia.materialize_f64(), vec![1.0, 2.0, 3.0]);
2517            let ib = tensor::value_into_tensor_for("union", eval.ib_value()).unwrap();
2518            assert_eq!(ib.materialize_f64(), vec![2.0]);
2519        });
2520    }
2521
2522    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2523    #[test]
2524    fn union_rejects_legacy_option() {
2525        let tensor =
2526            Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).expect("tensor construction failed");
2527        let err = evaluate_sync(
2528            Value::Tensor(tensor.clone()),
2529            Value::Tensor(tensor),
2530            &[Value::from("legacy")],
2531        )
2532        .unwrap_err();
2533        assert_eq!(
2534            err.identifier(),
2535            UNION_ERROR_LEGACY_OPTION_UNSUPPORTED.identifier
2536        );
2537    }
2538
2539    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2540    #[test]
2541    fn union_rejects_conflicting_order_options() {
2542        let tensor = Tensor::new(vec![1.0, 2.0], vec![2, 1]).expect("tensor construction failed");
2543        let err = evaluate_sync(
2544            Value::Tensor(tensor.clone()),
2545            Value::Tensor(tensor),
2546            &[Value::from("stable"), Value::from("sorted")],
2547        )
2548        .unwrap_err();
2549        assert_eq!(
2550            err.identifier(),
2551            UNION_ERROR_CONFLICTING_ORDER_OPTIONS.identifier
2552        );
2553    }
2554
2555    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2556    #[test]
2557    fn union_rejects_unknown_option() {
2558        let tensor = Tensor::new(vec![1.0, 2.0], vec![2, 1]).expect("tensor construction failed");
2559        let err = evaluate_sync(
2560            Value::Tensor(tensor.clone()),
2561            Value::Tensor(tensor),
2562            &[Value::from("bogus")],
2563        )
2564        .unwrap_err();
2565        assert_eq!(err.identifier(), UNION_ERROR_UNKNOWN_OPTION.identifier);
2566    }
2567
2568    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2569    #[test]
2570    fn union_rows_dimension_mismatch() {
2571        let a = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]).unwrap();
2572        let b = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
2573        let err =
2574            evaluate_sync(Value::Tensor(a), Value::Tensor(b), &[Value::from("rows")]).unwrap_err();
2575        assert_eq!(
2576            err.identifier(),
2577            UNION_ERROR_ROWS_COLUMN_MISMATCH.identifier
2578        );
2579    }
2580
2581    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2582    #[test]
2583    fn union_requires_matching_types() {
2584        let a = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
2585        let b = CharArray::new(vec!['a', 'b'], 1, 2).unwrap();
2586        let err = union_host(
2587            Value::Tensor(a),
2588            Value::CharArray(b),
2589            &UnionOptions {
2590                rows: false,
2591                order: UnionOrder::Sorted,
2592            },
2593        )
2594        .unwrap_err();
2595        assert_eq!(
2596            err.identifier(),
2597            UNION_ERROR_UNSUPPORTED_INPUT_TYPE.identifier
2598        );
2599    }
2600
2601    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2602    #[test]
2603    fn union_accepts_scalar_inputs() {
2604        let eval =
2605            evaluate_sync(Value::Int(IntValue::I32(1)), Value::Num(3.0), &[]).expect("union");
2606        match eval.values_value() {
2607            Value::Tensor(t) => {
2608                assert_eq!(t.materialize_f64(), vec![1.0, 3.0]);
2609                assert_eq!(t.shape, vec![2, 1]);
2610            }
2611            other => panic!("expected numeric tensor, got {other:?}"),
2612        }
2613        let ia = tensor::value_into_tensor_for("union", eval.ia_value()).unwrap();
2614        assert_eq!(ia.materialize_f64(), vec![1.0]);
2615        let ib = tensor::value_into_tensor_for("union", eval.ib_value()).unwrap();
2616        assert_eq!(ib.materialize_f64(), vec![1.0]);
2617    }
2618
2619    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2620    #[test]
2621    #[cfg(feature = "wgpu")]
2622    fn union_wgpu_matches_cpu() {
2623        let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
2624            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
2625        );
2626        let a = Tensor::new(vec![4.0, 1.0, 2.0, 3.0], vec![4, 1]).unwrap();
2627        let b = Tensor::new(vec![2.0, 6.0, 3.0], vec![3, 1]).unwrap();
2628
2629        let cpu_eval =
2630            evaluate_sync(Value::Tensor(a.clone()), Value::Tensor(b.clone()), &[]).expect("union");
2631        let cpu_values = tensor::value_into_tensor_for("union", cpu_eval.values_value()).unwrap();
2632        let cpu_ia = tensor::value_into_tensor_for("union", cpu_eval.ia_value()).unwrap();
2633        let cpu_ib = tensor::value_into_tensor_for("union", cpu_eval.ib_value()).unwrap();
2634
2635        let provider = runmat_accelerate_api::provider().expect("provider");
2636        let view_a = HostTensorView {
2637            data: &a.materialize_f64(),
2638            shape: &a.shape,
2639        };
2640        let view_b = HostTensorView {
2641            data: &b.materialize_f64(),
2642            shape: &b.shape,
2643        };
2644        let handle_a = provider.upload(&view_a).expect("upload A");
2645        let handle_b = provider.upload(&view_b).expect("upload B");
2646        let gpu_eval = evaluate_sync(Value::GpuTensor(handle_a), Value::GpuTensor(handle_b), &[])
2647            .expect("union");
2648        let gpu_values = tensor::value_into_tensor_for("union", gpu_eval.values_value()).unwrap();
2649        let gpu_ia = tensor::value_into_tensor_for("union", gpu_eval.ia_value()).unwrap();
2650        let gpu_ib = tensor::value_into_tensor_for("union", gpu_eval.ib_value()).unwrap();
2651
2652        assert_eq!(gpu_values.materialize_f64(), cpu_values.materialize_f64());
2653        assert_eq!(gpu_values.shape, cpu_values.shape);
2654        assert_eq!(gpu_ia.materialize_f64(), cpu_ia.materialize_f64());
2655        assert_eq!(gpu_ib.materialize_f64(), cpu_ib.materialize_f64());
2656    }
2657}