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//! Direct `gpuArray(X)` upload follows MATLAB semantics. Optional size arguments,
4//! `'like'` prototypes, and explicit dtype toggles are RunMat-mode extensions.
5
6use crate::builtins::acceleration::gpu::type_resolvers::gpuarray_type;
7use crate::builtins::common::spec::{
8    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
9    ProviderHook, ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
10};
11use crate::builtins::common::{gpu_helpers, tensor};
12use runmat_accelerate_api::{GpuTensorHandle, ProviderPrecision};
13#[cfg(test)]
14use runmat_accelerate_api::{HostIntegerDataView, HostIntegerTensorView};
15use runmat_builtins::{
16    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinExtensionDescriptor,
17    BuiltinExtensionMode, BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor,
18    BuiltinIntegerComputationDomain, BuiltinIntegerInputAvailability,
19    BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule, BuiltinIntegerOverflowRule,
20    BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule, BuiltinOutputMode,
21    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
22};
23use runmat_macros::runtime_builtin;
24use runmat_value::ComplexStorage;
25use runmat_value::{
26    CharArray, ComplexTensor, IntValue, IntegerComplexStorage, IntegerStorage, NumericDType,
27    NumericStorage, Tensor, Value,
28};
29
30use crate::{build_runtime_error, BuiltinResult, RuntimeError};
31
32const BUILTIN_NAME: &str = "gpuArray";
33
34const GPUARRAY_SIZE_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
35    id: "gpuarray-size-arguments",
36    mode: BuiltinExtensionMode::RunMatOnly,
37    description: "gpuArray size arguments are a RunMat extension",
38    error_identifier: Some("RunMat:compatibility:GpuArraySizeExtension"),
39};
40
41const GPUARRAY_DTYPE_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
42    id: "gpuarray-dtype-selector",
43    mode: BuiltinExtensionMode::RunMatOnly,
44    description: "gpuArray dtype selectors are a RunMat extension",
45    error_identifier: Some("RunMat:compatibility:GpuArrayDtypeExtension"),
46};
47
48const GPUARRAY_LIKE_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
49    id: "gpuarray-like",
50    mode: BuiltinExtensionMode::RunMatOnly,
51    description: "the gpuArray \"like\" prototype selector is a RunMat extension",
52    error_identifier: Some("RunMat:compatibility:GpuArrayLikeExtension"),
53};
54
55const GPUARRAY_TEXT_UPLOAD_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
56    id: "gpuarray-text-upload",
57    mode: BuiltinExtensionMode::RunMatOnly,
58    description:
59        "uploading character vectors or string scalars with gpuArray is a RunMat extension",
60    error_identifier: Some("RunMat:compatibility:GpuArrayTextUploadExtension"),
61};
62
63pub const GPUARRAY_EXTENSIONS: [BuiltinExtensionDescriptor; 4] = [
64    GPUARRAY_SIZE_EXTENSION,
65    GPUARRAY_DTYPE_EXTENSION,
66    GPUARRAY_LIKE_EXTENSION,
67    GPUARRAY_TEXT_UPLOAD_EXTENSION,
68];
69
70const GPUARRAY_INTEGER_INPUTS: [BuiltinIntegerInputCapability; 1] =
71    [BuiltinIntegerInputCapability {
72        name: "X",
73        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
74        availability: BuiltinIntegerInputAvailability::Documented,
75        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
76        notes: "All eight integer classes upload as exact same-class real or paired-complex gpuArray storage with the original shape.",
77    }];
78
79const GPUARRAY_INTEGER_DTYPE_INPUTS: [BuiltinIntegerInputCapability; 1] =
80    [BuiltinIntegerInputCapability {
81        name: "X",
82        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
83        availability: BuiltinIntegerInputAvailability::RunMatOnly,
84        scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
85        notes: "RunMat-only dtype selectors may explicitly convert X to any supported integer gpuArray class.",
86    }];
87
88pub const GPUARRAY_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 2] = [
89    BuiltinIntegerCapabilityDescriptor {
90        form: "G = gpuArray(integer_X)",
91        inputs: &GPUARRAY_INTEGER_INPUTS,
92        computation_domain: BuiltinIntegerComputationDomain::Structural,
93        output_class: BuiltinIntegerOutputClassRule::PreserveInput,
94        overflow: BuiltinIntegerOverflowRule::NotApplicable,
95        backend: BuiltinIntegerBackendRule::HostAndGpu,
96        overload: BuiltinIntegerOverloadKind::ElementwiseShapePreserving,
97        notes: "The transfer preserves exact values, class, shape, and supported complexity. An existing gpuArray input is returned unchanged and remains valid.",
98    },
99    BuiltinIntegerCapabilityDescriptor {
100        form: "G = gpuArray(X, integer_dtype)",
101        inputs: &GPUARRAY_INTEGER_DTYPE_INPUTS,
102        computation_domain: BuiltinIntegerComputationDomain::Structural,
103        output_class: BuiltinIntegerOutputClassRule::OptionDependent,
104        overflow: BuiltinIntegerOverflowRule::Saturate,
105        backend: BuiltinIntegerBackendRule::HostAndGpu,
106        overload: BuiltinIntegerOverloadKind::ElementwiseShapePreserving,
107        notes: "RunMat-only conversion uses the requested native integer class and never consumes or invalidates a gpuArray input.",
108    },
109];
110
111const GPUARRAY_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
112    name: "G",
113    ty: BuiltinParamType::Any,
114    arity: BuiltinParamArity::Required,
115    default: None,
116    description: "GPU-resident handle containing uploaded/converted data.",
117}];
118
119const GPUARRAY_INPUTS_BASE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
120    name: "X",
121    ty: BuiltinParamType::Any,
122    arity: BuiltinParamArity::Required,
123    default: None,
124    description: "Input value to upload or recast on GPU.",
125}];
126
127const GPUARRAY_INPUTS_DIMS: [BuiltinParamDescriptor; 2] = [
128    BuiltinParamDescriptor {
129        name: "X",
130        ty: BuiltinParamType::Any,
131        arity: BuiltinParamArity::Required,
132        default: None,
133        description: "Input value to upload or recast on GPU.",
134    },
135    BuiltinParamDescriptor {
136        name: "dim",
137        ty: BuiltinParamType::SizeArg,
138        arity: BuiltinParamArity::Variadic,
139        default: None,
140        description: "Reshape dimensions (scalar dims or a single size vector tensor).",
141    },
142];
143
144const GPUARRAY_INPUTS_DTYPE: [BuiltinParamDescriptor; 2] = [
145    BuiltinParamDescriptor {
146        name: "X",
147        ty: BuiltinParamType::Any,
148        arity: BuiltinParamArity::Required,
149        default: None,
150        description: "Input value to upload or recast on GPU.",
151    },
152    BuiltinParamDescriptor {
153        name: "dtype",
154        ty: BuiltinParamType::StringScalar,
155        arity: BuiltinParamArity::Required,
156        default: Some("\"double\""),
157        description: "Class tag such as `single`, `int32`, `uint8`, `logical`, or `double`.",
158    },
159];
160
161const GPUARRAY_INPUTS_LIKE: [BuiltinParamDescriptor; 3] = [
162    BuiltinParamDescriptor {
163        name: "X",
164        ty: BuiltinParamType::Any,
165        arity: BuiltinParamArity::Required,
166        default: None,
167        description: "Input value to upload or recast on GPU.",
168    },
169    BuiltinParamDescriptor {
170        name: "like",
171        ty: BuiltinParamType::StringScalar,
172        arity: BuiltinParamArity::Required,
173        default: None,
174        description: "Literal keyword `\"like\"`.",
175    },
176    BuiltinParamDescriptor {
177        name: "prototype",
178        ty: BuiltinParamType::LikePrototype,
179        arity: BuiltinParamArity::Required,
180        default: None,
181        description: "Prototype value whose class drives output conversion.",
182    },
183];
184
185const GPUARRAY_INPUTS_DIMS_OPTIONS: [BuiltinParamDescriptor; 3] = [
186    BuiltinParamDescriptor {
187        name: "X",
188        ty: BuiltinParamType::Any,
189        arity: BuiltinParamArity::Required,
190        default: None,
191        description: "Input value to upload or recast on GPU.",
192    },
193    BuiltinParamDescriptor {
194        name: "dim",
195        ty: BuiltinParamType::SizeArg,
196        arity: BuiltinParamArity::Variadic,
197        default: None,
198        description: "Reshape dimensions (scalar dims or a single size vector tensor).",
199    },
200    BuiltinParamDescriptor {
201        name: "option",
202        ty: BuiltinParamType::Any,
203        arity: BuiltinParamArity::Variadic,
204        default: None,
205        description: "Class tags and/or `\"like\", prototype` qualifiers.",
206    },
207];
208
209const GPUARRAY_SIGNATURES: [BuiltinSignatureDescriptor; 5] = [
210    BuiltinSignatureDescriptor {
211        label: "G = gpuArray(X)",
212        inputs: &GPUARRAY_INPUTS_BASE,
213        outputs: &GPUARRAY_OUTPUT,
214    },
215    BuiltinSignatureDescriptor {
216        label: "G = gpuArray(X, dim, ...)",
217        inputs: &GPUARRAY_INPUTS_DIMS,
218        outputs: &GPUARRAY_OUTPUT,
219    },
220    BuiltinSignatureDescriptor {
221        label: "G = gpuArray(X, dtype)",
222        inputs: &GPUARRAY_INPUTS_DTYPE,
223        outputs: &GPUARRAY_OUTPUT,
224    },
225    BuiltinSignatureDescriptor {
226        label: "G = gpuArray(X, \"like\", prototype)",
227        inputs: &GPUARRAY_INPUTS_LIKE,
228        outputs: &GPUARRAY_OUTPUT,
229    },
230    BuiltinSignatureDescriptor {
231        label: "G = gpuArray(X, dim, ..., option, ...)",
232        inputs: &GPUARRAY_INPUTS_DIMS_OPTIONS,
233        outputs: &GPUARRAY_OUTPUT,
234    },
235];
236
237const GPUARRAY_ERROR_NO_PROVIDER: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
238    code: "RM.GPUARRAY.NO_PROVIDER",
239    identifier: Some("RunMat:gpuArray:NoProvider"),
240    when: "No acceleration provider is registered for host/device transfers.",
241    message: "gpuArray: no acceleration provider registered",
242};
243
244const GPUARRAY_ERROR_OPTION_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
245    code: "RM.GPUARRAY.OPTION_ARGUMENT",
246    identifier: Some("RunMat:gpuArray:OptionArgument"),
247    when: "Option tail contains non-text values where class tags/keywords are expected.",
248    message: "gpuArray: invalid option argument",
249};
250
251const GPUARRAY_ERROR_LIKE_MISSING: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
252    code: "RM.GPUARRAY.LIKE_MISSING",
253    identifier: Some("RunMat:gpuArray:LikeMissingPrototype"),
254    when: "Keyword `like` is supplied without a following prototype value.",
255    message: "gpuArray: expected a prototype value after 'like'",
256};
257
258const GPUARRAY_ERROR_LIKE_DUPLICATE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
259    code: "RM.GPUARRAY.LIKE_DUPLICATE",
260    identifier: Some("RunMat:gpuArray:LikeDuplicate"),
261    when: "Keyword `like` appears more than once.",
262    message: "gpuArray: duplicate 'like' qualifier",
263};
264
265const GPUARRAY_ERROR_CODISTRIBUTED_UNSUPPORTED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
266    code: "RM.GPUARRAY.CODISTRIBUTED_UNSUPPORTED",
267    identifier: Some("RunMat:gpuArray:CodistributedUnsupported"),
268    when: "Distributed/codistributed qualifiers are requested.",
269    message: "gpuArray: codistributed arrays are not supported yet",
270};
271
272const GPUARRAY_ERROR_CONFLICTING_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
273    code: "RM.GPUARRAY.CONFLICTING_TYPE",
274    identifier: Some("RunMat:gpuArray:ConflictingTypeQualifiers"),
275    when: "Multiple incompatible class qualifiers are supplied.",
276    message: "gpuArray: conflicting type qualifiers supplied",
277};
278
279const GPUARRAY_ERROR_UNKNOWN_OPTION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
280    code: "RM.GPUARRAY.UNKNOWN_OPTION",
281    identifier: Some("RunMat:gpuArray:UnknownOption"),
282    when: "Text option is not a recognized class/keyword token.",
283    message: "gpuArray: unrecognised option",
284};
285
286const GPUARRAY_ERROR_SIZE_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
287    code: "RM.GPUARRAY.SIZE_ARGUMENT",
288    identifier: Some("RunMat:gpuArray:InvalidSizeArgument"),
289    when: "Size arguments are malformed (not finite integers, negative, or invalid combinations).",
290    message: "gpuArray: invalid size argument",
291};
292
293const GPUARRAY_ERROR_LIKE_PROTOTYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
294    code: "RM.GPUARRAY.LIKE_PROTOTYPE",
295    identifier: Some("RunMat:gpuArray:InvalidLikePrototype"),
296    when: "`like` prototype is unsupported for type inference.",
297    message: "gpuArray: invalid 'like' prototype",
298};
299
300const GPUARRAY_ERROR_INPUT_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
301    code: "RM.GPUARRAY.INPUT_TYPE",
302    identifier: Some("RunMat:gpuArray:UnsupportedInputType"),
303    when: "Input value type cannot be uploaded/coerced to supported gpuArray storage.",
304    message: "gpuArray: unsupported input type",
305};
306
307const GPUARRAY_ERROR_TYPED_INTEGER: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
308    code: "RM.GPUARRAY.TYPED_INTEGER",
309    identifier: Some("RunMat:gpuArray:TypedIntegerUnsupported"),
310    when: "A native integer value or integer GPU class is requested without matching provider storage.",
311    message: "gpuArray: native integer storage is not supported by the active acceleration provider",
312};
313
314const GPUARRAY_ERROR_CONVERSION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
315    code: "RM.GPUARRAY.CONVERSION",
316    identifier: Some("RunMat:gpuArray:ConversionFailed"),
317    when: "Requested dtype conversion cannot be performed (for example NaN->logical).",
318    message: "gpuArray: conversion failed",
319};
320
321const GPUARRAY_ERROR_RESHAPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
322    code: "RM.GPUARRAY.RESHAPE",
323    identifier: Some("RunMat:gpuArray:ReshapeMismatch"),
324    when: "Requested shape does not preserve the element count.",
325    message: "gpuArray: cannot reshape gpuArray into requested size",
326};
327
328const GPUARRAY_ERROR_PROVIDER_IO: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
329    code: "RM.GPUARRAY.PROVIDER_IO",
330    identifier: Some("RunMat:gpuArray:ProviderIO"),
331    when: "Provider upload/download interaction fails.",
332    message: "gpuArray: provider I/O failed",
333};
334
335const GPUARRAY_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
336    code: "RM.GPUARRAY.INTERNAL",
337    identifier: Some("RunMat:gpuArray:InternalError"),
338    when: "Internal tensor/container conversion fails.",
339    message: "gpuArray: internal error",
340};
341
342const GPUARRAY_ERRORS: [BuiltinErrorDescriptor; 15] = [
343    GPUARRAY_ERROR_NO_PROVIDER,
344    GPUARRAY_ERROR_OPTION_ARGUMENT,
345    GPUARRAY_ERROR_LIKE_MISSING,
346    GPUARRAY_ERROR_LIKE_DUPLICATE,
347    GPUARRAY_ERROR_CODISTRIBUTED_UNSUPPORTED,
348    GPUARRAY_ERROR_CONFLICTING_TYPE,
349    GPUARRAY_ERROR_UNKNOWN_OPTION,
350    GPUARRAY_ERROR_SIZE_ARGUMENT,
351    GPUARRAY_ERROR_LIKE_PROTOTYPE,
352    GPUARRAY_ERROR_INPUT_TYPE,
353    GPUARRAY_ERROR_TYPED_INTEGER,
354    GPUARRAY_ERROR_CONVERSION,
355    GPUARRAY_ERROR_RESHAPE,
356    GPUARRAY_ERROR_PROVIDER_IO,
357    GPUARRAY_ERROR_INTERNAL,
358];
359
360pub const GPUARRAY_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
361    signatures: &GPUARRAY_SIGNATURES,
362    output_mode: BuiltinOutputMode::Fixed,
363    completion_policy: BuiltinCompletionPolicy::Public,
364    errors: &GPUARRAY_ERRORS,
365};
366
367fn gpu_array_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
368    gpu_array_error_with_message(error.message, error)
369}
370
371fn gpu_array_error_with_message(
372    message: impl Into<String>,
373    error: &'static BuiltinErrorDescriptor,
374) -> RuntimeError {
375    let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
376    if let Some(identifier) = error.identifier {
377        builder = builder.with_identifier(identifier);
378    }
379    builder.build()
380}
381
382fn gpu_array_error_with_detail(
383    error: &'static BuiltinErrorDescriptor,
384    detail: impl AsRef<str>,
385) -> RuntimeError {
386    gpu_array_error_with_message(format!("{}: {}", error.message, detail.as_ref()), error)
387}
388
389#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::acceleration::gpu::gpuarray")]
390pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
391    name: "gpuArray",
392    op_kind: GpuOpKind::Custom("upload"),
393    supported_precisions: &[ScalarType::F32, ScalarType::F64],
394    broadcast: BroadcastSemantics::None,
395    provider_hooks: &[ProviderHook::Custom("upload_numeric")],
396    constant_strategy: ConstantStrategy::InlineLiteral,
397    residency: ResidencyPolicy::NewHandle,
398    nan_mode: ReductionNaN::Include,
399    two_pass_threshold: None,
400    workgroup_size: None,
401    accepts_nan_mode: false,
402    notes: "Invokes the provider's native numeric upload contract for real or complex double, single, and integer storage, and reuploads gpuArray inputs when dtype conversion is requested. Handles class strings, size vectors, and `'like'` prototypes.",
403};
404
405#[runmat_macros::register_fusion_spec(
406    builtin_path = "crate::builtins::acceleration::gpu::gpuarray"
407)]
408pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
409    name: "gpuArray",
410    shape: ShapeRequirements::Any,
411    constant_strategy: ConstantStrategy::InlineLiteral,
412    elementwise: None,
413    reduction: None,
414    emits_nan: false,
415    notes:
416        "Acts as a residency boundary; fusion graphs never cross explicit host↔device transfers.",
417};
418
419#[runtime_builtin(
420    name = "gpuArray",
421    category = "acceleration/gpu",
422    summary = "Move data to the GPU as gpuArray values.",
423    keywords = "gpuArray,gpu,accelerate,upload,dtype,like",
424    examples = "G = gpuArray([1 2 3], 'single');",
425    accel = "array_construct",
426    type_resolver(gpuarray_type),
427    descriptor(crate::builtins::acceleration::gpu::gpuarray::GPUARRAY_DESCRIPTOR),
428    extensions(crate::builtins::acceleration::gpu::gpuarray::GPUARRAY_EXTENSIONS),
429    integer_capabilities(
430        crate::builtins::acceleration::gpu::gpuarray::GPUARRAY_INTEGER_CAPABILITIES
431    ),
432    builtin_path = "crate::builtins::acceleration::gpu::gpuarray"
433)]
434async fn gpu_array_builtin(value: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
435    let options = parse_options(&rest)?;
436    if options.dims.is_some() {
437        crate::compatibility::ensure_builtin_extension_enabled(
438            &GPUARRAY_SIZE_EXTENSION,
439            BUILTIN_NAME,
440        )?;
441    }
442    if options.explicit_dtype.is_some() {
443        crate::compatibility::ensure_builtin_extension_enabled(
444            &GPUARRAY_DTYPE_EXTENSION,
445            BUILTIN_NAME,
446        )?;
447    }
448    if options.prototype.is_some() {
449        crate::compatibility::ensure_builtin_extension_enabled(
450            &GPUARRAY_LIKE_EXTENSION,
451            BUILTIN_NAME,
452        )?;
453    }
454    if matches!(value, Value::CharArray(_) | Value::String(_)) {
455        crate::compatibility::ensure_builtin_extension_enabled(
456            &GPUARRAY_TEXT_UPLOAD_EXTENSION,
457            BUILTIN_NAME,
458        )?;
459    }
460    if rest.is_empty() {
461        if let Value::GpuTensor(handle) = &value {
462            // gpuArray(G) is an identity operation. In particular, do not download,
463            // re-upload, or release the storage owned by the caller's handle.
464            let explicit = handle
465                .clone()
466                .with_provenance(runmat_accelerate_api::GpuHandleProvenance::Explicit);
467            return Ok(Value::GpuTensor(explicit));
468        }
469    }
470    let dtype = resolve_dtype(&value, &options)?;
471    let dims = options.dims.clone();
472    let expected_shape = dims
473        .clone()
474        .unwrap_or_else(|| gpu_array_value_shape(&value));
475
476    let prepared = match value {
477        Value::GpuTensor(handle) => convert_device_value(handle, dtype).await?,
478        other => upload_host_value(other, dtype)?,
479    };
480
481    let mut handle = prepared.handle;
482    if let Some(dims) = dims.as_ref() {
483        apply_dims(&mut handle, dims)?;
484    }
485
486    if let Err(error) = validate_prepared_handle(
487        &handle,
488        prepared.provider,
489        &expected_shape,
490        prepared.storage,
491        dtype,
492        prepared.logical,
493    ) {
494        if prepared.owns_handle {
495            let _ = prepared.provider.free(&handle);
496        }
497        return Err(error);
498    }
499    runmat_accelerate_api::set_handle_logical(&handle, prepared.logical);
500    runmat_accelerate_api::set_handle_class_name(&handle, dtype.class_name());
501    handle.descriptor.provenance = Some(runmat_accelerate_api::GpuHandleProvenance::Explicit);
502
503    Ok(Value::GpuTensor(handle))
504}
505
506fn gpu_array_value_shape(value: &Value) -> Vec<usize> {
507    match value {
508        Value::Tensor(tensor) => tensor.shape.clone(),
509        Value::ComplexTensor(tensor) => tensor.shape.clone(),
510        Value::LogicalArray(array) => array.shape.clone(),
511        Value::GpuTensor(handle) => handle.shape.clone(),
512        Value::CharArray(array) => array.shape.clone(),
513        Value::String(text) => vec![1, text.chars().count()],
514        _ => vec![1, 1],
515    }
516}
517
518#[derive(Clone, Copy, Debug, PartialEq, Eq)]
519enum DataClass {
520    Double,
521    Single,
522    Logical,
523    Int8,
524    Int16,
525    Int32,
526    Int64,
527    UInt8,
528    UInt16,
529    UInt32,
530    UInt64,
531}
532
533impl DataClass {
534    fn is_integer(self) -> bool {
535        matches!(
536            self,
537            Self::Int8
538                | Self::Int16
539                | Self::Int32
540                | Self::Int64
541                | Self::UInt8
542                | Self::UInt16
543                | Self::UInt32
544                | Self::UInt64
545        )
546    }
547
548    fn from_tag(tag: &str) -> Option<Self> {
549        match tag {
550            "double" => Some(Self::Double),
551            "single" | "float32" => Some(Self::Single),
552            "logical" | "bool" | "boolean" => Some(Self::Logical),
553            "int8" => Some(Self::Int8),
554            "int16" => Some(Self::Int16),
555            "int32" | "int" => Some(Self::Int32),
556            "int64" => Some(Self::Int64),
557            "uint8" => Some(Self::UInt8),
558            "uint16" => Some(Self::UInt16),
559            "uint32" => Some(Self::UInt32),
560            "uint64" => Some(Self::UInt64),
561            "gpuarray" => None, // compatibility no-op
562            _ => None,
563        }
564    }
565
566    fn class_name(self) -> &'static str {
567        match self {
568            Self::Double => "double",
569            Self::Single => "single",
570            Self::Logical => "logical",
571            Self::Int8 => "int8",
572            Self::Int16 => "int16",
573            Self::Int32 => "int32",
574            Self::Int64 => "int64",
575            Self::UInt8 => "uint8",
576            Self::UInt16 => "uint16",
577            Self::UInt32 => "uint32",
578            Self::UInt64 => "uint64",
579        }
580    }
581
582    fn numeric_dtype(self) -> NumericDType {
583        match self {
584            Self::Double => NumericDType::F64,
585            Self::Single => NumericDType::F32,
586            Self::Logical => NumericDType::F64,
587            Self::Int8 => NumericDType::I8,
588            Self::Int16 => NumericDType::I16,
589            Self::Int32 => NumericDType::I32,
590            Self::Int64 => NumericDType::I64,
591            Self::UInt8 => NumericDType::U8,
592            Self::UInt16 => NumericDType::U16,
593            Self::UInt32 => NumericDType::U32,
594            Self::UInt64 => NumericDType::U64,
595        }
596    }
597}
598
599#[derive(Debug, Default)]
600struct ParsedOptions {
601    dims: Option<Vec<usize>>,
602    explicit_dtype: Option<DataClass>,
603    prototype: Option<Value>,
604}
605
606fn parse_options(rest: &[Value]) -> BuiltinResult<ParsedOptions> {
607    let (index_after_dims, dims) = parse_size_arguments(rest)?;
608    let mut options = ParsedOptions {
609        dims,
610        ..ParsedOptions::default()
611    };
612
613    let mut idx = index_after_dims;
614    while idx < rest.len() {
615        let tag = value_to_lower_string(&rest[idx]).ok_or_else(|| {
616            gpu_array_error_with_message(
617                format!(
618                "gpuArray: unexpected argument {:?}; expected a class string or the keyword 'like'",
619                rest[idx]
620                ),
621                &GPUARRAY_ERROR_OPTION_ARGUMENT,
622            )
623        })?;
624
625        match tag.as_str() {
626            "like" => {
627                idx += 1;
628                if idx >= rest.len() {
629                    return Err(gpu_array_error(&GPUARRAY_ERROR_LIKE_MISSING));
630                }
631                if options.prototype.is_some() {
632                    return Err(gpu_array_error(&GPUARRAY_ERROR_LIKE_DUPLICATE));
633                }
634                options.prototype = Some(rest[idx].clone());
635            }
636            "distributed" | "codistributed" => {
637                return Err(gpu_array_error(&GPUARRAY_ERROR_CODISTRIBUTED_UNSUPPORTED));
638            }
639            tag => {
640                if let Some(class) = DataClass::from_tag(tag) {
641                    if let Some(existing) = options.explicit_dtype {
642                        if existing != class {
643                            return Err(gpu_array_error(&GPUARRAY_ERROR_CONFLICTING_TYPE));
644                        }
645                    } else {
646                        options.explicit_dtype = Some(class);
647                    }
648                } else if tag != "gpuarray" {
649                    return Err(gpu_array_error_with_detail(
650                        &GPUARRAY_ERROR_UNKNOWN_OPTION,
651                        format!("unrecognised option '{tag}'"),
652                    ));
653                }
654            }
655        }
656
657        idx += 1;
658    }
659
660    Ok(options)
661}
662
663fn parse_size_arguments(rest: &[Value]) -> BuiltinResult<(usize, Option<Vec<usize>>)> {
664    let mut idx = 0;
665    let mut dims: Vec<usize> = Vec::new();
666    let mut vector_consumed = false;
667
668    while idx < rest.len() {
669        // Stop at textual qualifiers only; numeric values continue parsing as size args.
670        match &rest[idx] {
671            Value::String(_) | Value::StringArray(_) | Value::CharArray(_) => break,
672            _ => {}
673        }
674
675        match &rest[idx] {
676            Value::Int(i) => {
677                dims.push(int_to_dim(i)?);
678            }
679            Value::Num(n) => {
680                dims.push(float_to_dim(*n)?);
681            }
682            Value::Tensor(t) => {
683                if vector_consumed || !dims.is_empty() {
684                    return Err(gpu_array_error_with_message(
685                        "gpuArray: size vectors cannot be combined with scalar dimensions",
686                        &GPUARRAY_ERROR_SIZE_ARGUMENT,
687                    ));
688                }
689                dims = tensor_to_dims(t)?;
690                vector_consumed = true;
691            }
692            _ => break,
693        }
694        idx += 1;
695    }
696
697    let dims_option = if dims.is_empty() { None } else { Some(dims) };
698    Ok((idx, dims_option))
699}
700
701fn value_to_lower_string(value: &Value) -> Option<String> {
702    crate::builtins::common::tensor::value_to_string(value).map(|s| s.trim().to_ascii_lowercase())
703}
704
705fn int_to_dim(value: &IntValue) -> BuiltinResult<usize> {
706    value.try_to_usize().ok_or_else(|| {
707        gpu_array_error_with_message(
708            "gpuArray: size arguments must be non-negative integers",
709            &GPUARRAY_ERROR_SIZE_ARGUMENT,
710        )
711    })
712}
713
714fn float_to_dim(value: f64) -> BuiltinResult<usize> {
715    if !value.is_finite() {
716        return Err(gpu_array_error_with_message(
717            "gpuArray: size arguments must be finite integers",
718            &GPUARRAY_ERROR_SIZE_ARGUMENT,
719        ));
720    }
721    let rounded = value.round();
722    if (rounded - value).abs() > f64::EPSILON {
723        return Err(gpu_array_error_with_message(
724            "gpuArray: size arguments must be integers",
725            &GPUARRAY_ERROR_SIZE_ARGUMENT,
726        ));
727    }
728    if rounded < 0.0 {
729        return Err(gpu_array_error_with_message(
730            "gpuArray: size arguments must be non-negative",
731            &GPUARRAY_ERROR_SIZE_ARGUMENT,
732        ));
733    }
734    if rounded > usize::MAX as f64 || (usize::BITS == 64 && rounded == usize::MAX as f64) {
735        return Err(gpu_array_error_with_message(
736            "gpuArray: size arguments exceed maximum supported size",
737            &GPUARRAY_ERROR_SIZE_ARGUMENT,
738        ));
739    }
740    Ok(rounded as usize)
741}
742
743fn tensor_to_dims(tensor: &Tensor) -> BuiltinResult<Vec<usize>> {
744    if let Some(storage) = tensor.integer_storage() {
745        return storage
746            .exact_values()
747            .iter()
748            .map(int_to_dim)
749            .collect::<BuiltinResult<Vec<_>>>();
750    }
751
752    let values = tensor::tensor_values_f64_cow(tensor);
753    let mut dims = Vec::with_capacity(values.len());
754    for value in values.iter() {
755        dims.push(float_to_dim(*value)?);
756    }
757    Ok(dims)
758}
759
760fn resolve_dtype(value: &Value, options: &ParsedOptions) -> BuiltinResult<DataClass> {
761    if let Some(explicit) = options.explicit_dtype {
762        return Ok(explicit);
763    }
764    if let Some(prototype) = options.prototype.as_ref() {
765        return infer_dtype_from_prototype(prototype);
766    }
767    if let Value::GpuTensor(handle) = value {
768        return dtype_from_gpu_handle(handle);
769    }
770    if let Value::Int(value) = value {
771        return Ok(data_class_from_int_value(value));
772    }
773    if let Value::Tensor(tensor) = value {
774        return Ok(data_class_from_numeric_dtype(tensor.numeric_dtype()));
775    }
776    if let Value::ComplexTensor(tensor) = value {
777        return Ok(data_class_from_numeric_dtype(tensor.numeric_dtype()));
778    }
779    if value_defaults_to_logical(value) {
780        return Ok(DataClass::Logical);
781    }
782    Ok(DataClass::Double)
783}
784
785fn data_class_from_int_value(value: &IntValue) -> DataClass {
786    match value {
787        IntValue::I8(_) => DataClass::Int8,
788        IntValue::I16(_) => DataClass::Int16,
789        IntValue::I32(_) => DataClass::Int32,
790        IntValue::I64(_) => DataClass::Int64,
791        IntValue::U8(_) => DataClass::UInt8,
792        IntValue::U16(_) => DataClass::UInt16,
793        IntValue::U32(_) => DataClass::UInt32,
794        IntValue::U64(_) => DataClass::UInt64,
795    }
796}
797
798fn data_class_from_numeric_dtype(dtype: NumericDType) -> DataClass {
799    match dtype {
800        NumericDType::F64 => DataClass::Double,
801        NumericDType::F32 => DataClass::Single,
802        NumericDType::I8 => DataClass::Int8,
803        NumericDType::I16 => DataClass::Int16,
804        NumericDType::I32 => DataClass::Int32,
805        NumericDType::I64 => DataClass::Int64,
806        NumericDType::U8 => DataClass::UInt8,
807        NumericDType::U16 => DataClass::UInt16,
808        NumericDType::U32 => DataClass::UInt32,
809        NumericDType::U64 => DataClass::UInt64,
810    }
811}
812
813fn integer_prototype(dtype: DataClass) -> Option<IntegerStorage> {
814    Some(match dtype {
815        DataClass::Int8 => IntegerStorage::I8(Vec::new()),
816        DataClass::Int16 => IntegerStorage::I16(Vec::new()),
817        DataClass::Int32 => IntegerStorage::I32(Vec::new()),
818        DataClass::Int64 => IntegerStorage::I64(Vec::new()),
819        DataClass::UInt8 => IntegerStorage::U8(Vec::new()),
820        DataClass::UInt16 => IntegerStorage::U16(Vec::new()),
821        DataClass::UInt32 => IntegerStorage::U32(Vec::new()),
822        DataClass::UInt64 => IntegerStorage::U64(Vec::new()),
823        _ => return None,
824    })
825}
826
827fn infer_dtype_from_prototype(proto: &Value) -> BuiltinResult<DataClass> {
828    match proto {
829        Value::GpuTensor(handle) => dtype_from_gpu_handle(handle),
830        Value::LogicalArray(_) | Value::Bool(_) => Ok(DataClass::Logical),
831        Value::Int(int) => Ok(match int {
832            IntValue::I8(_) => DataClass::Int8,
833            IntValue::I16(_) => DataClass::Int16,
834            IntValue::I32(_) => DataClass::Int32,
835            IntValue::I64(_) => DataClass::Int64,
836            IntValue::U8(_) => DataClass::UInt8,
837            IntValue::U16(_) => DataClass::UInt16,
838            IntValue::U32(_) => DataClass::UInt32,
839            IntValue::U64(_) => DataClass::UInt64,
840        }),
841        Value::Tensor(tensor) => Ok(data_class_from_numeric_dtype(tensor.numeric_dtype())),
842        Value::Num(_) => Ok(DataClass::Double),
843        Value::CharArray(_) => Ok(DataClass::Double),
844        Value::String(_) => Err(gpu_array_error_with_message(
845            "gpuArray: 'like' does not accept MATLAB string scalars; convert to char() first",
846            &GPUARRAY_ERROR_LIKE_PROTOTYPE,
847        )),
848        Value::StringArray(_) => Err(gpu_array_error_with_message(
849            "gpuArray: 'like' does not accept string arrays; convert to char arrays first",
850            &GPUARRAY_ERROR_LIKE_PROTOTYPE,
851        )),
852        Value::Complex(_, _) => Ok(DataClass::Double),
853        Value::ComplexTensor(tensor) => {
854            Ok(data_class_from_numeric_dtype(tensor.numeric_dtype()))
855        }
856        other => Err(gpu_array_error_with_message(
857            format!(
858                "gpuArray: unsupported 'like' prototype type {other:?}; expected numeric or logical values"
859            ),
860            &GPUARRAY_ERROR_LIKE_PROTOTYPE,
861        )),
862    }
863}
864
865fn dtype_from_gpu_handle(handle: &GpuTensorHandle) -> BuiltinResult<DataClass> {
866    if runmat_accelerate_api::handle_is_logical(handle) {
867        return Ok(DataClass::Logical);
868    }
869    if let Some(class_name) = runmat_accelerate_api::handle_class_name(handle) {
870        if let Some(dtype) = DataClass::from_tag(class_name.trim().to_ascii_lowercase().as_str()) {
871            return Ok(dtype);
872        }
873    }
874    let precision = runmat_accelerate_api::handle_precision(handle).or_else(|| {
875        runmat_accelerate_api::provider_for_handle(handle).map(|provider| provider.precision())
876    });
877    Ok(match precision {
878        Some(ProviderPrecision::F32) => DataClass::Single,
879        Some(ProviderPrecision::F64) | None => DataClass::Double,
880    })
881}
882
883fn value_defaults_to_logical(value: &Value) -> bool {
884    match value {
885        Value::LogicalArray(_) | Value::Bool(_) => true,
886        Value::GpuTensor(handle) => runmat_accelerate_api::handle_is_logical(handle),
887        _ => false,
888    }
889}
890
891struct PreparedHandle {
892    handle: GpuTensorHandle,
893    provider: &'static dyn runmat_accelerate_api::AccelProvider,
894    logical: bool,
895    storage: runmat_accelerate_api::GpuTensorStorage,
896    owns_handle: bool,
897}
898
899fn upload_host_value(value: Value, dtype: DataClass) -> BuiltinResult<PreparedHandle> {
900    #[cfg(all(test, feature = "wgpu"))]
901    {
902        if runmat_accelerate_api::provider().is_none() {
903            let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
904                runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
905            );
906        }
907    }
908    let provider = runmat_accelerate_api::provider()
909        .ok_or_else(|| gpu_array_error(&GPUARRAY_ERROR_NO_PROVIDER))?;
910
911    match value {
912        Value::Complex(re, im) => {
913            let tensor = ComplexTensor::new(vec![(re, im)], vec![1, 1]).map_err(|err| {
914                gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
915            })?;
916            upload_complex_host_value(provider, tensor, dtype)
917        }
918        Value::ComplexTensor(tensor) => upload_complex_host_value(provider, tensor, dtype),
919        value => upload_real_host_value(provider, value, dtype),
920    }
921}
922
923fn upload_real_host_value(
924    provider: &'static dyn runmat_accelerate_api::AccelProvider,
925    value: Value,
926    dtype: DataClass,
927) -> BuiltinResult<PreparedHandle> {
928    if dtype.is_integer() {
929        let (storage, shape) = match value {
930            Value::Int(value) => {
931                let source = IntegerStorage::from_scalar(value);
932                (cast_integer_storage(&source, dtype)?, vec![1, 1])
933            }
934            Value::Tensor(tensor) => {
935                let shape = tensor.shape.clone();
936                let storage = if let Some(storage) = tensor.integer_storage().cloned() {
937                    cast_integer_storage(&storage, dtype)?
938                } else {
939                    let values = tensor::tensor_into_values_f64(tensor);
940                    Tensor::new_with_dtype(values, shape.clone(), dtype.numeric_dtype())
941                        .map_err(|err| {
942                            gpu_array_error_with_message(
943                                format!("gpuArray: {err}"),
944                                &GPUARRAY_ERROR_CONVERSION,
945                            )
946                        })?
947                        .integer_storage()
948                        .expect("integer dtype constructs integer storage")
949                        .clone()
950                };
951                (storage, shape)
952            }
953            other => {
954                let tensor = coerce_host_value(other)?;
955                let shape = tensor.shape.clone();
956                let values = tensor::tensor_into_values_f64(tensor);
957                let storage = Tensor::new_with_dtype(values, shape.clone(), dtype.numeric_dtype())
958                    .map_err(|err| {
959                        gpu_array_error_with_message(
960                            format!("gpuArray: {err}"),
961                            &GPUARRAY_ERROR_CONVERSION,
962                        )
963                    })?
964                    .integer_storage()
965                    .expect("integer dtype constructs integer storage")
966                    .clone();
967                (storage, shape)
968            }
969        };
970        let tensor = Tensor::new_integer(storage, shape).map_err(|err| {
971            gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_CONVERSION)
972        })?;
973        let handle = gpu_helpers::upload_tensor(provider, &tensor).map_err(|err| {
974            gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_PROVIDER_IO)
975        })?;
976        return Ok(PreparedHandle {
977            handle,
978            provider,
979            logical: false,
980            storage: runmat_accelerate_api::GpuTensorStorage::Real,
981            owns_handle: true,
982        });
983    }
984    let tensor = coerce_host_value(value)?;
985    let (tensor, logical) = cast_tensor(tensor, dtype)?;
986    let tensor = physical_logical_tensor(tensor, logical, provider.precision())?;
987    let new_handle = gpu_helpers::upload_tensor(provider, &tensor).map_err(|err| {
988        gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_PROVIDER_IO)
989    })?;
990    if logical {
991        runmat_accelerate_api::set_handle_logical(&new_handle, true);
992    }
993
994    Ok(PreparedHandle {
995        handle: new_handle,
996        provider,
997        logical,
998        storage: runmat_accelerate_api::GpuTensorStorage::Real,
999        owns_handle: true,
1000    })
1001}
1002
1003fn cast_integer_storage(
1004    source: &IntegerStorage,
1005    dtype: DataClass,
1006) -> BuiltinResult<IntegerStorage> {
1007    let Some(prototype) = integer_prototype(dtype) else {
1008        return Err(gpu_array_error(&GPUARRAY_ERROR_TYPED_INTEGER));
1009    };
1010    let values = source
1011        .exact_values()
1012        .into_iter()
1013        .map(|value| prototype.cast_exact_assignment(&value))
1014        .collect();
1015    prototype.from_same_class_values(values).map_err(|err| {
1016        gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_CONVERSION)
1017    })
1018}
1019
1020fn upload_complex_host_value(
1021    provider: &'static dyn runmat_accelerate_api::AccelProvider,
1022    tensor: ComplexTensor,
1023    dtype: DataClass,
1024) -> BuiltinResult<PreparedHandle> {
1025    let shape = tensor.shape.clone();
1026    let storage = tensor.into_complex_storage();
1027    let storage = match dtype {
1028        DataClass::Double => match storage {
1029            storage @ ComplexStorage::F64(_) => storage,
1030            storage => ComplexStorage::F64(storage.materialize_f64()),
1031        },
1032        DataClass::Single => match storage {
1033            storage @ ComplexStorage::F32(_) => storage,
1034            storage => ComplexStorage::F32(
1035                storage
1036                    .materialize_f64()
1037                    .into_iter()
1038                    .map(|(real, imag)| (real as f32, imag as f32))
1039                    .collect(),
1040            ),
1041        },
1042        dtype @ (DataClass::Int8
1043        | DataClass::Int16
1044        | DataClass::Int32
1045        | DataClass::Int64
1046        | DataClass::UInt8
1047        | DataClass::UInt16
1048        | DataClass::UInt32
1049        | DataClass::UInt64) => {
1050            let ComplexStorage::Integer(storage) = storage else {
1051                return Err(gpu_array_error_with_message(
1052                    "gpuArray: converting floating complex input to an integer class is not supported",
1053                    &GPUARRAY_ERROR_INPUT_TYPE,
1054                ));
1055            };
1056            ComplexStorage::Integer(
1057                IntegerComplexStorage::new(
1058                    cast_integer_storage(&storage.real, dtype)?,
1059                    cast_integer_storage(&storage.imag, dtype)?,
1060                )
1061                .map_err(|error| gpu_array_error_with_message(error, &GPUARRAY_ERROR_CONVERSION))?,
1062            )
1063        }
1064        DataClass::Logical => {
1065            return Err(gpu_array_error_with_message(
1066                "gpuArray: complex inputs cannot be uploaded as logical storage",
1067                &GPUARRAY_ERROR_INPUT_TYPE,
1068            ));
1069        }
1070    };
1071    let tensor = ComplexTensor::from_complex_storage(storage, shape)
1072        .map_err(|error| gpu_array_error_with_message(error, &GPUARRAY_ERROR_INPUT_TYPE))?;
1073
1074    let handle = gpu_helpers::upload_complex_tensor(provider, &tensor).map_err(|err| {
1075        gpu_array_error_with_message(err.to_string(), &GPUARRAY_ERROR_PROVIDER_IO)
1076    })?;
1077    Ok(PreparedHandle {
1078        handle,
1079        provider,
1080        logical: false,
1081        storage: runmat_accelerate_api::GpuTensorStorage::ComplexInterleaved,
1082        owns_handle: true,
1083    })
1084}
1085
1086async fn convert_device_value(
1087    handle: GpuTensorHandle,
1088    dtype: DataClass,
1089) -> BuiltinResult<PreparedHandle> {
1090    let provider = runmat_accelerate_api::provider_for_handle(&handle)
1091        .ok_or_else(|| gpu_array_error(&GPUARRAY_ERROR_NO_PROVIDER))?;
1092    let was_logical = runmat_accelerate_api::handle_is_logical(&handle);
1093    let was_complex = runmat_accelerate_api::handle_storage(&handle)
1094        == runmat_accelerate_api::GpuTensorStorage::ComplexInterleaved;
1095    let current_precision = runmat_accelerate_api::handle_precision(&handle);
1096    let integer_type = runmat_accelerate_api::handle_integer_type(&handle);
1097    match dtype {
1098        DataClass::Double => {
1099            if !was_logical
1100                && integer_type.is_none()
1101                && current_precision == Some(runmat_accelerate_api::ProviderPrecision::F64)
1102            {
1103                return Ok(PreparedHandle {
1104                    handle,
1105                    provider,
1106                    logical: false,
1107                    storage: if was_complex {
1108                        runmat_accelerate_api::GpuTensorStorage::ComplexInterleaved
1109                    } else {
1110                        runmat_accelerate_api::GpuTensorStorage::Real
1111                    },
1112                    owns_handle: false,
1113                });
1114            }
1115        }
1116        DataClass::Logical => {
1117            if was_logical {
1118                return Ok(PreparedHandle {
1119                    handle,
1120                    provider,
1121                    logical: true,
1122                    storage: runmat_accelerate_api::GpuTensorStorage::Real,
1123                    owns_handle: false,
1124                });
1125            }
1126        }
1127        dtype if dtype.is_integer() => {
1128            if integer_type.is_some()
1129                && dtype_from_gpu_handle(&handle)
1130                    .ok()
1131                    .is_some_and(|current| current == dtype)
1132            {
1133                return Ok(PreparedHandle {
1134                    handle,
1135                    provider,
1136                    logical: false,
1137                    storage: if was_complex {
1138                        runmat_accelerate_api::GpuTensorStorage::ComplexInterleaved
1139                    } else {
1140                        runmat_accelerate_api::GpuTensorStorage::Real
1141                    },
1142                    owns_handle: false,
1143                });
1144            }
1145        }
1146        _ => {}
1147    }
1148
1149    if was_complex {
1150        let gathered = gpu_helpers::gather_value_async(&Value::GpuTensor(handle.clone()))
1151            .await
1152            .map_err(|err| {
1153                gpu_array_error_with_message(err.to_string(), &GPUARRAY_ERROR_PROVIDER_IO)
1154            })?;
1155        let Value::ComplexTensor(tensor) = gathered else {
1156            return Err(gpu_array_error_with_message(
1157                "gpuArray: expected complex gpuArray data during conversion",
1158                &GPUARRAY_ERROR_PROVIDER_IO,
1159            ));
1160        };
1161        let prepared = upload_complex_host_value(provider, tensor, dtype)?;
1162        return Ok(prepared);
1163    }
1164
1165    if let Some(integer_type) = integer_type.filter(|_| dtype.is_integer()) {
1166        let downloaded = provider.download_integer(&handle).await.map_err(|err| {
1167            gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_PROVIDER_IO)
1168        })?;
1169        if downloaded.shape != handle.shape || downloaded.data.element_type() != integer_type {
1170            return Err(gpu_array_error_with_message(
1171                "gpuArray: provider returned contradictory integer payload metadata",
1172                &GPUARRAY_ERROR_PROVIDER_IO,
1173            ));
1174        }
1175        let shape = downloaded.shape;
1176        let storage = gpu_helpers::integer_storage_from_owned(downloaded.data);
1177        let cast = cast_integer_storage(&storage, dtype)?;
1178        let tensor = Tensor::new_integer(cast, shape).map_err(|err| {
1179            gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_CONVERSION)
1180        })?;
1181        let new_handle = gpu_helpers::upload_tensor(provider, &tensor).map_err(|err| {
1182            gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_PROVIDER_IO)
1183        })?;
1184        return Ok(PreparedHandle {
1185            handle: new_handle,
1186            provider,
1187            logical: false,
1188            storage: runmat_accelerate_api::GpuTensorStorage::Real,
1189            owns_handle: true,
1190        });
1191    }
1192
1193    let tensor = gpu_helpers::gather_tensor_async(&handle)
1194        .await
1195        .map_err(|err| {
1196            gpu_array_error_with_message(err.to_string(), &GPUARRAY_ERROR_PROVIDER_IO)
1197        })?;
1198    let (tensor, logical) = cast_tensor(tensor, dtype)?;
1199    let tensor = physical_logical_tensor(tensor, logical, provider.precision())?;
1200    let new_handle = gpu_helpers::upload_tensor(provider, &tensor).map_err(|err| {
1201        gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_PROVIDER_IO)
1202    })?;
1203    if logical {
1204        runmat_accelerate_api::set_handle_logical(&new_handle, true);
1205    }
1206
1207    Ok(PreparedHandle {
1208        handle: new_handle,
1209        provider,
1210        logical,
1211        storage: runmat_accelerate_api::GpuTensorStorage::Real,
1212        owns_handle: true,
1213    })
1214}
1215
1216fn validate_prepared_handle(
1217    handle: &GpuTensorHandle,
1218    provider: &'static dyn runmat_accelerate_api::AccelProvider,
1219    expected_shape: &[usize],
1220    expected_storage: runmat_accelerate_api::GpuTensorStorage,
1221    dtype: DataClass,
1222    logical: bool,
1223) -> BuiltinResult<()> {
1224    let expected_element = match dtype {
1225        DataClass::Double => runmat_accelerate_api::NumericElementType::F64,
1226        DataClass::Single => runmat_accelerate_api::NumericElementType::F32,
1227        DataClass::Logical => match provider.precision() {
1228            ProviderPrecision::F32 => runmat_accelerate_api::NumericElementType::F32,
1229            ProviderPrecision::F64 => runmat_accelerate_api::NumericElementType::F64,
1230        },
1231        DataClass::Int8 => runmat_accelerate_api::NumericElementType::I8,
1232        DataClass::Int16 => runmat_accelerate_api::NumericElementType::I16,
1233        DataClass::Int32 => runmat_accelerate_api::NumericElementType::I32,
1234        DataClass::Int64 => runmat_accelerate_api::NumericElementType::I64,
1235        DataClass::UInt8 => runmat_accelerate_api::NumericElementType::U8,
1236        DataClass::UInt16 => runmat_accelerate_api::NumericElementType::U16,
1237        DataClass::UInt32 => runmat_accelerate_api::NumericElementType::U32,
1238        DataClass::UInt64 => runmat_accelerate_api::NumericElementType::U64,
1239    };
1240    let owner_matches = runmat_accelerate_api::provider_for_handle(handle)
1241        .is_some_and(|owner| std::ptr::eq(owner, provider));
1242    if !owner_matches
1243        || handle.device_id != provider.device_id()
1244        || handle.shape != expected_shape
1245        || handle.descriptor.storage != Some(expected_storage)
1246        || handle.descriptor.element_type != Some(expected_element)
1247    {
1248        return Err(gpu_array_error_with_message(
1249            "gpuArray: provider returned a handle with the wrong owner or shape",
1250            &GPUARRAY_ERROR_PROVIDER_IO,
1251        ));
1252    }
1253    let existing_class = runmat_accelerate_api::handle_class_name(handle);
1254    if (runmat_accelerate_api::handle_is_logical(handle) && !logical)
1255        || existing_class
1256            .as_deref()
1257            .is_some_and(|class_name| class_name != dtype.class_name())
1258    {
1259        return Err(gpu_array_error_with_message(
1260            "gpuArray: provider returned contradictory class metadata",
1261            &GPUARRAY_ERROR_PROVIDER_IO,
1262        ));
1263    }
1264    Ok(())
1265}
1266
1267fn coerce_host_value(value: Value) -> BuiltinResult<Tensor> {
1268    match value {
1269        Value::Tensor(t) => Ok(t),
1270        Value::LogicalArray(logical) => tensor::logical_to_tensor(&logical).map_err(|err| {
1271            gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
1272        }),
1273        Value::Bool(flag) => {
1274            Tensor::new(vec![if flag { 1.0 } else { 0.0 }], vec![1, 1]).map_err(|err| {
1275                gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
1276            })
1277        }
1278        Value::Num(n) => Tensor::new(vec![n], vec![1, 1]).map_err(|err| {
1279            gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
1280        }),
1281        Value::Int(i) => Tensor::new(vec![i.to_f64()], vec![1, 1]).map_err(|err| {
1282            gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
1283        }),
1284        Value::CharArray(ca) => char_array_to_tensor(&ca),
1285        Value::String(text) => {
1286            let ca = CharArray::new_row(&text);
1287            char_array_to_tensor(&ca)
1288        }
1289        Value::StringArray(_) => Err(gpu_array_error_with_message(
1290            "gpuArray: string arrays are not supported yet; convert to char arrays with CHAR first",
1291            &GPUARRAY_ERROR_INPUT_TYPE,
1292        )),
1293        Value::Complex(_, _) | Value::ComplexTensor(_) => Err(gpu_array_error_with_message(
1294            "gpuArray: internal complex upload routing failed",
1295            &GPUARRAY_ERROR_INTERNAL,
1296        )),
1297        other => Err(gpu_array_error_with_detail(
1298            &GPUARRAY_ERROR_INPUT_TYPE,
1299            format!("unsupported input type for GPU transfer: {other:?}"),
1300        )),
1301    }
1302}
1303
1304fn physical_logical_tensor(
1305    tensor: Tensor,
1306    logical: bool,
1307    precision: ProviderPrecision,
1308) -> BuiltinResult<Tensor> {
1309    if !logical || precision == ProviderPrecision::F64 {
1310        return Ok(tensor);
1311    }
1312    let shape = tensor.shape.clone();
1313    let values = tensor
1314        .into_numeric_storage()
1315        .map(numeric_storage_into_f64)
1316        .map_err(|err| {
1317            gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_CONVERSION)
1318        })?;
1319    Tensor::from_f32(
1320        values.into_iter().map(|value| value as f32).collect(),
1321        shape,
1322    )
1323    .map_err(|err| {
1324        gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_CONVERSION)
1325    })
1326}
1327
1328fn cast_tensor(tensor: Tensor, dtype: DataClass) -> BuiltinResult<(Tensor, bool)> {
1329    // This function is reached for non-integer destinations only.  Native integer
1330    // inputs take the native integer upload path above unless the caller explicitly
1331    // requested a floating or logical class, in which case this is the deliberate
1332    // MATLAB-style cast boundary.
1333    let shape = tensor.shape.clone();
1334    let storage = tensor.into_numeric_storage().map_err(|err| {
1335        gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_CONVERSION)
1336    })?;
1337    let tensor = match dtype {
1338        DataClass::Logical => {
1339            let mut values = numeric_storage_into_f64(storage);
1340            convert_to_logical(&mut values)?;
1341            Tensor::new(values, shape).map_err(|err| {
1342                gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_CONVERSION)
1343            })?
1344        }
1345        DataClass::Single => {
1346            let storage = match storage {
1347                NumericStorage::F32(values) => NumericStorage::F32(values),
1348                storage => {
1349                    let mut values = numeric_storage_into_f64(storage);
1350                    convert_to_single(&mut values);
1351                    NumericStorage::F32(values.into_iter().map(|value| value as f32).collect())
1352                }
1353            };
1354            Tensor::from_numeric_storage(storage, shape).map_err(|err| {
1355                gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_CONVERSION)
1356            })?
1357        }
1358        DataClass::Double => {
1359            let values = numeric_storage_into_f64(storage);
1360            Tensor::new(values, shape).map_err(|err| {
1361                gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_CONVERSION)
1362            })?
1363        }
1364        dtype => {
1365            debug_assert!(dtype.is_integer());
1366            return Err(gpu_array_error_with_message(
1367                format!(
1368                    "gpuArray: internal integer destination {} reached floating upload path",
1369                    dtype.class_name()
1370                ),
1371                &GPUARRAY_ERROR_INTERNAL,
1372            ));
1373        }
1374    };
1375
1376    Ok((tensor, dtype == DataClass::Logical))
1377}
1378
1379fn numeric_storage_into_f64(storage: NumericStorage) -> Vec<f64> {
1380    match storage {
1381        NumericStorage::F64(values) => values,
1382        NumericStorage::F32(values) => values.into_iter().map(f64::from).collect(),
1383        NumericStorage::I8(values) => values.into_iter().map(|value| value as f64).collect(),
1384        NumericStorage::I16(values) => values.into_iter().map(|value| value as f64).collect(),
1385        NumericStorage::I32(values) => values.into_iter().map(|value| value as f64).collect(),
1386        NumericStorage::I64(values) => values.into_iter().map(|value| value as f64).collect(),
1387        NumericStorage::U8(values) => values.into_iter().map(|value| value as f64).collect(),
1388        NumericStorage::U16(values) => values.into_iter().map(|value| value as f64).collect(),
1389        NumericStorage::U32(values) => values.into_iter().map(|value| value as f64).collect(),
1390        NumericStorage::U64(values) => values.into_iter().map(|value| value as f64).collect(),
1391    }
1392}
1393
1394fn convert_to_logical(data: &mut [f64]) -> BuiltinResult<()> {
1395    for value in data.iter_mut() {
1396        if value.is_nan() {
1397            return Err(gpu_array_error_with_message(
1398                "gpuArray: cannot convert NaN to logical",
1399                &GPUARRAY_ERROR_CONVERSION,
1400            ));
1401        }
1402        *value = if *value != 0.0 { 1.0 } else { 0.0 };
1403    }
1404    Ok(())
1405}
1406
1407fn convert_to_single(data: &mut [f64]) {
1408    for value in data.iter_mut() {
1409        *value = (*value as f32) as f64;
1410    }
1411}
1412
1413fn apply_dims(handle: &mut GpuTensorHandle, dims: &[usize]) -> BuiltinResult<()> {
1414    let new_elems: usize = dims.iter().product();
1415    let current_elems: usize = if handle.shape.is_empty() {
1416        new_elems
1417    } else {
1418        handle.shape.iter().product()
1419    };
1420    if new_elems != current_elems {
1421        return Err(gpu_array_error_with_message(
1422            format!(
1423                "gpuArray: cannot reshape gpuArray of {current_elems} elements into size {:?}",
1424                dims
1425            ),
1426            &GPUARRAY_ERROR_RESHAPE,
1427        ));
1428    }
1429    handle.shape = dims.to_vec();
1430    Ok(())
1431}
1432
1433fn char_array_to_tensor(ca: &CharArray) -> BuiltinResult<Tensor> {
1434    let rows = ca.rows;
1435    let cols = ca.cols;
1436    if rows == 0 || cols == 0 {
1437        return Tensor::new(Vec::new(), vec![rows, cols]).map_err(|err| {
1438            gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
1439        });
1440    }
1441    let mut data = vec![0.0; rows * cols];
1442    // Store in row-major to preserve the original character order when interpreted with column-major indexing
1443    for row in 0..rows {
1444        for col in 0..cols {
1445            let idx_char = row * cols + col;
1446            let ch = ca.data[idx_char];
1447            data[row * cols + col] = ch as u32 as f64;
1448        }
1449    }
1450    Tensor::new(data, vec![rows, cols]).map_err(|err| {
1451        gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
1452    })
1453}
1454
1455#[cfg(test)]
1456pub(crate) mod tests {
1457    use super::*;
1458    use crate::builtins::common::test_support;
1459    use futures::executor::block_on;
1460    use runmat_accelerate_api::{GpuTensorStorage, HostTensorView};
1461    use runmat_builtins::{ResolveContext, Type};
1462    use runmat_value::{ComplexTensor, IntegerComplexStorage, IntegerStorage, LogicalArray};
1463
1464    fn call(value: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
1465        let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1466        block_on(gpu_array_builtin(value, rest))
1467    }
1468
1469    fn call_with_mode(
1470        value: Value,
1471        rest: Vec<Value>,
1472        extensions_enabled: bool,
1473    ) -> crate::BuiltinResult<Value> {
1474        let _compat = crate::compatibility::push_runmat_extensions_enabled(extensions_enabled);
1475        block_on(gpu_array_builtin(value, rest))
1476    }
1477
1478    fn gather_complex(value: Value) -> ComplexTensor {
1479        match block_on(crate::dispatcher::gather_if_needed_async(&value)).expect("gather complex") {
1480            Value::ComplexTensor(tensor) => tensor,
1481            other => panic!("expected ComplexTensor, got {other:?}"),
1482        }
1483    }
1484
1485    fn assert_complex_close(actual: &[(f64, f64)], expected: &[(f64, f64)]) {
1486        assert_eq!(actual.len(), expected.len());
1487        for (idx, ((ar, ai), (er, ei))) in actual.iter().zip(expected.iter()).enumerate() {
1488            assert!(
1489                (ar - er).abs() < 1e-12 && (ai - ei).abs() < 1e-12,
1490                "at {idx}: expected ({er}, {ei}), got ({ar}, {ai})"
1491            );
1492        }
1493    }
1494
1495    #[test]
1496    fn gpu_array_rejects_handle_mislabeled_as_another_registered_provider() {
1497        use runmat_accelerate_api::AccelProvider as _;
1498
1499        let _guard = test_support::accel_test_lock();
1500        let producer = Box::leak(Box::new(
1501            runmat_accelerate::simple_provider::InProcessProvider::new(),
1502        ));
1503        let mislabeled_owner = Box::leak(Box::new(
1504            runmat_accelerate::simple_provider::InProcessProvider::new(),
1505        ));
1506        unsafe {
1507            runmat_accelerate_api::register_provider(producer);
1508            runmat_accelerate_api::register_provider(mislabeled_owner);
1509        }
1510        let handle = GpuTensorHandle {
1511            shape: vec![1, 1],
1512            device_id: mislabeled_owner.device_id(),
1513            buffer_id: 991,
1514            descriptor: Default::default(),
1515        };
1516
1517        let error = validate_prepared_handle(
1518            &handle,
1519            producer,
1520            &[1, 1],
1521            GpuTensorStorage::Real,
1522            DataClass::Double,
1523            false,
1524        )
1525        .expect_err("a result must be owned by the producing provider");
1526
1527        assert_eq!(error.identifier(), Some("RunMat:gpuArray:ProviderIO"));
1528    }
1529
1530    #[test]
1531    fn gpu_array_ignores_stale_floating_annotation_on_durable_integer_handle() {
1532        test_support::with_test_provider(|provider| {
1533            let values = [1_i32, 2_i32];
1534            let handle = provider
1535                .upload_integer(&HostIntegerTensorView {
1536                    data: HostIntegerDataView::I32(&values),
1537                    shape: &[1, 2],
1538                })
1539                .expect("integer upload");
1540            validate_prepared_handle(
1541                &handle,
1542                provider,
1543                &[1, 2],
1544                GpuTensorStorage::Real,
1545                DataClass::Int32,
1546                false,
1547            )
1548            .expect("durable integer descriptor takes precedence over a stale annotation");
1549            let _ = provider.free(&handle);
1550        });
1551    }
1552
1553    #[test]
1554    fn gpu_array_extra_construction_forms_follow_compatibility_mode() {
1555        test_support::with_test_provider(|_| {
1556            call_with_mode(Value::Num(1.0), Vec::new(), false)
1557                .expect("MATLAB mode accepts documented gpuArray(X)");
1558        });
1559        for (rest, identifier) in [
1560            (
1561                vec![Value::from(1i32), Value::from(1i32)],
1562                "RunMat:compatibility:GpuArraySizeExtension",
1563            ),
1564            (
1565                vec![Value::from("uint8")],
1566                "RunMat:compatibility:GpuArrayDtypeExtension",
1567            ),
1568            (
1569                vec![Value::from("like"), Value::Num(0.0)],
1570                "RunMat:compatibility:GpuArrayLikeExtension",
1571            ),
1572        ] {
1573            let error = call_with_mode(Value::Num(1.0), rest, false)
1574                .expect_err("MATLAB mode rejects extra gpuArray construction form");
1575            assert_eq!(error.identifier(), Some(identifier));
1576        }
1577    }
1578
1579    #[test]
1580    fn gpu_array_text_upload_is_gated() {
1581        test_support::with_test_provider(|_| {
1582            let chars = CharArray::new("ab".chars().collect(), 1, 2).expect("chars");
1583            let error = call_with_mode(Value::CharArray(chars), Vec::new(), false)
1584                .expect_err("char upload is a RunMat extension");
1585            assert_eq!(
1586                error.identifier(),
1587                Some("RunMat:compatibility:GpuArrayTextUploadExtension")
1588            );
1589
1590            let error = call_with_mode(Value::String("ab".into()), Vec::new(), false)
1591                .expect_err("string upload is a RunMat extension");
1592            assert_eq!(
1593                error.identifier(),
1594                Some("RunMat:compatibility:GpuArrayTextUploadExtension")
1595            );
1596        });
1597    }
1598
1599    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1600    #[test]
1601    fn gpu_array_transfers_numeric_tensor() {
1602        test_support::with_test_provider(|_| {
1603            let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]).unwrap();
1604            let result = call(Value::Tensor(tensor.clone()), Vec::new()).expect("gpuArray upload");
1605            let Value::GpuTensor(handle) = result else {
1606                panic!("expected gpu tensor");
1607            };
1608            assert_eq!(handle.shape, tensor.shape);
1609            assert!(runmat_accelerate_api::handle_is_explicit(&handle));
1610            let gathered =
1611                test_support::gather(Value::GpuTensor(handle.clone())).expect("gather values");
1612            assert_eq!(gathered.shape, tensor.shape);
1613            assert_eq!(gathered.materialize_f64(), tensor.materialize_f64());
1614        });
1615    }
1616
1617    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1618    #[test]
1619    fn gpu_array_infers_and_preserves_native_single_tensor_class() {
1620        test_support::with_f32_test_provider(|_| {
1621            let tensor = Tensor::from_f32(vec![1.25, -3.5], vec![2, 1]).expect("single tensor");
1622            let result = call(Value::Tensor(tensor), Vec::new()).expect("gpuArray single upload");
1623            let Value::GpuTensor(handle) = result else {
1624                panic!("expected gpu tensor");
1625            };
1626            assert_eq!(
1627                runmat_accelerate_api::handle_class_name(&handle).as_deref(),
1628                Some("single")
1629            );
1630            assert_eq!(
1631                runmat_accelerate_api::handle_precision(&handle),
1632                Some(ProviderPrecision::F32)
1633            );
1634
1635            let gathered =
1636                test_support::gather(Value::GpuTensor(handle)).expect("gather native single");
1637            assert_eq!(gathered.numeric_dtype(), NumericDType::F32);
1638            assert_eq!(
1639                gathered.into_numeric_storage().expect("single storage"),
1640                NumericStorage::F32(vec![1.25, -3.5])
1641            );
1642
1643            let prototype =
1644                Tensor::from_f32(vec![0.0], vec![1, 1]).expect("single prototype tensor");
1645            let like_result = call(
1646                Value::Tensor(Tensor::new(vec![2.5], vec![1, 1]).expect("double input")),
1647                vec![Value::from("like"), Value::Tensor(prototype)],
1648            )
1649            .expect("gpuArray like single");
1650            let Value::GpuTensor(like_handle) = like_result else {
1651                panic!("expected like gpu tensor");
1652            };
1653            assert_eq!(
1654                runmat_accelerate_api::handle_class_name(&like_handle).as_deref(),
1655                Some("single")
1656            );
1657            assert_eq!(
1658                runmat_accelerate_api::handle_precision(&like_handle),
1659                Some(ProviderPrecision::F32)
1660            );
1661        });
1662    }
1663
1664    #[test]
1665    fn gpu_array_of_existing_single_handle_is_identity() {
1666        test_support::with_f32_test_provider(|_| {
1667            let uploaded = call(
1668                Value::Tensor(Tensor::from_f32(vec![1.25, -3.5], vec![2, 1]).unwrap()),
1669                Vec::new(),
1670            )
1671            .expect("single upload");
1672            let Value::GpuTensor(handle) = uploaded else {
1673                panic!("expected gpu tensor");
1674            };
1675            let identity =
1676                call(Value::GpuTensor(handle.clone()), Vec::new()).expect("gpuArray identity");
1677            let Value::GpuTensor(identity) = identity else {
1678                panic!("expected gpu tensor");
1679            };
1680            assert_eq!(
1681                runmat_accelerate_api::handle_identity(&identity),
1682                runmat_accelerate_api::handle_identity(&handle)
1683            );
1684            assert_eq!(
1685                identity.descriptor.provenance,
1686                Some(runmat_accelerate_api::GpuHandleProvenance::Explicit)
1687            );
1688            let gathered = test_support::gather(Value::GpuTensor(handle.clone()))
1689                .expect("source remains valid");
1690            assert_eq!(gathered.numeric_dtype(), NumericDType::F32);
1691            assert_eq!(gathered.materialize_f64(), vec![1.25, -3.5]);
1692        });
1693    }
1694
1695    #[test]
1696    fn direct_gpu_builtin_results_preserve_explicit_gpuarray_intent() {
1697        test_support::with_test_provider(|_| {
1698            let input = call(
1699                Value::Tensor(Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap()),
1700                Vec::new(),
1701            )
1702            .expect("explicit gpuArray");
1703            let output =
1704                crate::call_builtin("plus", &[input.clone(), input]).expect("direct provider plus");
1705            let Value::GpuTensor(handle) = &output else {
1706                panic!("expected resident plus output");
1707            };
1708            assert!(runmat_accelerate_api::handle_is_explicit(handle));
1709            assert_eq!(
1710                handle.descriptor.provenance,
1711                Some(runmat_accelerate_api::GpuHandleProvenance::Explicit)
1712            );
1713            assert_eq!(
1714                crate::call_builtin("isgpuarray", &[output]).expect("isgpuarray"),
1715                Value::Bool(true)
1716            );
1717        });
1718    }
1719
1720    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1721    #[test]
1722    fn gpu_array_rewraps_integer_handles_without_double_roundtrip() {
1723        test_support::with_test_provider(|_| {
1724            let tensor = Tensor::new_integer(
1725                IntegerStorage::U64(vec![9_223_372_036_854_775_808, u64::MAX]),
1726                vec![1, 2],
1727            )
1728            .expect("integer tensor");
1729            let uploaded =
1730                call(Value::Tensor(tensor), Vec::new()).expect("gpuArray integer upload");
1731            let Value::GpuTensor(handle) = uploaded else {
1732                panic!("expected integer gpu tensor");
1733            };
1734
1735            let rewrapped =
1736                call(Value::GpuTensor(handle.clone()), Vec::new()).expect("gpuArray rewrap");
1737            let Value::GpuTensor(rewrapped) = rewrapped else {
1738                panic!("expected rewrapped gpu tensor");
1739            };
1740            assert_eq!(rewrapped.buffer_id, handle.buffer_id);
1741            assert_eq!(
1742                runmat_accelerate_api::handle_class_name(&rewrapped).as_deref(),
1743                Some("uint64")
1744            );
1745
1746            let gathered =
1747                test_support::gather(Value::GpuTensor(rewrapped)).expect("gather integer");
1748            assert_eq!(
1749                gathered.integer_storage(),
1750                Some(&IntegerStorage::U64(vec![
1751                    9_223_372_036_854_775_808,
1752                    u64::MAX,
1753                ]))
1754            );
1755        });
1756    }
1757
1758    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1759    #[test]
1760    fn gpu_array_noninteger_class_uploads_read_typed_integer_storage_exactly() {
1761        test_support::with_test_provider(|_| {
1762            for (class_name, expected, logical) in [
1763                ("double", vec![2.0, 4.0], false),
1764                ("logical", vec![1.0, 1.0], true),
1765            ] {
1766                let tensor = Tensor::new_integer(IntegerStorage::I32(vec![2, 4]), vec![1, 2])
1767                    .expect("integer tensor");
1768
1769                let uploaded = call(Value::Tensor(tensor), vec![Value::from(class_name)])
1770                    .expect("gpuArray class upload");
1771                let Value::GpuTensor(handle) = uploaded else {
1772                    panic!("expected gpu tensor for {class_name}");
1773                };
1774                assert_eq!(runmat_accelerate_api::handle_is_logical(&handle), logical);
1775                assert!(runmat_accelerate_api::handle_integer_type(&handle).is_none());
1776                let gathered =
1777                    test_support::gather(Value::GpuTensor(handle)).expect("gather uploaded tensor");
1778                assert_eq!(gathered.shape, vec![1, 2]);
1779                assert_eq!(gathered.materialize_f64(), expected, "{class_name}");
1780            }
1781        });
1782        test_support::with_f32_test_provider(|_| {
1783            let tensor = Tensor::new_integer(IntegerStorage::I32(vec![2, 4]), vec![1, 2])
1784                .expect("integer tensor");
1785            let uploaded = call(Value::Tensor(tensor), vec![Value::from("single")])
1786                .expect("gpuArray single upload");
1787            let Value::GpuTensor(handle) = uploaded else {
1788                panic!("expected single gpu tensor");
1789            };
1790            let gathered = test_support::gather(Value::GpuTensor(handle)).expect("gather single");
1791            assert_eq!(gathered.numeric_dtype(), NumericDType::F32);
1792            assert_eq!(gathered.materialize_f64(), vec![2.0, 4.0]);
1793        });
1794    }
1795
1796    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1797    #[test]
1798    fn gpu_array_integer_handle_conversion_uses_integer_buffers() {
1799        test_support::with_test_provider(|_| {
1800            let tensor = Tensor::new_integer(
1801                IntegerStorage::U64(vec![0, 9_223_372_036_854_775_808, u64::MAX]),
1802                vec![3, 1],
1803            )
1804            .expect("integer tensor");
1805            let uploaded =
1806                call(Value::Tensor(tensor), Vec::new()).expect("gpuArray integer upload");
1807            let Value::GpuTensor(handle) = uploaded else {
1808                panic!("expected integer gpu tensor");
1809            };
1810
1811            let converted = call(Value::GpuTensor(handle.clone()), vec![Value::from("int16")])
1812                .expect("gpuArray int16 conversion");
1813            let Value::GpuTensor(converted) = converted else {
1814                panic!("expected converted gpu tensor");
1815            };
1816            assert_eq!(
1817                runmat_accelerate_api::handle_class_name(&converted).as_deref(),
1818                Some("int16")
1819            );
1820            let gathered =
1821                test_support::gather(Value::GpuTensor(converted)).expect("gather converted");
1822            assert_eq!(
1823                gathered.integer_storage(),
1824                Some(&IntegerStorage::I16(vec![0, i16::MAX, i16::MAX]))
1825            );
1826            let original =
1827                test_support::gather(Value::GpuTensor(handle)).expect("source remains valid");
1828            assert_eq!(
1829                original.integer_storage(),
1830                Some(&IntegerStorage::U64(vec![
1831                    0,
1832                    9_223_372_036_854_775_808,
1833                    u64::MAX,
1834                ]))
1835            );
1836        });
1837    }
1838
1839    #[test]
1840    fn gpu_array_dimension_preserves_representable_uint64_values() {
1841        let expected = usize::try_from(u64::MAX).ok();
1842        assert_eq!(int_to_dim(&IntValue::U64(u64::MAX)).ok(), expected);
1843        assert!(int_to_dim(&IntValue::I64(-1)).is_err());
1844    }
1845
1846    #[test]
1847    fn gpu_array_tensor_dimensions_use_exact_integer_storage() {
1848        let expected = usize::try_from(u64::MAX).ok();
1849        let dims = Tensor::new_integer(IntegerStorage::U64(vec![u64::MAX]), vec![1, 1])
1850            .expect("integer size vector");
1851        assert_eq!(tensor_to_dims(&dims).ok(), expected.map(|dim| vec![dim]));
1852
1853        let negative = Tensor::new_integer(IntegerStorage::I8(vec![-1]), vec![1, 1])
1854            .expect("integer size vector");
1855        assert!(tensor_to_dims(&negative).is_err());
1856    }
1857
1858    #[test]
1859    fn gpu_array_float_dimensions_reject_unrepresentable_usize_boundary() {
1860        let boundary = if usize::BITS == 64 {
1861            usize::MAX as f64
1862        } else {
1863            (usize::MAX as f64) + 1.0
1864        };
1865
1866        assert!(float_to_dim(boundary).is_err());
1867    }
1868
1869    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1870    #[test]
1871    fn gpu_array_marks_logical_inputs() {
1872        test_support::with_test_provider(|_| {
1873            let logical =
1874                LogicalArray::new(vec![1, 0, 1, 1], vec![2, 2]).expect("logical construction");
1875            let result =
1876                call(Value::LogicalArray(logical.clone()), Vec::new()).expect("gpuArray logical");
1877            let Value::GpuTensor(handle) = result else {
1878                panic!("expected gpu tensor");
1879            };
1880            assert!(runmat_accelerate_api::handle_is_logical(&handle));
1881            let gathered =
1882                test_support::gather(Value::GpuTensor(handle.clone())).expect("gather logical");
1883            assert_eq!(gathered.shape, logical.shape);
1884            assert_eq!(gathered.materialize_f64(), vec![1.0, 0.0, 1.0, 1.0]);
1885        });
1886    }
1887
1888    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1889    #[test]
1890    fn gpu_array_uploads_complex_tensor() {
1891        test_support::with_test_provider(|_| {
1892            let complex = ComplexTensor::new(vec![(1.0, -2.0), (3.5, 4.25)], vec![1, 2]).unwrap();
1893            let result =
1894                call(Value::ComplexTensor(complex.clone()), Vec::new()).expect("gpuArray complex");
1895            let Value::GpuTensor(handle) = result else {
1896                panic!("expected gpu tensor");
1897            };
1898            assert_eq!(
1899                runmat_accelerate_api::handle_storage(&handle),
1900                GpuTensorStorage::ComplexInterleaved
1901            );
1902            let gathered = gather_complex(Value::GpuTensor(handle.clone()));
1903            assert_eq!(gathered.shape, complex.shape);
1904            assert_complex_close(&gathered.materialize_f64(), &complex.materialize_f64());
1905        });
1906    }
1907
1908    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1909    #[test]
1910    fn gpu_array_round_trips_typed_complex_integer_tensor_exactly() {
1911        test_support::with_test_provider(|_| {
1912            let complex = ComplexTensor::new_integer(
1913                IntegerComplexStorage::new(
1914                    IntegerStorage::U64(vec![1_u64 << 63, u64::MAX]),
1915                    IntegerStorage::U64(vec![1, 2]),
1916                )
1917                .unwrap(),
1918                vec![1, 2],
1919            )
1920            .unwrap();
1921            let result = call(Value::ComplexTensor(complex.clone()), Vec::new())
1922                .expect("typed complex integer upload");
1923            let Value::GpuTensor(handle) = &result else {
1924                panic!("expected gpuArray handle")
1925            };
1926            assert_eq!(
1927                runmat_accelerate_api::handle_integer_type(handle),
1928                Some(runmat_accelerate_api::IntegerElementType::U64)
1929            );
1930            assert_eq!(
1931                runmat_accelerate_api::handle_storage(handle),
1932                GpuTensorStorage::ComplexInterleaved
1933            );
1934            assert_eq!(gather_complex(result), complex);
1935        });
1936    }
1937
1938    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1939    #[test]
1940    fn gpu_array_handles_scalar_bool() {
1941        test_support::with_test_provider(|_| {
1942            let result = call(Value::Bool(true), Vec::new()).expect("gpuArray bool");
1943            let Value::GpuTensor(handle) = result else {
1944                panic!("expected gpu tensor");
1945            };
1946            assert!(runmat_accelerate_api::handle_is_logical(&handle));
1947            let gathered =
1948                test_support::gather(Value::GpuTensor(handle.clone())).expect("gather bool");
1949            assert_eq!(gathered.shape, vec![1, 1]);
1950            assert_eq!(gathered.materialize_f64(), vec![1.0]);
1951        });
1952    }
1953
1954    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1955    #[test]
1956    fn gpu_array_supports_char_arrays() {
1957        test_support::with_test_provider(|_| {
1958            let chars = CharArray::new("row1row2".chars().collect(), 2, 4).unwrap();
1959            let original: Vec<char> = chars.data.clone();
1960            let result =
1961                call(Value::CharArray(chars), Vec::new()).expect("gpuArray char array upload");
1962            let Value::GpuTensor(handle) = result else {
1963                panic!("expected gpu tensor");
1964            };
1965            let gathered =
1966                test_support::gather(Value::GpuTensor(handle.clone())).expect("gather chars");
1967            assert_eq!(gathered.shape, vec![2, 4]);
1968            let mut recovered = Vec::new();
1969            for col in 0..4 {
1970                for row in 0..2 {
1971                    let idx = row + col * 2;
1972                    let code = gathered.materialize_f64()[idx];
1973                    let ch = char::from_u32(code as u32)
1974                        .expect("valid unicode scalar from numeric code");
1975                    recovered.push(ch);
1976                }
1977            }
1978            assert_eq!(recovered, original);
1979        });
1980    }
1981
1982    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1983    #[test]
1984    fn gpu_array_converts_strings() {
1985        test_support::with_test_provider(|_| {
1986            let result = call(Value::String("gpu".into()), Vec::new()).expect("gpuArray string");
1987            let Value::GpuTensor(handle) = result else {
1988                panic!("expected gpu tensor");
1989            };
1990            let gathered =
1991                test_support::gather(Value::GpuTensor(handle.clone())).expect("gather string");
1992            assert_eq!(gathered.shape, vec![1, 3]);
1993            let expected: Vec<f64> = "gpu".chars().map(|ch| ch as u32 as f64).collect();
1994            assert_eq!(gathered.materialize_f64(), expected);
1995        });
1996    }
1997
1998    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1999    #[test]
2000    fn gpu_array_passthrough_existing_handle() {
2001        test_support::with_test_provider(|provider| {
2002            let tensor = Tensor::new(vec![5.0, 6.0], vec![2, 1]).unwrap();
2003            let view = HostTensorView {
2004                data: &tensor.materialize_f64(),
2005                shape: &tensor.shape,
2006            };
2007            let handle = provider.upload(&view).expect("upload");
2008            let cloned = handle.clone();
2009            let result =
2010                call(Value::GpuTensor(handle.clone()), Vec::new()).expect("gpuArray passthrough");
2011            let Value::GpuTensor(returned) = result else {
2012                panic!("expected gpu tensor");
2013            };
2014            assert_eq!(returned.buffer_id, cloned.buffer_id);
2015            assert_eq!(returned.shape, cloned.shape);
2016        });
2017    }
2018
2019    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2020    #[test]
2021    fn gpu_array_passthrough_existing_complex_handle() {
2022        test_support::with_test_provider(|provider| {
2023            let complex = ComplexTensor::new(vec![(2.0, 3.0), (-4.0, 5.5)], vec![2, 1]).unwrap();
2024            let handle = gpu_helpers::upload_complex_tensor(provider, &complex).unwrap();
2025            let result =
2026                call(Value::GpuTensor(handle.clone()), Vec::new()).expect("gpuArray passthrough");
2027            let Value::GpuTensor(returned) = result else {
2028                panic!("expected gpu tensor");
2029            };
2030            assert_eq!(returned.buffer_id, handle.buffer_id);
2031            assert_eq!(
2032                runmat_accelerate_api::handle_storage(&returned),
2033                GpuTensorStorage::ComplexInterleaved
2034            );
2035            let gathered = gather_complex(Value::GpuTensor(returned.clone()));
2036            assert_complex_close(&gathered.materialize_f64(), &complex.materialize_f64());
2037        });
2038    }
2039
2040    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2041    #[test]
2042    fn gpu_array_complex_gpu_to_single_uses_native_transfer_storage() {
2043        test_support::with_test_provider(|provider| {
2044            let complex = ComplexTensor::new(
2045                vec![(1.234_567_89, -2.345_678_91), (3.456_789_12, 4.567_891_23)],
2046                vec![1, 2],
2047            )
2048            .unwrap();
2049            let handle = gpu_helpers::upload_complex_tensor(provider, &complex).unwrap();
2050            let converted = call(
2051                Value::GpuTensor(handle.clone()),
2052                vec![Value::from("single")],
2053            )
2054            .expect("shared transfer storage preserves complex single");
2055            let gathered = gather_complex(converted);
2056            assert_eq!(gathered.numeric_dtype(), NumericDType::F32);
2057            let expected: Vec<(f64, f64)> = complex
2058                .materialize_f64()
2059                .into_iter()
2060                .map(|(real, imag)| (f64::from(real as f32), f64::from(imag as f32)))
2061                .collect();
2062            assert_complex_close(&gathered.materialize_f64(), &expected);
2063            let source = gather_complex(Value::GpuTensor(handle));
2064            assert_eq!(source, complex);
2065        });
2066    }
2067
2068    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2069    #[test]
2070    fn gpu_array_casts_to_int32() {
2071        test_support::with_test_provider(|_| {
2072            let tensor = Tensor::new(vec![1.2, -3.7, 123456.0], vec![3, 1]).unwrap();
2073            let result =
2074                call(Value::Tensor(tensor), vec![Value::from("int32")]).expect("gpuArray int32");
2075            let Value::GpuTensor(handle) = result else {
2076                panic!("expected gpu tensor");
2077            };
2078            let gathered =
2079                test_support::gather(Value::GpuTensor(handle.clone())).expect("gather int32");
2080            assert_eq!(gathered.materialize_f64(), vec![1.0, -4.0, 123456.0]);
2081        });
2082    }
2083
2084    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2085    #[test]
2086    fn gpu_array_casts_to_uint8() {
2087        test_support::with_test_provider(|_| {
2088            let tensor = Tensor::new(vec![-12.0, 12.8, 300.4, f64::INFINITY], vec![4, 1]).unwrap();
2089            let result =
2090                call(Value::Tensor(tensor), vec![Value::from("uint8")]).expect("gpuArray uint8");
2091            let Value::GpuTensor(handle) = result else {
2092                panic!("expected gpu tensor");
2093            };
2094            let gathered =
2095                test_support::gather(Value::GpuTensor(handle.clone())).expect("gather uint8");
2096            assert_eq!(gathered.materialize_f64(), vec![0.0, 13.0, 255.0, 255.0]);
2097        });
2098    }
2099
2100    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2101    #[test]
2102    fn gpu_array_single_precision_rounds() {
2103        test_support::with_f32_test_provider(|_| {
2104            let tensor = Tensor::new(vec![1.23456789, -9.87654321], vec![2, 1]).unwrap();
2105            let result =
2106                call(Value::Tensor(tensor), vec![Value::from("single")]).expect("gpuArray single");
2107            let Value::GpuTensor(handle) = result else {
2108                panic!("expected gpu tensor");
2109            };
2110            let gathered =
2111                test_support::gather(Value::GpuTensor(handle.clone())).expect("gather single");
2112            let expected = [1.234_567_9_f32 as f64, (-9.876_543_f32) as f64];
2113            for (observed, expected) in gathered.materialize_f64().iter().zip(expected.iter()) {
2114                assert!((observed - expected).abs() < 1e-6);
2115            }
2116        });
2117    }
2118
2119    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2120    #[test]
2121    fn gpu_array_like_infers_logical() {
2122        test_support::with_test_provider(|_| {
2123            let tensor = Tensor::new(vec![0.0, 2.0, -3.0], vec![3, 1]).unwrap();
2124            let logical_proto =
2125                LogicalArray::new(vec![0, 1, 0], vec![3, 1]).expect("logical proto");
2126            let result = call(
2127                Value::Tensor(tensor),
2128                vec![Value::from("like"), Value::LogicalArray(logical_proto)],
2129            )
2130            .expect("gpuArray like logical");
2131            let Value::GpuTensor(handle) = result else {
2132                panic!("expected gpu tensor");
2133            };
2134            assert!(runmat_accelerate_api::handle_is_logical(&handle));
2135            let gathered = test_support::gather(Value::GpuTensor(handle.clone())).expect("gather");
2136            assert_eq!(gathered.materialize_f64(), vec![0.0, 1.0, 1.0]);
2137        });
2138    }
2139
2140    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2141    #[test]
2142    fn gpu_array_like_requires_argument() {
2143        test_support::with_test_provider(|_| {
2144            let tensor = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
2145            let err = call(Value::Tensor(tensor), vec![Value::from("like")]).unwrap_err();
2146            assert_eq!(err.to_string(), GPUARRAY_ERROR_LIKE_MISSING.message);
2147            assert_eq!(err.identifier(), GPUARRAY_ERROR_LIKE_MISSING.identifier);
2148        });
2149    }
2150
2151    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2152    #[test]
2153    fn gpu_array_unknown_option_errors() {
2154        test_support::with_test_provider(|_| {
2155            let tensor = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
2156            let err = call(Value::Tensor(tensor), vec![Value::from("mystery")]).unwrap_err();
2157            assert!(err
2158                .to_string()
2159                .contains(GPUARRAY_ERROR_UNKNOWN_OPTION.message));
2160            assert_eq!(err.identifier(), GPUARRAY_ERROR_UNKNOWN_OPTION.identifier);
2161        });
2162    }
2163
2164    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2165    #[test]
2166    fn gpu_array_gpu_to_logical_reuploads() {
2167        test_support::with_test_provider(|provider| {
2168            let tensor = Tensor::new(vec![2.0, 0.0, -5.5], vec![3, 1]).unwrap();
2169            let view = HostTensorView {
2170                data: &tensor.materialize_f64(),
2171                shape: &tensor.shape,
2172            };
2173            let handle = provider.upload(&view).expect("upload");
2174            let result = call(
2175                Value::GpuTensor(handle.clone()),
2176                vec![Value::from("logical")],
2177            )
2178            .expect("gpuArray logical cast");
2179            let Value::GpuTensor(new_handle) = result else {
2180                panic!("expected gpu tensor");
2181            };
2182            assert!(runmat_accelerate_api::handle_is_logical(&new_handle));
2183            let gathered =
2184                test_support::gather(Value::GpuTensor(new_handle.clone())).expect("gather");
2185            assert_eq!(gathered.materialize_f64(), vec![1.0, 0.0, 1.0]);
2186            provider.free(&handle).ok();
2187            provider.free(&new_handle).ok();
2188        });
2189    }
2190
2191    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2192    #[test]
2193    fn gpu_array_gpu_logical_to_double_clears_flag() {
2194        test_support::with_test_provider(|provider| {
2195            let tensor = Tensor::new(vec![1.0, 0.0], vec![2, 1]).unwrap();
2196            let view = HostTensorView {
2197                data: &tensor.materialize_f64(),
2198                shape: &tensor.shape,
2199            };
2200            let handle = provider.upload(&view).expect("upload");
2201            runmat_accelerate_api::set_handle_logical(&handle, true);
2202            let result = call(
2203                Value::GpuTensor(handle.clone()),
2204                vec![Value::from("double")],
2205            )
2206            .expect("gpuArray double cast");
2207            let Value::GpuTensor(new_handle) = result else {
2208                panic!("expected gpu tensor");
2209            };
2210            assert!(!runmat_accelerate_api::handle_is_logical(&new_handle));
2211            provider.free(&handle).ok();
2212            provider.free(&new_handle).ok();
2213        });
2214    }
2215
2216    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2217    #[test]
2218    fn gpu_array_applies_size_arguments() {
2219        test_support::with_test_provider(|_| {
2220            let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![4, 1]).unwrap();
2221            let result = call(
2222                Value::Tensor(tensor),
2223                vec![Value::from(2i32), Value::from(2i32)],
2224            )
2225            .expect("gpuArray reshape");
2226            let Value::GpuTensor(handle) = result else {
2227                panic!("expected gpu tensor");
2228            };
2229            assert_eq!(handle.shape, vec![2, 2]);
2230        });
2231    }
2232
2233    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2234    #[test]
2235    fn gpu_array_gpu_size_arguments_update_shape() {
2236        test_support::with_test_provider(|provider| {
2237            let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![4, 1]).unwrap();
2238            let view = HostTensorView {
2239                data: &tensor.materialize_f64(),
2240                shape: &tensor.shape,
2241            };
2242            let handle = provider.upload(&view).expect("upload");
2243            let result = call(
2244                Value::GpuTensor(handle.clone()),
2245                vec![Value::from(2i32), Value::from(2i32)],
2246            )
2247            .expect("gpuArray gpu reshape");
2248            let Value::GpuTensor(new_handle) = result else {
2249                panic!("expected gpu tensor");
2250            };
2251            assert_eq!(new_handle.shape, vec![2, 2]);
2252            provider.free(&handle).ok();
2253            provider.free(&new_handle).ok();
2254        });
2255    }
2256
2257    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2258    #[test]
2259    fn gpu_array_size_mismatch_errors() {
2260        test_support::with_test_provider(|_| {
2261            let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
2262            let err = call(
2263                Value::Tensor(tensor),
2264                vec![Value::from(2i32), Value::from(2i32)],
2265            )
2266            .unwrap_err();
2267            assert!(err.to_string().contains("cannot reshape"));
2268            assert_eq!(err.identifier(), GPUARRAY_ERROR_RESHAPE.identifier);
2269        });
2270    }
2271
2272    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2273    #[test]
2274    #[cfg(feature = "wgpu")]
2275    fn gpu_array_wgpu_native_integer_roundtrip() {
2276        use runmat_accelerate_api::AccelProvider;
2277
2278        match runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
2279            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
2280        ) {
2281            Ok(provider) => {
2282                for storage in [
2283                    IntegerStorage::I8(vec![i8::MIN, i8::MAX]),
2284                    IntegerStorage::I16(vec![i16::MIN, i16::MAX]),
2285                    IntegerStorage::I32(vec![i32::MIN, i32::MAX]),
2286                    IntegerStorage::I64(vec![i64::MIN, i64::MAX]),
2287                    IntegerStorage::U8(vec![0, u8::MAX]),
2288                    IntegerStorage::U16(vec![0, u16::MAX]),
2289                    IntegerStorage::U32(vec![0, u32::MAX]),
2290                    IntegerStorage::U64(vec![1_u64 << 63, u64::MAX]),
2291                ] {
2292                    let expected = storage.clone();
2293                    let result = call(
2294                        Value::Tensor(Tensor::new_integer(storage, vec![2, 1]).unwrap()),
2295                        Vec::new(),
2296                    )
2297                    .expect("wgpu integer upload");
2298                    let Value::GpuTensor(handle) = result else {
2299                        panic!("expected gpu tensor");
2300                    };
2301                    assert!(runmat_accelerate_api::handle_integer_type(&handle).is_some());
2302                    let gathered = test_support::gather(Value::GpuTensor(handle.clone()))
2303                        .expect("wgpu integer gather");
2304                    assert_eq!(gathered.shape, vec![2, 1]);
2305                    assert_eq!(gathered.integer_storage(), Some(&expected));
2306                    provider.free(&handle).expect("free integer gpu buffer");
2307                }
2308            }
2309            Err(err) => {
2310                tracing::warn!("Skipping gpu_array_wgpu_native_integer_roundtrip: {err}");
2311            }
2312        }
2313        runmat_accelerate::simple_provider::register_inprocess_provider();
2314    }
2315
2316    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2317    #[test]
2318    #[cfg(feature = "wgpu")]
2319    fn gpu_array_wgpu_u64_linear_gather_and_scatter_remain_exact() {
2320        use runmat_accelerate_api::AccelProvider;
2321
2322        let Ok(provider) = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
2323            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
2324        ) else {
2325            return;
2326        };
2327        let source = match call(
2328            Value::Tensor(
2329                Tensor::new_integer(
2330                    IntegerStorage::U64(vec![0, 1_u64 << 63, u64::MAX]),
2331                    vec![1, 3],
2332                )
2333                .unwrap(),
2334            ),
2335            Vec::new(),
2336        )
2337        .expect("upload source")
2338        {
2339            Value::GpuTensor(handle) => handle,
2340            other => panic!("expected gpu tensor, got {other:?}"),
2341        };
2342        let selected = provider
2343            .gather_linear(&source, &[2, 1], &[1, 2])
2344            .expect("exact u64 gather");
2345        let gathered = test_support::gather(Value::GpuTensor(selected.clone())).expect("gather");
2346        assert_eq!(
2347            gathered.integer_storage(),
2348            Some(&IntegerStorage::U64(vec![u64::MAX, 1_u64 << 63]))
2349        );
2350
2351        let target = match call(
2352            Value::Tensor(
2353                Tensor::new_integer(IntegerStorage::U64(vec![0, 0, 0]), vec![1, 3]).unwrap(),
2354            ),
2355            Vec::new(),
2356        )
2357        .expect("upload target")
2358        {
2359            Value::GpuTensor(handle) => handle,
2360            other => panic!("expected gpu tensor, got {other:?}"),
2361        };
2362        provider
2363            .scatter_linear(&target, &[0, 2], &selected)
2364            .expect("exact u64 scatter");
2365        let gathered = test_support::gather(Value::GpuTensor(target.clone())).expect("gather");
2366        assert_eq!(
2367            gathered.integer_storage(),
2368            Some(&IntegerStorage::U64(vec![u64::MAX, 0, 1_u64 << 63]))
2369        );
2370        for handle in [&source, &selected, &target] {
2371            provider.free(handle).expect("free gpu buffer");
2372        }
2373        runmat_accelerate::simple_provider::register_inprocess_provider();
2374    }
2375
2376    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2377    #[test]
2378    #[cfg(feature = "wgpu")]
2379    fn gpu_array_wgpu_complex_and_integer_roundtrip() {
2380        use runmat_accelerate_api::AccelProvider;
2381
2382        match runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
2383            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
2384        ) {
2385            Ok(provider) => {
2386                let complex =
2387                    ComplexTensor::new(vec![(1.25, -0.5), (-3.0, 4.0)], vec![1, 2]).unwrap();
2388                let result = call(Value::ComplexTensor(complex.clone()), Vec::new())
2389                    .expect("wgpu complex-double upload");
2390                let Value::GpuTensor(handle) = result else {
2391                    panic!("expected gpu tensor");
2392                };
2393                assert_eq!(
2394                    runmat_accelerate_api::handle_storage(&handle),
2395                    GpuTensorStorage::ComplexInterleaved
2396                );
2397                let gathered = gather_complex(Value::GpuTensor(handle.clone()));
2398                assert_eq!(gathered.shape, vec![1, 2]);
2399                assert_complex_close(&gathered.materialize_f64(), &complex.materialize_f64());
2400                provider.free(&handle).ok();
2401
2402                let complex_integer = ComplexTensor::new_integer(
2403                    IntegerComplexStorage::new(
2404                        IntegerStorage::U64(vec![1_u64 << 63, u64::MAX]),
2405                        IntegerStorage::U64(vec![3, 4]),
2406                    )
2407                    .unwrap(),
2408                    vec![1, 2],
2409                )
2410                .unwrap();
2411                let result = call(Value::ComplexTensor(complex_integer.clone()), Vec::new())
2412                    .expect("wgpu complex-integer upload");
2413                let Value::GpuTensor(handle) = result else {
2414                    panic!("expected complex integer gpu tensor")
2415                };
2416                assert_eq!(
2417                    runmat_accelerate_api::handle_integer_type(&handle),
2418                    Some(runmat_accelerate_api::IntegerElementType::U64)
2419                );
2420                assert_eq!(
2421                    gather_complex(Value::GpuTensor(handle.clone())),
2422                    complex_integer
2423                );
2424                provider.free(&handle).ok();
2425            }
2426            Err(err) => {
2427                tracing::warn!("Skipping gpu_array_wgpu_complex_roundtrip: {err}");
2428            }
2429        }
2430        runmat_accelerate::simple_provider::register_inprocess_provider();
2431    }
2432
2433    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2434    #[test]
2435    fn gpu_array_roundtrips_native_integer_inputs_and_class_requests() {
2436        test_support::with_test_provider(|_| {
2437            for (storage, element_type, class_name) in [
2438                (
2439                    IntegerStorage::I8(vec![i8::MIN, i8::MAX]),
2440                    runmat_accelerate_api::IntegerElementType::I8,
2441                    "int8",
2442                ),
2443                (
2444                    IntegerStorage::I16(vec![i16::MIN, i16::MAX]),
2445                    runmat_accelerate_api::IntegerElementType::I16,
2446                    "int16",
2447                ),
2448                (
2449                    IntegerStorage::I32(vec![i32::MIN, i32::MAX]),
2450                    runmat_accelerate_api::IntegerElementType::I32,
2451                    "int32",
2452                ),
2453                (
2454                    IntegerStorage::I64(vec![i64::MIN, i64::MAX]),
2455                    runmat_accelerate_api::IntegerElementType::I64,
2456                    "int64",
2457                ),
2458                (
2459                    IntegerStorage::U8(vec![0, u8::MAX]),
2460                    runmat_accelerate_api::IntegerElementType::U8,
2461                    "uint8",
2462                ),
2463                (
2464                    IntegerStorage::U16(vec![0, u16::MAX]),
2465                    runmat_accelerate_api::IntegerElementType::U16,
2466                    "uint16",
2467                ),
2468                (
2469                    IntegerStorage::U32(vec![0, u32::MAX]),
2470                    runmat_accelerate_api::IntegerElementType::U32,
2471                    "uint32",
2472                ),
2473                (
2474                    IntegerStorage::U64(vec![1_u64 << 63, u64::MAX]),
2475                    runmat_accelerate_api::IntegerElementType::U64,
2476                    "uint64",
2477                ),
2478            ] {
2479                let expected = storage.clone();
2480                let value = Value::Tensor(Tensor::new_integer(storage, vec![1, 2]).unwrap());
2481                let handle = match call(value, Vec::new()).expect("integer gpuArray upload") {
2482                    Value::GpuTensor(handle) => handle,
2483                    other => panic!("expected gpu tensor, got {other:?}"),
2484                };
2485                assert_eq!(
2486                    runmat_accelerate_api::handle_integer_type(&handle),
2487                    Some(element_type)
2488                );
2489                assert_eq!(
2490                    runmat_accelerate_api::handle_class_name(&handle).as_deref(),
2491                    Some(class_name)
2492                );
2493                let gathered = test_support::gather(Value::GpuTensor(handle)).expect("gather");
2494                assert_eq!(gathered.integer_storage(), Some(&expected));
2495            }
2496
2497            let handle = match call(Value::Num(1.0), vec![Value::from("uint64")])
2498                .expect("uint64 gpuArray conversion")
2499            {
2500                Value::GpuTensor(handle) => handle,
2501                other => panic!("expected gpu tensor, got {other:?}"),
2502            };
2503            let gathered = test_support::gather(Value::GpuTensor(handle)).expect("gather");
2504            assert_eq!(
2505                gathered.integer_storage(),
2506                Some(&IntegerStorage::U64(vec![1]))
2507            );
2508        });
2509    }
2510
2511    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2512    #[test]
2513    fn gpu_array_like_integer_tensor_prototype_preserves_native_class() {
2514        test_support::with_test_provider(|_| {
2515            let prototype = Value::Tensor(
2516                Tensor::new_integer(IntegerStorage::U64(vec![u64::MAX]), vec![1, 1])
2517                    .expect("prototype"),
2518            );
2519            let handle = match call(Value::Num(7.0), vec![Value::from("like"), prototype])
2520                .expect("gpuArray like integer prototype")
2521            {
2522                Value::GpuTensor(handle) => handle,
2523                other => panic!("expected gpu tensor, got {other:?}"),
2524            };
2525
2526            assert_eq!(
2527                runmat_accelerate_api::handle_integer_type(&handle),
2528                Some(runmat_accelerate_api::IntegerElementType::U64)
2529            );
2530            assert_eq!(
2531                runmat_accelerate_api::handle_class_name(&handle).as_deref(),
2532                Some("uint64")
2533            );
2534            let gathered = test_support::gather(Value::GpuTensor(handle)).expect("gather");
2535            assert_eq!(
2536                gathered.integer_storage(),
2537                Some(&IntegerStorage::U64(vec![7]))
2538            );
2539        });
2540    }
2541
2542    #[test]
2543    fn gpuarray_type_for_logical_is_logical() {
2544        assert_eq!(
2545            gpuarray_type(&[Type::logical()], &ResolveContext::new(Vec::new())),
2546            Type::logical()
2547        );
2548    }
2549}