Skip to main content

runmat_runtime/builtins/acceleration/gpu/
gpuarray.rs

1//! MATLAB-compatible `gpuArray` builtin that uploads host data to the active accelerator.
2//!
3//! The implementation mirrors MathWorks MATLAB semantics, including optional
4//! size arguments, `'like'` prototypes, and explicit dtype toggles. When no
5//! acceleration provider is registered the builtin surfaces a MATLAB-style
6//! error, ensuring callers know residency could not be established.
7
8use crate::builtins::acceleration::gpu::type_resolvers::gpuarray_type;
9use crate::builtins::common::spec::{
10    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
11    ProviderHook, ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
12};
13use crate::builtins::common::{gpu_helpers, tensor};
14use runmat_accelerate_api::{GpuTensorHandle, HostTensorView, ProviderPrecision};
15use runmat_builtins::{
16    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
17    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
18    CharArray, ComplexTensor, IntValue, Tensor, Value,
19};
20use runmat_macros::runtime_builtin;
21
22use crate::{build_runtime_error, BuiltinResult, RuntimeError};
23
24const BUILTIN_NAME: &str = "gpuArray";
25
26const GPUARRAY_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
27    name: "G",
28    ty: BuiltinParamType::Any,
29    arity: BuiltinParamArity::Required,
30    default: None,
31    description: "GPU-resident handle containing uploaded/converted data.",
32}];
33
34const GPUARRAY_INPUTS_BASE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
35    name: "X",
36    ty: BuiltinParamType::Any,
37    arity: BuiltinParamArity::Required,
38    default: None,
39    description: "Input value to upload or recast on GPU.",
40}];
41
42const GPUARRAY_INPUTS_DIMS: [BuiltinParamDescriptor; 2] = [
43    BuiltinParamDescriptor {
44        name: "X",
45        ty: BuiltinParamType::Any,
46        arity: BuiltinParamArity::Required,
47        default: None,
48        description: "Input value to upload or recast on GPU.",
49    },
50    BuiltinParamDescriptor {
51        name: "dim",
52        ty: BuiltinParamType::SizeArg,
53        arity: BuiltinParamArity::Variadic,
54        default: None,
55        description: "Reshape dimensions (scalar dims or a single size vector tensor).",
56    },
57];
58
59const GPUARRAY_INPUTS_DTYPE: [BuiltinParamDescriptor; 2] = [
60    BuiltinParamDescriptor {
61        name: "X",
62        ty: BuiltinParamType::Any,
63        arity: BuiltinParamArity::Required,
64        default: None,
65        description: "Input value to upload or recast on GPU.",
66    },
67    BuiltinParamDescriptor {
68        name: "dtype",
69        ty: BuiltinParamType::StringScalar,
70        arity: BuiltinParamArity::Required,
71        default: Some("\"double\""),
72        description: "Class tag such as `single`, `int32`, `uint8`, `logical`, or `double`.",
73    },
74];
75
76const GPUARRAY_INPUTS_LIKE: [BuiltinParamDescriptor; 3] = [
77    BuiltinParamDescriptor {
78        name: "X",
79        ty: BuiltinParamType::Any,
80        arity: BuiltinParamArity::Required,
81        default: None,
82        description: "Input value to upload or recast on GPU.",
83    },
84    BuiltinParamDescriptor {
85        name: "like",
86        ty: BuiltinParamType::StringScalar,
87        arity: BuiltinParamArity::Required,
88        default: None,
89        description: "Literal keyword `\"like\"`.",
90    },
91    BuiltinParamDescriptor {
92        name: "prototype",
93        ty: BuiltinParamType::LikePrototype,
94        arity: BuiltinParamArity::Required,
95        default: None,
96        description: "Prototype value whose class drives output conversion.",
97    },
98];
99
100const GPUARRAY_INPUTS_DIMS_OPTIONS: [BuiltinParamDescriptor; 3] = [
101    BuiltinParamDescriptor {
102        name: "X",
103        ty: BuiltinParamType::Any,
104        arity: BuiltinParamArity::Required,
105        default: None,
106        description: "Input value to upload or recast on GPU.",
107    },
108    BuiltinParamDescriptor {
109        name: "dim",
110        ty: BuiltinParamType::SizeArg,
111        arity: BuiltinParamArity::Variadic,
112        default: None,
113        description: "Reshape dimensions (scalar dims or a single size vector tensor).",
114    },
115    BuiltinParamDescriptor {
116        name: "option",
117        ty: BuiltinParamType::Any,
118        arity: BuiltinParamArity::Variadic,
119        default: None,
120        description: "Class tags and/or `\"like\", prototype` qualifiers.",
121    },
122];
123
124const GPUARRAY_SIGNATURES: [BuiltinSignatureDescriptor; 5] = [
125    BuiltinSignatureDescriptor {
126        label: "G = gpuArray(X)",
127        inputs: &GPUARRAY_INPUTS_BASE,
128        outputs: &GPUARRAY_OUTPUT,
129    },
130    BuiltinSignatureDescriptor {
131        label: "G = gpuArray(X, dim, ...)",
132        inputs: &GPUARRAY_INPUTS_DIMS,
133        outputs: &GPUARRAY_OUTPUT,
134    },
135    BuiltinSignatureDescriptor {
136        label: "G = gpuArray(X, dtype)",
137        inputs: &GPUARRAY_INPUTS_DTYPE,
138        outputs: &GPUARRAY_OUTPUT,
139    },
140    BuiltinSignatureDescriptor {
141        label: "G = gpuArray(X, \"like\", prototype)",
142        inputs: &GPUARRAY_INPUTS_LIKE,
143        outputs: &GPUARRAY_OUTPUT,
144    },
145    BuiltinSignatureDescriptor {
146        label: "G = gpuArray(X, dim, ..., option, ...)",
147        inputs: &GPUARRAY_INPUTS_DIMS_OPTIONS,
148        outputs: &GPUARRAY_OUTPUT,
149    },
150];
151
152const GPUARRAY_ERROR_NO_PROVIDER: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
153    code: "RM.GPUARRAY.NO_PROVIDER",
154    identifier: Some("RunMat:gpuArray:NoProvider"),
155    when: "No acceleration provider is registered for host/device transfers.",
156    message: "gpuArray: no acceleration provider registered",
157};
158
159const GPUARRAY_ERROR_OPTION_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
160    code: "RM.GPUARRAY.OPTION_ARGUMENT",
161    identifier: Some("RunMat:gpuArray:OptionArgument"),
162    when: "Option tail contains non-text values where class tags/keywords are expected.",
163    message: "gpuArray: invalid option argument",
164};
165
166const GPUARRAY_ERROR_LIKE_MISSING: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
167    code: "RM.GPUARRAY.LIKE_MISSING",
168    identifier: Some("RunMat:gpuArray:LikeMissingPrototype"),
169    when: "Keyword `like` is supplied without a following prototype value.",
170    message: "gpuArray: expected a prototype value after 'like'",
171};
172
173const GPUARRAY_ERROR_LIKE_DUPLICATE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
174    code: "RM.GPUARRAY.LIKE_DUPLICATE",
175    identifier: Some("RunMat:gpuArray:LikeDuplicate"),
176    when: "Keyword `like` appears more than once.",
177    message: "gpuArray: duplicate 'like' qualifier",
178};
179
180const GPUARRAY_ERROR_CODISTRIBUTED_UNSUPPORTED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
181    code: "RM.GPUARRAY.CODISTRIBUTED_UNSUPPORTED",
182    identifier: Some("RunMat:gpuArray:CodistributedUnsupported"),
183    when: "Distributed/codistributed qualifiers are requested.",
184    message: "gpuArray: codistributed arrays are not supported yet",
185};
186
187const GPUARRAY_ERROR_CONFLICTING_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
188    code: "RM.GPUARRAY.CONFLICTING_TYPE",
189    identifier: Some("RunMat:gpuArray:ConflictingTypeQualifiers"),
190    when: "Multiple incompatible class qualifiers are supplied.",
191    message: "gpuArray: conflicting type qualifiers supplied",
192};
193
194const GPUARRAY_ERROR_UNKNOWN_OPTION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
195    code: "RM.GPUARRAY.UNKNOWN_OPTION",
196    identifier: Some("RunMat:gpuArray:UnknownOption"),
197    when: "Text option is not a recognized class/keyword token.",
198    message: "gpuArray: unrecognised option",
199};
200
201const GPUARRAY_ERROR_SIZE_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
202    code: "RM.GPUARRAY.SIZE_ARGUMENT",
203    identifier: Some("RunMat:gpuArray:InvalidSizeArgument"),
204    when: "Size arguments are malformed (not finite integers, negative, or invalid combinations).",
205    message: "gpuArray: invalid size argument",
206};
207
208const GPUARRAY_ERROR_LIKE_PROTOTYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
209    code: "RM.GPUARRAY.LIKE_PROTOTYPE",
210    identifier: Some("RunMat:gpuArray:InvalidLikePrototype"),
211    when: "`like` prototype is unsupported for type inference.",
212    message: "gpuArray: invalid 'like' prototype",
213};
214
215const GPUARRAY_ERROR_INPUT_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
216    code: "RM.GPUARRAY.INPUT_TYPE",
217    identifier: Some("RunMat:gpuArray:UnsupportedInputType"),
218    when: "Input value type cannot be uploaded/coerced to supported gpuArray storage.",
219    message: "gpuArray: unsupported input type",
220};
221
222const GPUARRAY_ERROR_CONVERSION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
223    code: "RM.GPUARRAY.CONVERSION",
224    identifier: Some("RunMat:gpuArray:ConversionFailed"),
225    when: "Requested dtype conversion cannot be performed (for example NaN->logical).",
226    message: "gpuArray: conversion failed",
227};
228
229const GPUARRAY_ERROR_RESHAPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
230    code: "RM.GPUARRAY.RESHAPE",
231    identifier: Some("RunMat:gpuArray:ReshapeMismatch"),
232    when: "Requested shape does not preserve the element count.",
233    message: "gpuArray: cannot reshape gpuArray into requested size",
234};
235
236const GPUARRAY_ERROR_PROVIDER_IO: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
237    code: "RM.GPUARRAY.PROVIDER_IO",
238    identifier: Some("RunMat:gpuArray:ProviderIO"),
239    when: "Provider upload/download interaction fails.",
240    message: "gpuArray: provider I/O failed",
241};
242
243const GPUARRAY_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
244    code: "RM.GPUARRAY.INTERNAL",
245    identifier: Some("RunMat:gpuArray:InternalError"),
246    when: "Internal tensor/container conversion fails.",
247    message: "gpuArray: internal error",
248};
249
250const GPUARRAY_ERRORS: [BuiltinErrorDescriptor; 14] = [
251    GPUARRAY_ERROR_NO_PROVIDER,
252    GPUARRAY_ERROR_OPTION_ARGUMENT,
253    GPUARRAY_ERROR_LIKE_MISSING,
254    GPUARRAY_ERROR_LIKE_DUPLICATE,
255    GPUARRAY_ERROR_CODISTRIBUTED_UNSUPPORTED,
256    GPUARRAY_ERROR_CONFLICTING_TYPE,
257    GPUARRAY_ERROR_UNKNOWN_OPTION,
258    GPUARRAY_ERROR_SIZE_ARGUMENT,
259    GPUARRAY_ERROR_LIKE_PROTOTYPE,
260    GPUARRAY_ERROR_INPUT_TYPE,
261    GPUARRAY_ERROR_CONVERSION,
262    GPUARRAY_ERROR_RESHAPE,
263    GPUARRAY_ERROR_PROVIDER_IO,
264    GPUARRAY_ERROR_INTERNAL,
265];
266
267pub const GPUARRAY_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
268    signatures: &GPUARRAY_SIGNATURES,
269    output_mode: BuiltinOutputMode::Fixed,
270    completion_policy: BuiltinCompletionPolicy::Public,
271    errors: &GPUARRAY_ERRORS,
272};
273
274fn gpu_array_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
275    gpu_array_error_with_message(error.message, error)
276}
277
278fn gpu_array_error_with_message(
279    message: impl Into<String>,
280    error: &'static BuiltinErrorDescriptor,
281) -> RuntimeError {
282    let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
283    if let Some(identifier) = error.identifier {
284        builder = builder.with_identifier(identifier);
285    }
286    builder.build()
287}
288
289fn gpu_array_error_with_detail(
290    error: &'static BuiltinErrorDescriptor,
291    detail: impl AsRef<str>,
292) -> RuntimeError {
293    gpu_array_error_with_message(format!("{}: {}", error.message, detail.as_ref()), error)
294}
295
296#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::acceleration::gpu::gpuarray")]
297pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
298    name: "gpuArray",
299    op_kind: GpuOpKind::Custom("upload"),
300    supported_precisions: &[ScalarType::F32, ScalarType::F64],
301    broadcast: BroadcastSemantics::None,
302    provider_hooks: &[ProviderHook::Custom("upload")],
303    constant_strategy: ConstantStrategy::InlineLiteral,
304    residency: ResidencyPolicy::NewHandle,
305    nan_mode: ReductionNaN::Include,
306    two_pass_threshold: None,
307    workgroup_size: None,
308    accepts_nan_mode: false,
309    notes: "Invokes the provider `upload` hook, including complex interleaved uploads, and reuploads gpuArray inputs when dtype conversion is requested. Handles class strings, size vectors, and `'like'` prototypes.",
310};
311
312#[runmat_macros::register_fusion_spec(
313    builtin_path = "crate::builtins::acceleration::gpu::gpuarray"
314)]
315pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
316    name: "gpuArray",
317    shape: ShapeRequirements::Any,
318    constant_strategy: ConstantStrategy::InlineLiteral,
319    elementwise: None,
320    reduction: None,
321    emits_nan: false,
322    notes:
323        "Acts as a residency boundary; fusion graphs never cross explicit host↔device transfers.",
324};
325
326#[runtime_builtin(
327    name = "gpuArray",
328    category = "acceleration/gpu",
329    summary = "Move data to the GPU as gpuArray values.",
330    keywords = "gpuArray,gpu,accelerate,upload,dtype,like",
331    examples = "G = gpuArray([1 2 3], 'single');",
332    accel = "array_construct",
333    type_resolver(gpuarray_type),
334    descriptor(crate::builtins::acceleration::gpu::gpuarray::GPUARRAY_DESCRIPTOR),
335    builtin_path = "crate::builtins::acceleration::gpu::gpuarray"
336)]
337async fn gpu_array_builtin(value: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
338    let options = parse_options(&rest)?;
339    let incoming_precision = match &value {
340        Value::GpuTensor(handle) => runmat_accelerate_api::handle_precision(handle),
341        _ => None,
342    };
343    let dtype = resolve_dtype(&value, &options)?;
344    let dims = options.dims.clone();
345
346    let prepared = match value {
347        Value::GpuTensor(handle) => convert_device_value(handle, dtype).await?,
348        other => upload_host_value(other, dtype)?,
349    };
350
351    let mut handle = prepared.handle;
352
353    if let Some(dims) = dims.as_ref() {
354        apply_dims(&mut handle, dims)?;
355    }
356
357    let provider_precision = runmat_accelerate_api::provider()
358        .map(|p| p.precision())
359        .unwrap_or(ProviderPrecision::F64);
360    let requested_precision = match dtype {
361        DataClass::Single => Some(ProviderPrecision::F32),
362        _ => None,
363    };
364    let final_precision = requested_precision
365        .or(incoming_precision)
366        .unwrap_or(provider_precision);
367    runmat_accelerate_api::set_handle_precision(&handle, final_precision);
368
369    runmat_accelerate_api::set_handle_logical(&handle, prepared.logical);
370    runmat_accelerate_api::set_handle_class_name(&handle, dtype.class_name());
371
372    Ok(Value::GpuTensor(handle))
373}
374
375#[derive(Clone, Copy, Debug, PartialEq, Eq)]
376enum DataClass {
377    Double,
378    Single,
379    Logical,
380    Int8,
381    Int16,
382    Int32,
383    Int64,
384    UInt8,
385    UInt16,
386    UInt32,
387    UInt64,
388}
389
390impl DataClass {
391    fn from_tag(tag: &str) -> Option<Self> {
392        match tag {
393            "double" => Some(Self::Double),
394            "single" | "float32" => Some(Self::Single),
395            "logical" | "bool" | "boolean" => Some(Self::Logical),
396            "int8" => Some(Self::Int8),
397            "int16" => Some(Self::Int16),
398            "int32" | "int" => Some(Self::Int32),
399            "int64" => Some(Self::Int64),
400            "uint8" => Some(Self::UInt8),
401            "uint16" => Some(Self::UInt16),
402            "uint32" => Some(Self::UInt32),
403            "uint64" => Some(Self::UInt64),
404            "gpuarray" => None, // compatibility no-op
405            _ => None,
406        }
407    }
408
409    fn class_name(self) -> &'static str {
410        match self {
411            Self::Double => "double",
412            Self::Single => "single",
413            Self::Logical => "logical",
414            Self::Int8 => "int8",
415            Self::Int16 => "int16",
416            Self::Int32 => "int32",
417            Self::Int64 => "int64",
418            Self::UInt8 => "uint8",
419            Self::UInt16 => "uint16",
420            Self::UInt32 => "uint32",
421            Self::UInt64 => "uint64",
422        }
423    }
424}
425
426#[derive(Debug, Default)]
427struct ParsedOptions {
428    dims: Option<Vec<usize>>,
429    explicit_dtype: Option<DataClass>,
430    prototype: Option<Value>,
431}
432
433fn parse_options(rest: &[Value]) -> BuiltinResult<ParsedOptions> {
434    let (index_after_dims, dims) = parse_size_arguments(rest)?;
435    let mut options = ParsedOptions {
436        dims,
437        ..ParsedOptions::default()
438    };
439
440    let mut idx = index_after_dims;
441    while idx < rest.len() {
442        let tag = value_to_lower_string(&rest[idx]).ok_or_else(|| {
443            gpu_array_error_with_message(
444                format!(
445                "gpuArray: unexpected argument {:?}; expected a class string or the keyword 'like'",
446                rest[idx]
447                ),
448                &GPUARRAY_ERROR_OPTION_ARGUMENT,
449            )
450        })?;
451
452        match tag.as_str() {
453            "like" => {
454                idx += 1;
455                if idx >= rest.len() {
456                    return Err(gpu_array_error(&GPUARRAY_ERROR_LIKE_MISSING));
457                }
458                if options.prototype.is_some() {
459                    return Err(gpu_array_error(&GPUARRAY_ERROR_LIKE_DUPLICATE));
460                }
461                options.prototype = Some(rest[idx].clone());
462            }
463            "distributed" | "codistributed" => {
464                return Err(gpu_array_error(&GPUARRAY_ERROR_CODISTRIBUTED_UNSUPPORTED));
465            }
466            tag => {
467                if let Some(class) = DataClass::from_tag(tag) {
468                    if let Some(existing) = options.explicit_dtype {
469                        if existing != class {
470                            return Err(gpu_array_error(&GPUARRAY_ERROR_CONFLICTING_TYPE));
471                        }
472                    } else {
473                        options.explicit_dtype = Some(class);
474                    }
475                } else if tag != "gpuarray" {
476                    return Err(gpu_array_error_with_detail(
477                        &GPUARRAY_ERROR_UNKNOWN_OPTION,
478                        format!("unrecognised option '{tag}'"),
479                    ));
480                }
481            }
482        }
483
484        idx += 1;
485    }
486
487    Ok(options)
488}
489
490fn parse_size_arguments(rest: &[Value]) -> BuiltinResult<(usize, Option<Vec<usize>>)> {
491    let mut idx = 0;
492    let mut dims: Vec<usize> = Vec::new();
493    let mut vector_consumed = false;
494
495    while idx < rest.len() {
496        // Stop at textual qualifiers only; numeric values continue parsing as size args.
497        match &rest[idx] {
498            Value::String(_) | Value::StringArray(_) | Value::CharArray(_) => break,
499            _ => {}
500        }
501
502        match &rest[idx] {
503            Value::Int(i) => {
504                dims.push(int_to_dim(i)?);
505            }
506            Value::Num(n) => {
507                dims.push(float_to_dim(*n)?);
508            }
509            Value::Tensor(t) => {
510                if vector_consumed || !dims.is_empty() {
511                    return Err(gpu_array_error_with_message(
512                        "gpuArray: size vectors cannot be combined with scalar dimensions",
513                        &GPUARRAY_ERROR_SIZE_ARGUMENT,
514                    ));
515                }
516                dims = tensor_to_dims(t)?;
517                vector_consumed = true;
518            }
519            _ => break,
520        }
521        idx += 1;
522    }
523
524    let dims_option = if dims.is_empty() { None } else { Some(dims) };
525    Ok((idx, dims_option))
526}
527
528fn value_to_lower_string(value: &Value) -> Option<String> {
529    crate::builtins::common::tensor::value_to_string(value).map(|s| s.trim().to_ascii_lowercase())
530}
531
532fn int_to_dim(value: &IntValue) -> BuiltinResult<usize> {
533    let raw = value.to_i64();
534    if raw < 0 {
535        return Err(gpu_array_error_with_message(
536            "gpuArray: size arguments must be non-negative integers",
537            &GPUARRAY_ERROR_SIZE_ARGUMENT,
538        ));
539    }
540    Ok(raw as usize)
541}
542
543fn float_to_dim(value: f64) -> BuiltinResult<usize> {
544    if !value.is_finite() {
545        return Err(gpu_array_error_with_message(
546            "gpuArray: size arguments must be finite integers",
547            &GPUARRAY_ERROR_SIZE_ARGUMENT,
548        ));
549    }
550    let rounded = value.round();
551    if (rounded - value).abs() > f64::EPSILON {
552        return Err(gpu_array_error_with_message(
553            "gpuArray: size arguments must be integers",
554            &GPUARRAY_ERROR_SIZE_ARGUMENT,
555        ));
556    }
557    if rounded < 0.0 {
558        return Err(gpu_array_error_with_message(
559            "gpuArray: size arguments must be non-negative",
560            &GPUARRAY_ERROR_SIZE_ARGUMENT,
561        ));
562    }
563    Ok(rounded as usize)
564}
565
566fn tensor_to_dims(tensor: &Tensor) -> BuiltinResult<Vec<usize>> {
567    let mut dims = Vec::with_capacity(tensor.data.len());
568    for value in &tensor.data {
569        dims.push(float_to_dim(*value)?);
570    }
571    Ok(dims)
572}
573
574fn resolve_dtype(value: &Value, options: &ParsedOptions) -> BuiltinResult<DataClass> {
575    if let Some(explicit) = options.explicit_dtype {
576        return Ok(explicit);
577    }
578    if let Some(prototype) = options.prototype.as_ref() {
579        return infer_dtype_from_prototype(prototype);
580    }
581    if let Value::GpuTensor(handle) = value {
582        return dtype_from_gpu_handle(handle);
583    }
584    if value_defaults_to_logical(value) {
585        return Ok(DataClass::Logical);
586    }
587    Ok(DataClass::Double)
588}
589
590fn infer_dtype_from_prototype(proto: &Value) -> BuiltinResult<DataClass> {
591    match proto {
592        Value::GpuTensor(handle) => dtype_from_gpu_handle(handle),
593        Value::LogicalArray(_) | Value::Bool(_) => Ok(DataClass::Logical),
594        Value::Int(int) => Ok(match int {
595            IntValue::I8(_) => DataClass::Int8,
596            IntValue::I16(_) => DataClass::Int16,
597            IntValue::I32(_) => DataClass::Int32,
598            IntValue::I64(_) => DataClass::Int64,
599            IntValue::U8(_) => DataClass::UInt8,
600            IntValue::U16(_) => DataClass::UInt16,
601            IntValue::U32(_) => DataClass::UInt32,
602            IntValue::U64(_) => DataClass::UInt64,
603        }),
604        Value::Tensor(_) | Value::Num(_) => Ok(DataClass::Double),
605        Value::CharArray(_) => Ok(DataClass::Double),
606        Value::String(_) => Err(gpu_array_error_with_message(
607            "gpuArray: 'like' does not accept MATLAB string scalars; convert to char() first",
608            &GPUARRAY_ERROR_LIKE_PROTOTYPE,
609        )),
610        Value::StringArray(_) => Err(gpu_array_error_with_message(
611            "gpuArray: 'like' does not accept string arrays; convert to char arrays first",
612            &GPUARRAY_ERROR_LIKE_PROTOTYPE,
613        )),
614        Value::Complex(_, _) | Value::ComplexTensor(_) => Ok(DataClass::Double),
615        other => Err(gpu_array_error_with_message(
616            format!(
617                "gpuArray: unsupported 'like' prototype type {other:?}; expected numeric or logical values"
618            ),
619            &GPUARRAY_ERROR_LIKE_PROTOTYPE,
620        )),
621    }
622}
623
624fn dtype_from_gpu_handle(handle: &GpuTensorHandle) -> BuiltinResult<DataClass> {
625    if runmat_accelerate_api::handle_is_logical(handle) {
626        return Ok(DataClass::Logical);
627    }
628    if let Some(class_name) = runmat_accelerate_api::handle_class_name(handle) {
629        if let Some(dtype) = DataClass::from_tag(class_name.trim().to_ascii_lowercase().as_str()) {
630            return Ok(dtype);
631        }
632    }
633    let precision = runmat_accelerate_api::handle_precision(handle).or_else(|| {
634        runmat_accelerate_api::provider_for_handle(handle).map(|provider| provider.precision())
635    });
636    Ok(match precision {
637        Some(ProviderPrecision::F32) => DataClass::Single,
638        Some(ProviderPrecision::F64) | None => DataClass::Double,
639    })
640}
641
642fn value_defaults_to_logical(value: &Value) -> bool {
643    match value {
644        Value::LogicalArray(_) | Value::Bool(_) => true,
645        Value::GpuTensor(handle) => runmat_accelerate_api::handle_is_logical(handle),
646        _ => false,
647    }
648}
649
650struct PreparedHandle {
651    handle: GpuTensorHandle,
652    logical: bool,
653}
654
655fn upload_host_value(value: Value, dtype: DataClass) -> BuiltinResult<PreparedHandle> {
656    #[cfg(all(test, feature = "wgpu"))]
657    {
658        if runmat_accelerate_api::provider().is_none() {
659            let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
660                runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
661            );
662        }
663    }
664    let provider = runmat_accelerate_api::provider()
665        .ok_or_else(|| gpu_array_error(&GPUARRAY_ERROR_NO_PROVIDER))?;
666
667    match value {
668        Value::Complex(re, im) => {
669            let tensor = ComplexTensor::new(vec![(re, im)], vec![1, 1]).map_err(|err| {
670                gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
671            })?;
672            upload_complex_host_value(provider, tensor, dtype)
673        }
674        Value::ComplexTensor(tensor) => upload_complex_host_value(provider, tensor, dtype),
675        value => upload_real_host_value(provider, value, dtype),
676    }
677}
678
679fn upload_real_host_value(
680    provider: &dyn runmat_accelerate_api::AccelProvider,
681    value: Value,
682    dtype: DataClass,
683) -> BuiltinResult<PreparedHandle> {
684    let tensor = coerce_host_value(value)?;
685    let (mut tensor, logical) = cast_tensor(tensor, dtype)?;
686
687    let view = HostTensorView {
688        data: &tensor.data,
689        shape: &tensor.shape,
690    };
691    let new_handle = provider.upload(&view).map_err(|err| {
692        gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_PROVIDER_IO)
693    })?;
694
695    tensor.data.clear();
696
697    Ok(PreparedHandle {
698        handle: new_handle,
699        logical,
700    })
701}
702
703fn upload_complex_host_value(
704    provider: &dyn runmat_accelerate_api::AccelProvider,
705    mut tensor: ComplexTensor,
706    dtype: DataClass,
707) -> BuiltinResult<PreparedHandle> {
708    match dtype {
709        DataClass::Double => {}
710        DataClass::Single => {
711            for (re, im) in &mut tensor.data {
712                *re = (*re as f32) as f64;
713                *im = (*im as f32) as f64;
714            }
715        }
716        _ => {
717            return Err(gpu_array_error_with_message(
718                "gpuArray: complex inputs can only be uploaded as double or single precision",
719                &GPUARRAY_ERROR_INPUT_TYPE,
720            ));
721        }
722    }
723
724    let handle = gpu_helpers::upload_complex_tensor(provider, &tensor).map_err(|err| {
725        gpu_array_error_with_message(err.to_string(), &GPUARRAY_ERROR_PROVIDER_IO)
726    })?;
727    let precision = match dtype {
728        DataClass::Double => runmat_accelerate_api::ProviderPrecision::F64,
729        DataClass::Single => runmat_accelerate_api::ProviderPrecision::F32,
730        _ => unreachable!("complex dtype was validated above"),
731    };
732    runmat_accelerate_api::set_handle_precision(&handle, precision);
733    Ok(PreparedHandle {
734        handle,
735        logical: false,
736    })
737}
738
739async fn convert_device_value(
740    handle: GpuTensorHandle,
741    dtype: DataClass,
742) -> BuiltinResult<PreparedHandle> {
743    let was_logical = runmat_accelerate_api::handle_is_logical(&handle);
744    let was_complex = runmat_accelerate_api::handle_storage(&handle)
745        == runmat_accelerate_api::GpuTensorStorage::ComplexInterleaved;
746    let current_precision = runmat_accelerate_api::handle_precision(&handle);
747    match dtype {
748        DataClass::Double => {
749            if !(was_complex
750                && current_precision == Some(runmat_accelerate_api::ProviderPrecision::F32))
751            {
752                return Ok(PreparedHandle {
753                    handle,
754                    logical: false,
755                });
756            }
757        }
758        DataClass::Logical => {
759            if was_logical {
760                return Ok(PreparedHandle {
761                    handle,
762                    logical: true,
763                });
764            }
765        }
766        _ => {}
767    }
768
769    let provider = runmat_accelerate_api::provider_for_handle(&handle)
770        .or_else(runmat_accelerate_api::provider)
771        .ok_or_else(|| gpu_array_error(&GPUARRAY_ERROR_NO_PROVIDER))?;
772    if was_complex {
773        let gathered = gpu_helpers::gather_value_async(&Value::GpuTensor(handle.clone()))
774            .await
775            .map_err(|err| {
776                gpu_array_error_with_message(err.to_string(), &GPUARRAY_ERROR_PROVIDER_IO)
777            })?;
778        let Value::ComplexTensor(tensor) = gathered else {
779            return Err(gpu_array_error_with_message(
780                "gpuArray: expected complex gpuArray data during conversion",
781                &GPUARRAY_ERROR_PROVIDER_IO,
782            ));
783        };
784        let prepared = upload_complex_host_value(provider, tensor, dtype)?;
785        provider.free(&handle).ok();
786        return Ok(prepared);
787    }
788
789    let tensor = gpu_helpers::gather_tensor_async(&handle)
790        .await
791        .map_err(|err| {
792            gpu_array_error_with_message(err.to_string(), &GPUARRAY_ERROR_PROVIDER_IO)
793        })?;
794    let (mut tensor, logical) = cast_tensor(tensor, dtype)?;
795
796    let view = HostTensorView {
797        data: &tensor.data,
798        shape: &tensor.shape,
799    };
800    let new_handle = provider.upload(&view).map_err(|err| {
801        gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_PROVIDER_IO)
802    })?;
803
804    provider.free(&handle).ok();
805    tensor.data.clear();
806
807    Ok(PreparedHandle {
808        handle: new_handle,
809        logical,
810    })
811}
812
813fn coerce_host_value(value: Value) -> BuiltinResult<Tensor> {
814    match value {
815        Value::Tensor(t) => Ok(t),
816        Value::LogicalArray(logical) => tensor::logical_to_tensor(&logical).map_err(|err| {
817            gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
818        }),
819        Value::Bool(flag) => {
820            Tensor::new(vec![if flag { 1.0 } else { 0.0 }], vec![1, 1]).map_err(|err| {
821                gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
822            })
823        }
824        Value::Num(n) => Tensor::new(vec![n], vec![1, 1]).map_err(|err| {
825            gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
826        }),
827        Value::Int(i) => Tensor::new(vec![i.to_f64()], vec![1, 1]).map_err(|err| {
828            gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
829        }),
830        Value::CharArray(ca) => char_array_to_tensor(&ca),
831        Value::String(text) => {
832            let ca = CharArray::new_row(&text);
833            char_array_to_tensor(&ca)
834        }
835        Value::StringArray(_) => Err(gpu_array_error_with_message(
836            "gpuArray: string arrays are not supported yet; convert to char arrays with CHAR first",
837            &GPUARRAY_ERROR_INPUT_TYPE,
838        )),
839        Value::Complex(_, _) | Value::ComplexTensor(_) => Err(gpu_array_error_with_message(
840            "gpuArray: internal complex upload routing failed",
841            &GPUARRAY_ERROR_INTERNAL,
842        )),
843        other => Err(gpu_array_error_with_detail(
844            &GPUARRAY_ERROR_INPUT_TYPE,
845            format!("unsupported input type for GPU transfer: {other:?}"),
846        )),
847    }
848}
849
850fn cast_tensor(mut tensor: Tensor, dtype: DataClass) -> BuiltinResult<(Tensor, bool)> {
851    let logical = match dtype {
852        DataClass::Logical => {
853            convert_to_logical(&mut tensor.data)?;
854            true
855        }
856        DataClass::Single => {
857            convert_to_single(&mut tensor.data);
858            false
859        }
860        DataClass::Int8 => {
861            convert_to_int_range(&mut tensor.data, i8::MIN as f64, i8::MAX as f64);
862            false
863        }
864        DataClass::Int16 => {
865            convert_to_int_range(&mut tensor.data, i16::MIN as f64, i16::MAX as f64);
866            false
867        }
868        DataClass::Int32 => {
869            convert_to_int_range(&mut tensor.data, i32::MIN as f64, i32::MAX as f64);
870            false
871        }
872        DataClass::Int64 => {
873            convert_to_int_range(&mut tensor.data, i64::MIN as f64, i64::MAX as f64);
874            false
875        }
876        DataClass::UInt8 => {
877            convert_to_int_range(&mut tensor.data, 0.0, u8::MAX as f64);
878            false
879        }
880        DataClass::UInt16 => {
881            convert_to_int_range(&mut tensor.data, 0.0, u16::MAX as f64);
882            false
883        }
884        DataClass::UInt32 => {
885            convert_to_int_range(&mut tensor.data, 0.0, u32::MAX as f64);
886            false
887        }
888        DataClass::UInt64 => {
889            convert_to_int_range(&mut tensor.data, 0.0, u64::MAX as f64);
890            false
891        }
892        DataClass::Double => false,
893    };
894
895    Ok((tensor, logical))
896}
897
898fn convert_to_logical(data: &mut [f64]) -> BuiltinResult<()> {
899    for value in data.iter_mut() {
900        if value.is_nan() {
901            return Err(gpu_array_error_with_message(
902                "gpuArray: cannot convert NaN to logical",
903                &GPUARRAY_ERROR_CONVERSION,
904            ));
905        }
906        *value = if *value != 0.0 { 1.0 } else { 0.0 };
907    }
908    Ok(())
909}
910
911fn convert_to_single(data: &mut [f64]) {
912    for value in data.iter_mut() {
913        *value = (*value as f32) as f64;
914    }
915}
916
917fn convert_to_int_range(data: &mut [f64], min: f64, max: f64) {
918    for value in data.iter_mut() {
919        if value.is_nan() {
920            *value = min;
921            continue;
922        }
923        if value.is_infinite() {
924            *value = if value.is_sign_negative() { min } else { max };
925            continue;
926        }
927        let rounded = value.round();
928        *value = rounded.clamp(min, max);
929    }
930}
931
932fn apply_dims(handle: &mut GpuTensorHandle, dims: &[usize]) -> BuiltinResult<()> {
933    let new_elems: usize = dims.iter().product();
934    let current_elems: usize = if handle.shape.is_empty() {
935        new_elems
936    } else {
937        handle.shape.iter().product()
938    };
939    if new_elems != current_elems {
940        return Err(gpu_array_error_with_message(
941            format!(
942                "gpuArray: cannot reshape gpuArray of {current_elems} elements into size {:?}",
943                dims
944            ),
945            &GPUARRAY_ERROR_RESHAPE,
946        ));
947    }
948    handle.shape = dims.to_vec();
949    Ok(())
950}
951
952fn char_array_to_tensor(ca: &CharArray) -> BuiltinResult<Tensor> {
953    let rows = ca.rows;
954    let cols = ca.cols;
955    if rows == 0 || cols == 0 {
956        return Tensor::new(Vec::new(), vec![rows, cols]).map_err(|err| {
957            gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
958        });
959    }
960    let mut data = vec![0.0; rows * cols];
961    // Store in row-major to preserve the original character order when interpreted with column-major indexing
962    for row in 0..rows {
963        for col in 0..cols {
964            let idx_char = row * cols + col;
965            let ch = ca.data[idx_char];
966            data[row * cols + col] = ch as u32 as f64;
967        }
968    }
969    Tensor::new(data, vec![rows, cols]).map_err(|err| {
970        gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
971    })
972}
973
974#[cfg(test)]
975pub(crate) mod tests {
976    use super::*;
977    use crate::builtins::common::test_support;
978    use futures::executor::block_on;
979    use runmat_accelerate_api::{GpuTensorStorage, HostTensorView};
980    use runmat_builtins::{ComplexTensor, IntValue, LogicalArray, ResolveContext, Type};
981
982    fn call(value: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
983        block_on(gpu_array_builtin(value, rest))
984    }
985
986    fn gather_complex(value: Value) -> ComplexTensor {
987        match block_on(crate::dispatcher::gather_if_needed_async(&value)).expect("gather complex") {
988            Value::ComplexTensor(tensor) => tensor,
989            other => panic!("expected ComplexTensor, got {other:?}"),
990        }
991    }
992
993    fn assert_complex_close(actual: &[(f64, f64)], expected: &[(f64, f64)]) {
994        assert_eq!(actual.len(), expected.len());
995        for (idx, ((ar, ai), (er, ei))) in actual.iter().zip(expected.iter()).enumerate() {
996            assert!(
997                (ar - er).abs() < 1e-12 && (ai - ei).abs() < 1e-12,
998                "at {idx}: expected ({er}, {ei}), got ({ar}, {ai})"
999            );
1000        }
1001    }
1002
1003    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1004    #[test]
1005    fn gpu_array_transfers_numeric_tensor() {
1006        test_support::with_test_provider(|_| {
1007            let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]).unwrap();
1008            let result = call(Value::Tensor(tensor.clone()), Vec::new()).expect("gpuArray upload");
1009            let Value::GpuTensor(handle) = result else {
1010                panic!("expected gpu tensor");
1011            };
1012            assert_eq!(handle.shape, tensor.shape);
1013            let gathered =
1014                test_support::gather(Value::GpuTensor(handle.clone())).expect("gather values");
1015            assert_eq!(gathered.shape, tensor.shape);
1016            assert_eq!(gathered.data, tensor.data);
1017        });
1018    }
1019
1020    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1021    #[test]
1022    fn gpu_array_marks_logical_inputs() {
1023        test_support::with_test_provider(|_| {
1024            let logical =
1025                LogicalArray::new(vec![1, 0, 1, 1], vec![2, 2]).expect("logical construction");
1026            let result =
1027                call(Value::LogicalArray(logical.clone()), Vec::new()).expect("gpuArray logical");
1028            let Value::GpuTensor(handle) = result else {
1029                panic!("expected gpu tensor");
1030            };
1031            assert!(runmat_accelerate_api::handle_is_logical(&handle));
1032            let gathered =
1033                test_support::gather(Value::GpuTensor(handle.clone())).expect("gather logical");
1034            assert_eq!(gathered.shape, logical.shape);
1035            assert_eq!(gathered.data, vec![1.0, 0.0, 1.0, 1.0]);
1036        });
1037    }
1038
1039    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1040    #[test]
1041    fn gpu_array_uploads_complex_tensor() {
1042        test_support::with_test_provider(|_| {
1043            let complex = ComplexTensor::new(vec![(1.0, -2.0), (3.5, 4.25)], vec![1, 2]).unwrap();
1044            let result =
1045                call(Value::ComplexTensor(complex.clone()), Vec::new()).expect("gpuArray complex");
1046            let Value::GpuTensor(handle) = result else {
1047                panic!("expected gpu tensor");
1048            };
1049            assert_eq!(
1050                runmat_accelerate_api::handle_storage(&handle),
1051                GpuTensorStorage::ComplexInterleaved
1052            );
1053            let gathered = gather_complex(Value::GpuTensor(handle.clone()));
1054            assert_eq!(gathered.shape, complex.shape);
1055            assert_complex_close(&gathered.data, &complex.data);
1056        });
1057    }
1058
1059    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1060    #[test]
1061    fn gpu_array_handles_scalar_bool() {
1062        test_support::with_test_provider(|_| {
1063            let result = call(Value::Bool(true), Vec::new()).expect("gpuArray bool");
1064            let Value::GpuTensor(handle) = result else {
1065                panic!("expected gpu tensor");
1066            };
1067            assert!(runmat_accelerate_api::handle_is_logical(&handle));
1068            let gathered =
1069                test_support::gather(Value::GpuTensor(handle.clone())).expect("gather bool");
1070            assert_eq!(gathered.shape, vec![1, 1]);
1071            assert_eq!(gathered.data, vec![1.0]);
1072        });
1073    }
1074
1075    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1076    #[test]
1077    fn gpu_array_supports_char_arrays() {
1078        test_support::with_test_provider(|_| {
1079            let chars = CharArray::new("row1row2".chars().collect(), 2, 4).unwrap();
1080            let original: Vec<char> = chars.data.clone();
1081            let result =
1082                call(Value::CharArray(chars), Vec::new()).expect("gpuArray char array upload");
1083            let Value::GpuTensor(handle) = result else {
1084                panic!("expected gpu tensor");
1085            };
1086            let gathered =
1087                test_support::gather(Value::GpuTensor(handle.clone())).expect("gather chars");
1088            assert_eq!(gathered.shape, vec![2, 4]);
1089            let mut recovered = Vec::new();
1090            for col in 0..4 {
1091                for row in 0..2 {
1092                    let idx = row + col * 2;
1093                    let code = gathered.data[idx];
1094                    let ch = char::from_u32(code as u32)
1095                        .expect("valid unicode scalar from numeric code");
1096                    recovered.push(ch);
1097                }
1098            }
1099            assert_eq!(recovered, original);
1100        });
1101    }
1102
1103    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1104    #[test]
1105    fn gpu_array_converts_strings() {
1106        test_support::with_test_provider(|_| {
1107            let result = call(Value::String("gpu".into()), Vec::new()).expect("gpuArray string");
1108            let Value::GpuTensor(handle) = result else {
1109                panic!("expected gpu tensor");
1110            };
1111            let gathered =
1112                test_support::gather(Value::GpuTensor(handle.clone())).expect("gather string");
1113            assert_eq!(gathered.shape, vec![1, 3]);
1114            let expected: Vec<f64> = "gpu".chars().map(|ch| ch as u32 as f64).collect();
1115            assert_eq!(gathered.data, expected);
1116        });
1117    }
1118
1119    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1120    #[test]
1121    fn gpu_array_passthrough_existing_handle() {
1122        test_support::with_test_provider(|provider| {
1123            let tensor = Tensor::new(vec![5.0, 6.0], vec![2, 1]).unwrap();
1124            let view = HostTensorView {
1125                data: &tensor.data,
1126                shape: &tensor.shape,
1127            };
1128            let handle = provider.upload(&view).expect("upload");
1129            let cloned = handle.clone();
1130            let result =
1131                call(Value::GpuTensor(handle.clone()), Vec::new()).expect("gpuArray passthrough");
1132            let Value::GpuTensor(returned) = result else {
1133                panic!("expected gpu tensor");
1134            };
1135            assert_eq!(returned.buffer_id, cloned.buffer_id);
1136            assert_eq!(returned.shape, cloned.shape);
1137        });
1138    }
1139
1140    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1141    #[test]
1142    fn gpu_array_passthrough_existing_complex_handle() {
1143        test_support::with_test_provider(|provider| {
1144            let complex = ComplexTensor::new(vec![(2.0, 3.0), (-4.0, 5.5)], vec![2, 1]).unwrap();
1145            let handle = gpu_helpers::upload_complex_tensor(provider, &complex).unwrap();
1146            let result =
1147                call(Value::GpuTensor(handle.clone()), Vec::new()).expect("gpuArray passthrough");
1148            let Value::GpuTensor(returned) = result else {
1149                panic!("expected gpu tensor");
1150            };
1151            assert_eq!(returned.buffer_id, handle.buffer_id);
1152            assert_eq!(
1153                runmat_accelerate_api::handle_storage(&returned),
1154                GpuTensorStorage::ComplexInterleaved
1155            );
1156            let gathered = gather_complex(Value::GpuTensor(returned.clone()));
1157            assert_complex_close(&gathered.data, &complex.data);
1158        });
1159    }
1160
1161    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1162    #[test]
1163    fn gpu_array_complex_gpu_to_single_reuploads_complex_handle() {
1164        test_support::with_test_provider(|provider| {
1165            let complex = ComplexTensor::new(
1166                vec![(1.234_567_89, -2.345_678_91), (3.456_789_12, 4.567_891_23)],
1167                vec![1, 2],
1168            )
1169            .unwrap();
1170            let handle = gpu_helpers::upload_complex_tensor(provider, &complex).unwrap();
1171            let result = call(
1172                Value::GpuTensor(handle.clone()),
1173                vec![Value::from("single")],
1174            )
1175            .expect("gpuArray complex single");
1176            let Value::GpuTensor(returned) = result else {
1177                panic!("expected gpu tensor");
1178            };
1179            assert_ne!(returned.buffer_id, handle.buffer_id);
1180            assert_eq!(
1181                runmat_accelerate_api::handle_storage(&returned),
1182                GpuTensorStorage::ComplexInterleaved
1183            );
1184            assert_eq!(
1185                runmat_accelerate_api::handle_precision(&returned),
1186                Some(runmat_accelerate_api::ProviderPrecision::F32)
1187            );
1188            let gathered = gather_complex(Value::GpuTensor(returned.clone()));
1189            let expected = complex
1190                .data
1191                .iter()
1192                .map(|(re, im)| ((*re as f32) as f64, (*im as f32) as f64))
1193                .collect::<Vec<_>>();
1194            assert_complex_close(&gathered.data, &expected);
1195        });
1196    }
1197
1198    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1199    #[test]
1200    fn gpu_array_casts_to_int32() {
1201        test_support::with_test_provider(|_| {
1202            let tensor = Tensor::new(vec![1.2, -3.7, 123456.0], vec![3, 1]).unwrap();
1203            let result =
1204                call(Value::Tensor(tensor), vec![Value::from("int32")]).expect("gpuArray int32");
1205            let Value::GpuTensor(handle) = result else {
1206                panic!("expected gpu tensor");
1207            };
1208            let gathered =
1209                test_support::gather(Value::GpuTensor(handle.clone())).expect("gather int32");
1210            assert_eq!(gathered.data, vec![1.0, -4.0, 123456.0]);
1211        });
1212    }
1213
1214    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1215    #[test]
1216    fn gpu_array_casts_to_uint8() {
1217        test_support::with_test_provider(|_| {
1218            let tensor = Tensor::new(vec![-12.0, 12.8, 300.4, f64::INFINITY], vec![4, 1]).unwrap();
1219            let result =
1220                call(Value::Tensor(tensor), vec![Value::from("uint8")]).expect("gpuArray uint8");
1221            let Value::GpuTensor(handle) = result else {
1222                panic!("expected gpu tensor");
1223            };
1224            let gathered =
1225                test_support::gather(Value::GpuTensor(handle.clone())).expect("gather uint8");
1226            assert_eq!(gathered.data, vec![0.0, 13.0, 255.0, 255.0]);
1227        });
1228    }
1229
1230    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1231    #[test]
1232    fn gpu_array_single_precision_rounds() {
1233        test_support::with_test_provider(|_| {
1234            let tensor = Tensor::new(vec![1.23456789, -9.87654321], vec![2, 1]).unwrap();
1235            let result =
1236                call(Value::Tensor(tensor), vec![Value::from("single")]).expect("gpuArray single");
1237            let Value::GpuTensor(handle) = result else {
1238                panic!("expected gpu tensor");
1239            };
1240            let gathered =
1241                test_support::gather(Value::GpuTensor(handle.clone())).expect("gather single");
1242            let expected = [1.234_567_9_f32 as f64, (-9.876_543_f32) as f64];
1243            for (observed, expected) in gathered.data.iter().zip(expected.iter()) {
1244                assert!((observed - expected).abs() < 1e-6);
1245            }
1246        });
1247    }
1248
1249    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1250    #[test]
1251    fn gpu_array_like_infers_logical() {
1252        test_support::with_test_provider(|_| {
1253            let tensor = Tensor::new(vec![0.0, 2.0, -3.0], vec![3, 1]).unwrap();
1254            let logical_proto =
1255                LogicalArray::new(vec![0, 1, 0], vec![3, 1]).expect("logical proto");
1256            let result = call(
1257                Value::Tensor(tensor),
1258                vec![Value::from("like"), Value::LogicalArray(logical_proto)],
1259            )
1260            .expect("gpuArray like logical");
1261            let Value::GpuTensor(handle) = result else {
1262                panic!("expected gpu tensor");
1263            };
1264            assert!(runmat_accelerate_api::handle_is_logical(&handle));
1265            let gathered = test_support::gather(Value::GpuTensor(handle.clone())).expect("gather");
1266            assert_eq!(gathered.data, vec![0.0, 1.0, 1.0]);
1267        });
1268    }
1269
1270    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1271    #[test]
1272    fn gpu_array_like_requires_argument() {
1273        test_support::with_test_provider(|_| {
1274            let tensor = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
1275            let err = call(Value::Tensor(tensor), vec![Value::from("like")]).unwrap_err();
1276            assert_eq!(err.to_string(), GPUARRAY_ERROR_LIKE_MISSING.message);
1277            assert_eq!(err.identifier(), GPUARRAY_ERROR_LIKE_MISSING.identifier);
1278        });
1279    }
1280
1281    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1282    #[test]
1283    fn gpu_array_unknown_option_errors() {
1284        test_support::with_test_provider(|_| {
1285            let tensor = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
1286            let err = call(Value::Tensor(tensor), vec![Value::from("mystery")]).unwrap_err();
1287            assert!(err
1288                .to_string()
1289                .contains(GPUARRAY_ERROR_UNKNOWN_OPTION.message));
1290            assert_eq!(err.identifier(), GPUARRAY_ERROR_UNKNOWN_OPTION.identifier);
1291        });
1292    }
1293
1294    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1295    #[test]
1296    fn gpu_array_gpu_to_logical_reuploads() {
1297        test_support::with_test_provider(|provider| {
1298            let tensor = Tensor::new(vec![2.0, 0.0, -5.5], vec![3, 1]).unwrap();
1299            let view = HostTensorView {
1300                data: &tensor.data,
1301                shape: &tensor.shape,
1302            };
1303            let handle = provider.upload(&view).expect("upload");
1304            let result = call(
1305                Value::GpuTensor(handle.clone()),
1306                vec![Value::from("logical")],
1307            )
1308            .expect("gpuArray logical cast");
1309            let Value::GpuTensor(new_handle) = result else {
1310                panic!("expected gpu tensor");
1311            };
1312            assert!(runmat_accelerate_api::handle_is_logical(&new_handle));
1313            let gathered =
1314                test_support::gather(Value::GpuTensor(new_handle.clone())).expect("gather");
1315            assert_eq!(gathered.data, vec![1.0, 0.0, 1.0]);
1316            provider.free(&handle).ok();
1317            provider.free(&new_handle).ok();
1318        });
1319    }
1320
1321    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1322    #[test]
1323    fn gpu_array_gpu_logical_to_double_clears_flag() {
1324        test_support::with_test_provider(|provider| {
1325            let tensor = Tensor::new(vec![1.0, 0.0], vec![2, 1]).unwrap();
1326            let view = HostTensorView {
1327                data: &tensor.data,
1328                shape: &tensor.shape,
1329            };
1330            let handle = provider.upload(&view).expect("upload");
1331            runmat_accelerate_api::set_handle_logical(&handle, true);
1332            let result = call(
1333                Value::GpuTensor(handle.clone()),
1334                vec![Value::from("double")],
1335            )
1336            .expect("gpuArray double cast");
1337            let Value::GpuTensor(new_handle) = result else {
1338                panic!("expected gpu tensor");
1339            };
1340            assert!(!runmat_accelerate_api::handle_is_logical(&new_handle));
1341            provider.free(&handle).ok();
1342            provider.free(&new_handle).ok();
1343        });
1344    }
1345
1346    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1347    #[test]
1348    fn gpu_array_applies_size_arguments() {
1349        test_support::with_test_provider(|_| {
1350            let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![4, 1]).unwrap();
1351            let result = call(
1352                Value::Tensor(tensor),
1353                vec![Value::from(2i32), Value::from(2i32)],
1354            )
1355            .expect("gpuArray reshape");
1356            let Value::GpuTensor(handle) = result else {
1357                panic!("expected gpu tensor");
1358            };
1359            assert_eq!(handle.shape, vec![2, 2]);
1360        });
1361    }
1362
1363    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1364    #[test]
1365    fn gpu_array_gpu_size_arguments_update_shape() {
1366        test_support::with_test_provider(|provider| {
1367            let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![4, 1]).unwrap();
1368            let view = HostTensorView {
1369                data: &tensor.data,
1370                shape: &tensor.shape,
1371            };
1372            let handle = provider.upload(&view).expect("upload");
1373            let result = call(
1374                Value::GpuTensor(handle.clone()),
1375                vec![Value::from(2i32), Value::from(2i32)],
1376            )
1377            .expect("gpuArray gpu reshape");
1378            let Value::GpuTensor(new_handle) = result else {
1379                panic!("expected gpu tensor");
1380            };
1381            assert_eq!(new_handle.shape, vec![2, 2]);
1382            provider.free(&handle).ok();
1383            provider.free(&new_handle).ok();
1384        });
1385    }
1386
1387    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1388    #[test]
1389    fn gpu_array_size_mismatch_errors() {
1390        test_support::with_test_provider(|_| {
1391            let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
1392            let err = call(
1393                Value::Tensor(tensor),
1394                vec![Value::from(2i32), Value::from(2i32)],
1395            )
1396            .unwrap_err();
1397            assert!(err.to_string().contains("cannot reshape"));
1398            assert_eq!(err.identifier(), GPUARRAY_ERROR_RESHAPE.identifier);
1399        });
1400    }
1401
1402    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1403    #[test]
1404    #[cfg(feature = "wgpu")]
1405    fn gpu_array_wgpu_roundtrip() {
1406        use runmat_accelerate_api::AccelProvider;
1407
1408        match runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
1409            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
1410        ) {
1411            Ok(provider) => {
1412                let tensor = Tensor::new(vec![1.0, 2.5, 3.5], vec![3, 1]).unwrap();
1413                let result = call(Value::Tensor(tensor.clone()), vec![Value::from("int32")])
1414                    .expect("wgpu upload");
1415                let Value::GpuTensor(handle) = result else {
1416                    panic!("expected gpu tensor");
1417                };
1418                let gathered =
1419                    test_support::gather(Value::GpuTensor(handle.clone())).expect("wgpu gather");
1420                assert_eq!(gathered.shape, vec![3, 1]);
1421                assert_eq!(gathered.data, vec![1.0, 3.0, 4.0]);
1422                provider.free(&handle).ok();
1423            }
1424            Err(err) => {
1425                tracing::warn!("Skipping gpu_array_wgpu_roundtrip: {err}");
1426            }
1427        }
1428        runmat_accelerate::simple_provider::register_inprocess_provider();
1429    }
1430
1431    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1432    #[test]
1433    #[cfg(feature = "wgpu")]
1434    fn gpu_array_wgpu_complex_roundtrip() {
1435        use runmat_accelerate_api::AccelProvider;
1436
1437        match runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
1438            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
1439        ) {
1440            Ok(provider) => {
1441                let complex =
1442                    ComplexTensor::new(vec![(1.25, -0.5), (-3.0, 4.0)], vec![1, 2]).unwrap();
1443                let result =
1444                    call(Value::ComplexTensor(complex.clone()), Vec::new()).expect("wgpu upload");
1445                let Value::GpuTensor(handle) = result else {
1446                    panic!("expected gpu tensor");
1447                };
1448                assert_eq!(
1449                    runmat_accelerate_api::handle_storage(&handle),
1450                    GpuTensorStorage::ComplexInterleaved
1451                );
1452                let gathered = gather_complex(Value::GpuTensor(handle.clone()));
1453                assert_eq!(gathered.shape, vec![1, 2]);
1454                assert_complex_close(&gathered.data, &complex.data);
1455                provider.free(&handle).ok();
1456            }
1457            Err(err) => {
1458                tracing::warn!("Skipping gpu_array_wgpu_complex_roundtrip: {err}");
1459            }
1460        }
1461        runmat_accelerate::simple_provider::register_inprocess_provider();
1462    }
1463
1464    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1465    #[test]
1466    fn gpu_array_accepts_int_scalars() {
1467        test_support::with_test_provider(|_| {
1468            let value = Value::Int(IntValue::I32(7));
1469            let result = call(value, Vec::new()).expect("gpuArray int");
1470            let Value::GpuTensor(handle) = result else {
1471                panic!("expected gpu tensor");
1472            };
1473            let gathered =
1474                test_support::gather(Value::GpuTensor(handle.clone())).expect("gather int");
1475            assert_eq!(gathered.shape, vec![1, 1]);
1476            assert_eq!(gathered.data, vec![7.0]);
1477        });
1478    }
1479
1480    #[test]
1481    fn gpuarray_type_for_logical_is_logical() {
1482        assert_eq!(
1483            gpuarray_type(&[Type::logical()], &ResolveContext::new(Vec::new())),
1484            Type::logical()
1485        );
1486    }
1487}