Skip to main content

runmat_runtime/builtins/acceleration/gpu/
arrayfun.rs

1//! MATLAB-compatible `arrayfun` builtin with GPU-aware semantics.
2//!
3//! This implementation applies a scalar MATLAB function to every element of one or
4//! more array inputs. Supported builtin callbacks can dispatch directly for
5//! `gpuArray` inputs; other callbacks gather authoritative typed storage and
6//! re-upload uniform numeric or logical output.
7
8use crate::builtins::acceleration::gpu::type_resolvers::arrayfun_type;
9use crate::builtins::common::spec::{
10    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
11    ProviderHook, ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
12};
13use crate::builtins::common::{broadcast, gpu_helpers, tensor};
14use crate::{
15    build_runtime_error, gather_if_needed_async, make_cell_with_shape, user_functions,
16    BuiltinResult, RuntimeError,
17};
18use runmat_accelerate_api::{set_handle_logical, GpuTensorHandle};
19use runmat_builtins::{
20    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinExtensionDescriptor,
21    BuiltinExtensionMode, BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor,
22    BuiltinIntegerComputationDomain, BuiltinIntegerInputAvailability,
23    BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule, BuiltinIntegerOverflowRule,
24    BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule, BuiltinOutputMode,
25    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
26};
27use runmat_macros::runtime_builtin;
28use runmat_value::{
29    CharArray, Closure, ComplexTensor, IntValue, IntegerComplexStorage, IntegerStorage,
30    LogicalArray, NumericScalar, StringArray, Tensor, Value,
31};
32
33#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::acceleration::gpu::arrayfun")]
34pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
35    name: "arrayfun",
36    op_kind: GpuOpKind::Elementwise,
37    supported_precisions: &[ScalarType::F32, ScalarType::F64],
38    broadcast: BroadcastSemantics::Matlab,
39    provider_hooks: &[
40        ProviderHook::Unary { name: "unary_sin" },
41        ProviderHook::Unary { name: "unary_cos" },
42        ProviderHook::Unary { name: "unary_abs" },
43        ProviderHook::Unary { name: "unary_exp" },
44        ProviderHook::Unary { name: "unary_log" },
45        ProviderHook::Unary { name: "unary_sqrt" },
46        ProviderHook::Binary {
47            name: "elem_add",
48            commutative: true,
49        },
50        ProviderHook::Binary {
51            name: "elem_sub",
52            commutative: false,
53        },
54        ProviderHook::Binary {
55            name: "elem_mul",
56            commutative: true,
57        },
58        ProviderHook::Binary {
59            name: "elem_div",
60            commutative: false,
61        },
62    ],
63    constant_strategy: ConstantStrategy::InlineLiteral,
64    residency: ResidencyPolicy::NewHandle,
65    nan_mode: ReductionNaN::Include,
66    two_pass_threshold: None,
67    workgroup_size: None,
68    accepts_nan_mode: false,
69    notes: "Providers that implement the listed kernels can run supported callbacks entirely on the GPU; unsupported callbacks fall back to the host path with re-upload.",
70};
71
72#[runmat_macros::register_fusion_spec(
73    builtin_path = "crate::builtins::acceleration::gpu::arrayfun"
74)]
75pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
76    name: "arrayfun",
77    shape: ShapeRequirements::Any,
78    constant_strategy: ConstantStrategy::InlineLiteral,
79    elementwise: None,
80    reduction: None,
81    emits_nan: false,
82    notes: "Acts as a fusion barrier because the callback can run arbitrary MATLAB code.",
83};
84
85const BUILTIN_NAME: &str = "arrayfun";
86
87pub(crate) const ARRAYFUN_TEXT_CALLABLE_EXTENSION: BuiltinExtensionDescriptor =
88    BuiltinExtensionDescriptor {
89        id: "arrayfun-text-callable",
90        mode: BuiltinExtensionMode::RunMatOnly,
91        description:
92            "arrayfun with a character-vector or string-scalar callable is a RunMat extension",
93        error_identifier: Some("RunMat:compatibility:ArrayfunTextCallableExtension"),
94    };
95
96pub(crate) const ARRAYFUN_HOST_SCALAR_EXPANSION_EXTENSION: BuiltinExtensionDescriptor =
97    BuiltinExtensionDescriptor {
98        id: "arrayfun-host-scalar-expansion",
99        mode: BuiltinExtensionMode::RunMatOnly,
100        description:
101            "host arrayfun with scalar expansion across differently sized inputs is a RunMat extension",
102        error_identifier: Some("RunMat:compatibility:ArrayfunHostScalarExpansionExtension"),
103    };
104
105pub(crate) const ARRAYFUN_GPU_OPTIONS_EXTENSION: BuiltinExtensionDescriptor =
106    BuiltinExtensionDescriptor {
107        id: "arrayfun-gpu-options",
108        mode: BuiltinExtensionMode::RunMatOnly,
109        description:
110            "gpuArray arrayfun with UniformOutput or ErrorHandler options is a RunMat extension",
111        error_identifier: Some("RunMat:compatibility:ArrayfunGpuOptionsExtension"),
112    };
113
114pub const ARRAYFUN_EXTENSIONS: [BuiltinExtensionDescriptor; 3] = [
115    ARRAYFUN_TEXT_CALLABLE_EXTENSION,
116    ARRAYFUN_HOST_SCALAR_EXPANSION_EXTENSION,
117    ARRAYFUN_GPU_OPTIONS_EXTENSION,
118];
119
120const ARRAYFUN_INTEGER_ARRAY_INPUTS: [BuiltinIntegerInputCapability; 2] = [
121    BuiltinIntegerInputCapability {
122        name: "A1",
123        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
124        availability: BuiltinIntegerInputAvailability::Documented,
125        scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
126        notes: "Each element is passed to func in the exact integer class stored by A1.",
127    },
128    BuiltinIntegerInputCapability {
129        name: "An",
130        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
131        availability: BuiltinIntegerInputAvailability::Documented,
132        scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
133        notes: "Additional arrays may independently use any real integer class; callback overload semantics decide whether a class combination is valid.",
134    },
135];
136
137const ARRAYFUN_INTEGER_CALLBACK_RESULT: [BuiltinIntegerInputCapability; 1] =
138    [BuiltinIntegerInputCapability {
139        name: "func or ErrorHandler scalar result",
140        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
141        availability: BuiltinIntegerInputAvailability::Documented,
142        scalar_double: BuiltinIntegerScalarDoubleRule::Rejected,
143        notes: "Uniform output requires the same scalar class on every invocation and concatenates exact same-class integer results.",
144    }];
145
146const ARRAYFUN_REJECTED_INTEGER_UNIFORM_OUTPUT: [BuiltinIntegerInputCapability; 1] =
147    [BuiltinIntegerInputCapability {
148        name: "UniformOutput",
149        classes: &[],
150        availability: BuiltinIntegerInputAvailability::Rejected,
151        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
152        notes: "The public control is logical true or false; typed-integer controls are rejected.",
153    }];
154
155pub const ARRAYFUN_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 3] = [
156    BuiltinIntegerCapabilityDescriptor {
157        form: "B = arrayfun(func, integer_A1, integer_An...)",
158        inputs: &ARRAYFUN_INTEGER_ARRAY_INPUTS,
159        computation_domain: BuiltinIntegerComputationDomain::FunctionSpecific,
160        output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
161        overflow: BuiltinIntegerOverflowRule::FunctionSpecific,
162        backend: BuiltinIntegerBackendRule::HostAndGpu,
163        overload: BuiltinIntegerOverloadKind::Multiple,
164        notes: "arrayfun itself performs structural element extraction without conversion. The callback defines arithmetic, overflow, and result class. Host inputs must have equal size; the documented gpuArray overload permits compatible sizes.",
165    },
166    BuiltinIntegerCapabilityDescriptor {
167        form: "integer_B = arrayfun(func_returning_integer, A1, An...)",
168        inputs: &ARRAYFUN_INTEGER_CALLBACK_RESULT,
169        computation_domain: BuiltinIntegerComputationDomain::Structural,
170        output_class: BuiltinIntegerOutputClassRule::PreserveInput,
171        overflow: BuiltinIntegerOverflowRule::FunctionSpecific,
172        backend: BuiltinIntegerBackendRule::HostAndGpu,
173        overload: BuiltinIntegerOverloadKind::ElementwiseShapePreserving,
174        notes: "Uniform same-class scalar integer callback results are collected in authoritative native storage. With gpuArray input, supported results remain resident after direct execution or exact gather fallback and typed re-upload.",
175    },
176    BuiltinIntegerCapabilityDescriptor {
177        form: "B = arrayfun(func, A1, An..., \"UniformOutput\", typed_integer)",
178        inputs: &ARRAYFUN_REJECTED_INTEGER_UNIFORM_OUTPUT,
179        computation_domain: BuiltinIntegerComputationDomain::Structural,
180        output_class: BuiltinIntegerOutputClassRule::NotApplicable,
181        overflow: BuiltinIntegerOverflowRule::NotApplicable,
182        backend: BuiltinIntegerBackendRule::HostOnly,
183        overload: BuiltinIntegerOverloadKind::StructuralParameter,
184        notes: "All eight typed-integer scalar and tensor control classes reject rather than being coerced to logical.",
185    },
186];
187
188const ARRAYFUN_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
189    name: "B",
190    ty: BuiltinParamType::Any,
191    arity: BuiltinParamArity::Required,
192    default: None,
193    description: "Element-wise callback result (uniform array or cell array).",
194}];
195
196const ARRAYFUN_INPUTS_BASE: [BuiltinParamDescriptor; 3] = [
197    BuiltinParamDescriptor {
198        name: "func",
199        ty: BuiltinParamType::Any,
200        arity: BuiltinParamArity::Required,
201        default: None,
202        description: "Function handle or callable name.",
203    },
204    BuiltinParamDescriptor {
205        name: "A1",
206        ty: BuiltinParamType::Any,
207        arity: BuiltinParamArity::Required,
208        default: None,
209        description: "First input array.",
210    },
211    BuiltinParamDescriptor {
212        name: "An",
213        ty: BuiltinParamType::Any,
214        arity: BuiltinParamArity::Variadic,
215        default: None,
216        description: "Additional input arrays.",
217    },
218];
219
220const ARRAYFUN_INPUTS_UNIFORM: [BuiltinParamDescriptor; 5] = [
221    BuiltinParamDescriptor {
222        name: "func",
223        ty: BuiltinParamType::Any,
224        arity: BuiltinParamArity::Required,
225        default: None,
226        description: "Function handle or callable name.",
227    },
228    BuiltinParamDescriptor {
229        name: "A1",
230        ty: BuiltinParamType::Any,
231        arity: BuiltinParamArity::Required,
232        default: None,
233        description: "First input array.",
234    },
235    BuiltinParamDescriptor {
236        name: "An",
237        ty: BuiltinParamType::Any,
238        arity: BuiltinParamArity::Variadic,
239        default: None,
240        description: "Additional input arrays.",
241    },
242    BuiltinParamDescriptor {
243        name: "UniformOutput",
244        ty: BuiltinParamType::PropertyName,
245        arity: BuiltinParamArity::Required,
246        default: Some("\"UniformOutput\""),
247        description: "Name-value key that toggles uniform output collection.",
248    },
249    BuiltinParamDescriptor {
250        name: "tf",
251        ty: BuiltinParamType::Any,
252        arity: BuiltinParamArity::Required,
253        default: Some("true"),
254        description: "Logical true/false value for UniformOutput.",
255    },
256];
257
258const ARRAYFUN_INPUTS_HANDLER: [BuiltinParamDescriptor; 5] = [
259    BuiltinParamDescriptor {
260        name: "func",
261        ty: BuiltinParamType::Any,
262        arity: BuiltinParamArity::Required,
263        default: None,
264        description: "Function handle or callable name.",
265    },
266    BuiltinParamDescriptor {
267        name: "A1",
268        ty: BuiltinParamType::Any,
269        arity: BuiltinParamArity::Required,
270        default: None,
271        description: "First input array.",
272    },
273    BuiltinParamDescriptor {
274        name: "An",
275        ty: BuiltinParamType::Any,
276        arity: BuiltinParamArity::Variadic,
277        default: None,
278        description: "Additional input arrays.",
279    },
280    BuiltinParamDescriptor {
281        name: "ErrorHandler",
282        ty: BuiltinParamType::PropertyName,
283        arity: BuiltinParamArity::Required,
284        default: Some("\"ErrorHandler\""),
285        description: "Name-value key that provides fallback callback on per-element failures.",
286    },
287    BuiltinParamDescriptor {
288        name: "handler",
289        ty: BuiltinParamType::Any,
290        arity: BuiltinParamArity::Required,
291        default: None,
292        description: "Callback invoked with error struct and original scalar arguments.",
293    },
294];
295
296const ARRAYFUN_INPUTS_OPTIONS: [BuiltinParamDescriptor; 4] = [
297    BuiltinParamDescriptor {
298        name: "func",
299        ty: BuiltinParamType::Any,
300        arity: BuiltinParamArity::Required,
301        default: None,
302        description: "Function handle or callable name.",
303    },
304    BuiltinParamDescriptor {
305        name: "A1",
306        ty: BuiltinParamType::Any,
307        arity: BuiltinParamArity::Required,
308        default: None,
309        description: "First input array.",
310    },
311    BuiltinParamDescriptor {
312        name: "An",
313        ty: BuiltinParamType::Any,
314        arity: BuiltinParamArity::Variadic,
315        default: None,
316        description: "Additional input arrays.",
317    },
318    BuiltinParamDescriptor {
319        name: "nameValue",
320        ty: BuiltinParamType::Any,
321        arity: BuiltinParamArity::Variadic,
322        default: None,
323        description: "Name-value option pairs including UniformOutput and ErrorHandler.",
324    },
325];
326
327const ARRAYFUN_SIGNATURES: [BuiltinSignatureDescriptor; 4] = [
328    BuiltinSignatureDescriptor {
329        label: "B = arrayfun(func, A1, An...)",
330        inputs: &ARRAYFUN_INPUTS_BASE,
331        outputs: &ARRAYFUN_OUTPUT,
332    },
333    BuiltinSignatureDescriptor {
334        label: "B = arrayfun(func, A1, An..., \"UniformOutput\", tf)",
335        inputs: &ARRAYFUN_INPUTS_UNIFORM,
336        outputs: &ARRAYFUN_OUTPUT,
337    },
338    BuiltinSignatureDescriptor {
339        label: "B = arrayfun(func, A1, An..., \"ErrorHandler\", handler)",
340        inputs: &ARRAYFUN_INPUTS_HANDLER,
341        outputs: &ARRAYFUN_OUTPUT,
342    },
343    BuiltinSignatureDescriptor {
344        label: "B = arrayfun(func, A1, An..., nameValue...)",
345        inputs: &ARRAYFUN_INPUTS_OPTIONS,
346        outputs: &ARRAYFUN_OUTPUT,
347    },
348];
349
350const ARRAYFUN_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
351    code: "RM.ARRAYFUN.INVALID_INPUT",
352    identifier: Some("RunMat:arrayfun:InvalidInput"),
353    when: "Inputs, callable forms, or option tails violate arrayfun argument requirements.",
354    message: "arrayfun: invalid input arguments",
355};
356
357const ARRAYFUN_ERROR_UNIFORM_OUTPUT_OPTION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
358    code: "RM.ARRAYFUN.UNIFORM_OUTPUT_OPTION",
359    identifier: Some("RunMat:arrayfun:UniformOutputOption"),
360    when: "UniformOutput option value is not interpretable as logical true/false.",
361    message: "arrayfun: UniformOutput must be logical true or false",
362};
363
364const ARRAYFUN_ERROR_CALLBACK_FAILED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
365    code: "RM.ARRAYFUN.CALLBACK_FAILED",
366    identifier: Some("RunMat:arrayfun:CallbackFailed"),
367    when: "Callback invocation fails and no ErrorHandler recovers the element.",
368    message: "arrayfun: callback execution failed",
369};
370
371const ARRAYFUN_ERROR_UNIFORM_OUTPUT_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
372    code: "RM.ARRAYFUN.UNIFORM_OUTPUT_TYPE",
373    identifier: Some("RunMat:arrayfun:UniformOutputType"),
374    when: "UniformOutput=true callback result is not a supported scalar type.",
375    message: "arrayfun: callback must return scalar values for UniformOutput=true",
376};
377
378const ARRAYFUN_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
379    code: "RM.ARRAYFUN.INTERNAL",
380    identifier: Some("RunMat:arrayfun:InternalError"),
381    when: "Internal shape/index/materialization/upload path fails.",
382    message: "arrayfun: internal error",
383};
384
385const ARRAYFUN_ERROR_UNDEFINED_FUNCTION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
386    code: "RM.ARRAYFUN.UNDEFINED_FUNCTION",
387    identifier: Some("RunMat:UndefinedFunction"),
388    when: "External callable identity cannot be resolved in semantic/runtime boundaries.",
389    message: "arrayfun: undefined function",
390};
391
392const ARRAYFUN_ERRORS: [BuiltinErrorDescriptor; 6] = [
393    ARRAYFUN_ERROR_INVALID_INPUT,
394    ARRAYFUN_ERROR_UNIFORM_OUTPUT_OPTION,
395    ARRAYFUN_ERROR_CALLBACK_FAILED,
396    ARRAYFUN_ERROR_UNIFORM_OUTPUT_TYPE,
397    ARRAYFUN_ERROR_INTERNAL,
398    ARRAYFUN_ERROR_UNDEFINED_FUNCTION,
399];
400
401pub const ARRAYFUN_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
402    signatures: &ARRAYFUN_SIGNATURES,
403    output_mode: BuiltinOutputMode::Fixed,
404    completion_policy: BuiltinCompletionPolicy::Public,
405    errors: &ARRAYFUN_ERRORS,
406};
407
408fn arrayfun_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
409    arrayfun_error_with_message(error.message, error)
410}
411
412fn arrayfun_error_with_message(
413    message: impl Into<String>,
414    error: &'static BuiltinErrorDescriptor,
415) -> RuntimeError {
416    let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
417    if let Some(identifier) = error.identifier {
418        builder = builder.with_identifier(identifier);
419    }
420    builder.build()
421}
422
423fn arrayfun_error_with_detail(
424    error: &'static BuiltinErrorDescriptor,
425    detail: impl AsRef<str>,
426) -> RuntimeError {
427    arrayfun_error_with_message(format!("{}: {}", error.message, detail.as_ref()), error)
428}
429
430fn arrayfun_error_with_source(
431    message: impl Into<String>,
432    error: &'static BuiltinErrorDescriptor,
433    source: RuntimeError,
434) -> RuntimeError {
435    let identifier = source.identifier().map(str::to_string);
436    let mut builder = build_runtime_error(message.into())
437        .with_builtin(BUILTIN_NAME)
438        .with_source(source);
439    if let Some(identifier) = identifier.as_deref().or(error.identifier) {
440        builder = builder.with_identifier(identifier);
441    }
442    builder.build()
443}
444
445fn arrayfun_flow(message: impl Into<String>) -> RuntimeError {
446    arrayfun_error_with_message(message, &ARRAYFUN_ERROR_INVALID_INPUT)
447}
448
449fn arrayfun_internal(message: impl Into<String>) -> RuntimeError {
450    arrayfun_error_with_message(message, &ARRAYFUN_ERROR_INTERNAL)
451}
452
453fn arrayfun_flow_with_source(message: impl Into<String>, source: RuntimeError) -> RuntimeError {
454    arrayfun_error_with_source(message, &ARRAYFUN_ERROR_CALLBACK_FAILED, source)
455}
456
457fn format_handler_error(err: &RuntimeError) -> String {
458    if let Some(identifier) = err.identifier() {
459        if err.message().is_empty() {
460            return identifier.to_string();
461        }
462        if err.message().starts_with(identifier) {
463            return err.message().to_string();
464        }
465        return format!("{identifier}: {}", err.message());
466    }
467    err.message().to_string()
468}
469
470#[runtime_builtin(
471    name = "arrayfun",
472    category = "acceleration/gpu",
473    summary = "Apply a function element-wise across array inputs.",
474    keywords = "arrayfun,gpu,array,map,functional",
475    accel = "host",
476    type_resolver(arrayfun_type),
477    descriptor(crate::builtins::acceleration::gpu::arrayfun::ARRAYFUN_DESCRIPTOR),
478    extensions(crate::builtins::acceleration::gpu::arrayfun::ARRAYFUN_EXTENSIONS),
479    integer_capabilities(
480        crate::builtins::acceleration::gpu::arrayfun::ARRAYFUN_INTEGER_CAPABILITIES
481    ),
482    builtin_path = "crate::builtins::acceleration::gpu::arrayfun"
483)]
484async fn arrayfun_builtin(func: Value, mut rest: Vec<Value>) -> crate::BuiltinResult<Value> {
485    if matches!(
486        &func,
487        Value::String(_) | Value::StringArray(_) | Value::CharArray(_)
488    ) {
489        crate::compatibility::ensure_builtin_extension_enabled(
490            &ARRAYFUN_TEXT_CALLABLE_EXTENSION,
491            BUILTIN_NAME,
492        )?;
493    }
494    let callable = Callable::from_function(func)?;
495
496    let mut uniform_output = true;
497    let mut uniform_output_explicit = false;
498    let mut error_handler: Option<Callable> = None;
499
500    while rest.len() >= 2 {
501        let key_candidate = rest[rest.len() - 2].clone();
502        let Some(name) = extract_string(&key_candidate) else {
503            break;
504        };
505        let value = rest.pop().expect("value present");
506        rest.pop();
507        match name.trim().to_ascii_lowercase().as_str() {
508            "uniformoutput" => {
509                uniform_output = parse_uniform_output(value)?;
510                uniform_output_explicit = true;
511            }
512            "errorhandler" => {
513                if matches!(
514                    &value,
515                    Value::String(_) | Value::StringArray(_) | Value::CharArray(_)
516                ) {
517                    crate::compatibility::ensure_builtin_extension_enabled(
518                        &ARRAYFUN_TEXT_CALLABLE_EXTENSION,
519                        BUILTIN_NAME,
520                    )?;
521                }
522                error_handler = Some(Callable::from_function(value)?);
523            }
524            other => {
525                return Err(arrayfun_flow(format!(
526                    "arrayfun: unknown name-value argument '{other}'"
527                )))
528            }
529        }
530    }
531
532    if rest.is_empty() {
533        return Err(arrayfun_flow("arrayfun: expected at least one input array"));
534    }
535
536    let inputs_snapshot = rest.clone();
537    let has_gpu_input = inputs_snapshot
538        .iter()
539        .any(|value| matches!(value, Value::GpuTensor(_)));
540    let gpu_device_id = inputs_snapshot.iter().find_map(|v| {
541        if let Value::GpuTensor(h) = v {
542            Some(h.device_id)
543        } else {
544            None
545        }
546    });
547    if has_gpu_input && (uniform_output_explicit || error_handler.is_some()) {
548        crate::compatibility::ensure_builtin_extension_enabled(
549            &ARRAYFUN_GPU_OPTIONS_EXTENSION,
550            BUILTIN_NAME,
551        )?;
552    }
553
554    if uniform_output {
555        if let Some(gpu_result) =
556            try_gpu_fast_path(&callable, &inputs_snapshot, error_handler.as_ref()).await?
557        {
558            return Ok(gpu_result);
559        }
560    }
561
562    let mut inputs: Vec<ArrayInput> = Vec::with_capacity(rest.len());
563
564    for raw in rest {
565        if matches!(raw, Value::Cell(_)) {
566            return Err(arrayfun_flow(
567                "arrayfun: cell inputs are not supported (use cellfun instead)",
568            ));
569        }
570        if matches!(raw, Value::Struct(_)) {
571            return Err(arrayfun_flow("arrayfun: struct inputs are not supported"));
572        }
573
574        let host_value = gather_if_needed_async(&raw).await?;
575        let data = ArrayData::from_value(host_value)?;
576        let shape = data.shape_vec();
577        let strides = broadcast::compute_strides(&shape);
578        inputs.push(ArrayInput {
579            data,
580            shape,
581            strides,
582        });
583    }
584
585    let first_shape = inputs
586        .first()
587        .map(|input| input.shape.clone())
588        .unwrap_or_default();
589    let base_shape = if has_gpu_input {
590        inputs
591            .iter()
592            .skip(1)
593            .try_fold(first_shape, |shape, input| {
594                broadcast::broadcast_shapes(BUILTIN_NAME, &shape, &input.shape)
595                    .map_err(arrayfun_flow)
596            })?
597    } else {
598        let non_scalar_shapes: Vec<&[usize]> = inputs
599            .iter()
600            .filter(|input| input.len() != 1)
601            .map(|input| input.shape.as_slice())
602            .collect();
603        let target_shape = non_scalar_shapes
604            .first()
605            .copied()
606            .unwrap_or(first_shape.as_slice());
607        if non_scalar_shapes
608            .iter()
609            .skip(1)
610            .any(|shape| *shape != target_shape)
611        {
612            return Err(arrayfun_flow(
613                "arrayfun: host input does not match the size of the first array",
614            ));
615        }
616        if inputs.iter().any(|input| input.shape != target_shape) {
617            crate::compatibility::ensure_builtin_extension_enabled(
618                &ARRAYFUN_HOST_SCALAR_EXPANSION_EXTENSION,
619                BUILTIN_NAME,
620            )?;
621        }
622        target_shape.to_vec()
623    };
624
625    let total_len = base_shape.iter().product();
626
627    if total_len == 0 {
628        if uniform_output {
629            return Ok(empty_uniform(&base_shape));
630        } else {
631            return make_cell_with_shape(Vec::new(), base_shape)
632                .map_err(|e| arrayfun_flow(format!("arrayfun: {e}")));
633        }
634    }
635
636    let mut collector = if uniform_output {
637        Some(UniformCollector::Pending)
638    } else {
639        None
640    };
641
642    let mut cell_outputs: Vec<Value> = Vec::new();
643    let mut args: Vec<Value> = Vec::with_capacity(inputs.len());
644
645    for idx in 0..total_len {
646        args.clear();
647        for input in &inputs {
648            args.push(input.value_at(idx, &base_shape)?);
649        }
650
651        let result = match callable.call(&args).await {
652            Ok(value) => value,
653            Err(err) => {
654                let handler = match error_handler.as_ref() {
655                    Some(handler) => handler,
656                    None => {
657                        return Err(arrayfun_flow_with_source(
658                            format!("arrayfun: {}", err.message()),
659                            err,
660                        ))
661                    }
662                };
663                let err_message = format_handler_error(&err);
664                let err_value = make_error_struct(&err_message, idx);
665                let mut handler_args = Vec::with_capacity(1 + args.len());
666                handler_args.push(err_value);
667                handler_args.extend(args.clone());
668                handler.call(&handler_args).await?
669            }
670        };
671
672        let host_result = gather_if_needed_async(&result).await?;
673
674        if let Some(collector) = collector.as_mut() {
675            collector.push(&host_result)?;
676        } else {
677            cell_outputs.push(host_result);
678        }
679    }
680
681    if let Some(collector) = collector {
682        let uniform = collector.finish(&base_shape)?;
683        maybe_upload_uniform(uniform, has_gpu_input, gpu_device_id)
684    } else {
685        make_cell_with_shape(cell_outputs, base_shape)
686            .map_err(|e| arrayfun_flow(format!("arrayfun: {e}")))
687    }
688}
689
690fn maybe_upload_uniform(
691    value: Value,
692    has_gpu_input: bool,
693    gpu_device_id: Option<u32>,
694) -> BuiltinResult<Value> {
695    if !has_gpu_input {
696        return Ok(value);
697    }
698    let _ = gpu_device_id; // may be used only in cfg(test)
699    let provider = match runmat_accelerate_api::provider() {
700        Some(p) => p,
701        None => return Ok(value),
702    };
703
704    match value {
705        Value::Tensor(tensor) => {
706            let handle = gpu_helpers::upload_tensor(provider, &tensor)
707                .map_err(|e| arrayfun_flow(format!("arrayfun: {e}")))?;
708            Ok(Value::GpuTensor(handle))
709        }
710        Value::LogicalArray(logical) => {
711            let data: Vec<f64> = logical
712                .data
713                .iter()
714                .map(|&bit| if bit != 0 { 1.0 } else { 0.0 })
715                .collect();
716            let tensor = Tensor::new(data, logical.shape.clone())
717                .map_err(|e| arrayfun_flow(format!("arrayfun: {e}")))?;
718            let handle = gpu_helpers::upload_tensor(provider, &tensor)
719                .map_err(|e| arrayfun_flow(format!("arrayfun: {e}")))?;
720            set_handle_logical(&handle, true);
721            Ok(Value::GpuTensor(handle))
722        }
723        other => Ok(other),
724    }
725}
726
727fn empty_uniform(shape: &[usize]) -> Value {
728    if shape.is_empty() {
729        return Value::Tensor(Tensor::zeros(vec![0, 0]));
730    }
731    let total: usize = shape.iter().product();
732    let tensor = Tensor::new(vec![0.0; total], shape.to_vec())
733        .unwrap_or_else(|_| Tensor::zeros(shape.to_vec()));
734    Value::Tensor(tensor)
735}
736
737fn parse_uniform_output(value: Value) -> BuiltinResult<bool> {
738    match value {
739        Value::Bool(b) => Ok(b),
740        Value::LogicalArray(logical) if logical.len() == 1 => Ok(logical.data[0] != 0),
741        Value::Num(0.0) => Ok(false),
742        Value::Num(1.0) => Ok(true),
743        Value::Tensor(tensor)
744            if tensor.len() == 1 && tensor.numeric_dtype() == runmat_value::NumericDType::F64 =>
745        {
746            match tensor.numeric_value_at(0) {
747                Some(NumericScalar::F64(0.0)) => Ok(false),
748                Some(NumericScalar::F64(1.0)) => Ok(true),
749                _ => Err(arrayfun_error(&ARRAYFUN_ERROR_UNIFORM_OUTPUT_OPTION)),
750            }
751        }
752        other => Err(arrayfun_error_with_detail(
753            &ARRAYFUN_ERROR_UNIFORM_OUTPUT_OPTION,
754            format!("got {other:?}"),
755        )),
756    }
757}
758
759fn extract_string(value: &Value) -> Option<String> {
760    match value {
761        Value::String(s) => Some(s.clone()),
762        Value::CharArray(ca) if ca.rows == 1 => Some(ca.data.iter().collect()),
763        Value::StringArray(sa) if sa.data.len() == 1 => Some(sa.data[0].clone()),
764        _ => None,
765    }
766}
767
768struct ArrayInput {
769    data: ArrayData,
770    shape: Vec<usize>,
771    strides: Vec<usize>,
772}
773
774impl ArrayInput {
775    fn len(&self) -> usize {
776        self.data.len()
777    }
778
779    fn value_at(&self, idx: usize, output_shape: &[usize]) -> BuiltinResult<Value> {
780        let source_index =
781            broadcast::broadcast_index(idx, output_shape, &self.shape, &self.strides);
782        self.data.value_at(source_index)
783    }
784}
785
786enum ArrayData {
787    Tensor(Tensor),
788    Logical(LogicalArray),
789    Complex(ComplexTensor),
790    Char(CharArray),
791    String(StringArray),
792    Scalar(Value),
793}
794
795impl ArrayData {
796    fn from_value(value: Value) -> BuiltinResult<Self> {
797        match value {
798            Value::Tensor(t) => Ok(ArrayData::Tensor(t)),
799            Value::LogicalArray(l) => Ok(ArrayData::Logical(l)),
800            Value::ComplexTensor(c) => Ok(ArrayData::Complex(c)),
801            Value::CharArray(ca) => Ok(ArrayData::Char(ca)),
802            Value::StringArray(sa) => Ok(ArrayData::String(sa)),
803            Value::Num(_)
804            | Value::Bool(_)
805            | Value::Int(_)
806            | Value::Complex(_, _)
807            | Value::String(_) => {
808                Ok(ArrayData::Scalar(value))
809            }
810            other => Err(arrayfun_flow(format!(
811                "arrayfun: unsupported input type {other:?} (expected numeric, logical, complex, char, or string arrays)"
812            ))),
813        }
814    }
815
816    fn len(&self) -> usize {
817        match self {
818            ArrayData::Tensor(t) => t.len(),
819            ArrayData::Logical(l) => l.data.len(),
820            ArrayData::Complex(c) => c.len(),
821            ArrayData::Char(ca) => ca.rows * ca.cols,
822            ArrayData::String(sa) => sa.data.len(),
823            ArrayData::Scalar(_) => 1,
824        }
825    }
826
827    fn shape_vec(&self) -> Vec<usize> {
828        match self {
829            ArrayData::Tensor(t) => {
830                if t.shape.is_empty() {
831                    vec![1, 1]
832                } else {
833                    t.shape.clone()
834                }
835            }
836            ArrayData::Logical(l) => {
837                if l.shape.is_empty() {
838                    vec![1, 1]
839                } else {
840                    l.shape.clone()
841                }
842            }
843            ArrayData::Complex(c) => {
844                if c.shape.is_empty() {
845                    vec![1, 1]
846                } else {
847                    c.shape.clone()
848                }
849            }
850            ArrayData::Char(ca) => vec![ca.rows, ca.cols],
851            ArrayData::String(sa) => {
852                if sa.shape.is_empty() {
853                    vec![1, 1]
854                } else {
855                    sa.shape.clone()
856                }
857            }
858            ArrayData::Scalar(_) => vec![1, 1],
859        }
860    }
861
862    fn value_at(&self, idx: usize) -> BuiltinResult<Value> {
863        match self {
864            ArrayData::Tensor(t) => match t
865                .numeric_value_at(idx)
866                .ok_or_else(|| arrayfun_flow("arrayfun: index out of bounds"))?
867            {
868                NumericScalar::F64(value) => Ok(Value::Num(value)),
869                NumericScalar::F32(value) => Ok(Value::Tensor(
870                    Tensor::from_f32(vec![value], vec![1, 1])
871                        .map_err(|error| arrayfun_internal(format!("arrayfun: {error}")))?,
872                )),
873                value => Ok(Value::Int(value.into_int_value().ok_or_else(|| {
874                    arrayfun_internal("arrayfun: integer scalar classification failed")
875                })?)),
876            },
877            ArrayData::Logical(l) => Ok(Value::Bool(
878                *l.data
879                    .get(idx)
880                    .ok_or_else(|| arrayfun_flow("arrayfun: index out of bounds"))?
881                    != 0,
882            )),
883            ArrayData::Complex(c) => {
884                let (real, imag) = c
885                    .numeric_value_at(idx)
886                    .ok_or_else(|| arrayfun_flow("arrayfun: index out of bounds"))?;
887                match (real, imag) {
888                    (NumericScalar::F64(real), NumericScalar::F64(imag)) => {
889                        Ok(Value::Complex(real, imag))
890                    }
891                    (NumericScalar::F32(real), NumericScalar::F32(imag)) => {
892                        Ok(Value::ComplexTensor(
893                            ComplexTensor::from_f32(vec![(real, imag)], vec![1, 1])
894                                .map_err(|error| arrayfun_internal(format!("arrayfun: {error}")))?,
895                        ))
896                    }
897                    (real, imag) => {
898                        let real = real.into_int_value().ok_or_else(|| {
899                            arrayfun_internal(
900                                "arrayfun: complex scalar components have inconsistent classes",
901                            )
902                        })?;
903                        let imag = imag.into_int_value().ok_or_else(|| {
904                            arrayfun_internal(
905                                "arrayfun: complex scalar components have inconsistent classes",
906                            )
907                        })?;
908                        let storage = IntegerComplexStorage::new(
909                            IntegerStorage::from_scalar(real),
910                            IntegerStorage::from_scalar(imag),
911                        )
912                        .map_err(|error| arrayfun_internal(format!("arrayfun: {error}")))?;
913                        Ok(Value::ComplexTensor(
914                            ComplexTensor::new_integer(storage, vec![1, 1])
915                                .map_err(|error| arrayfun_internal(format!("arrayfun: {error}")))?,
916                        ))
917                    }
918                }
919            }
920            ArrayData::Char(ca) => {
921                if ca.rows == 0 || ca.cols == 0 {
922                    return Ok(Value::CharArray(
923                        CharArray::new(Vec::new(), 0, 0)
924                            .map_err(|e| arrayfun_flow(format!("arrayfun: {e}")))?,
925                    ));
926                }
927                let rows = ca.rows;
928                let cols = ca.cols;
929                let row = idx % rows;
930                let col = idx / rows;
931                let data_idx = row * cols + col;
932                let ch = *ca
933                    .data
934                    .get(data_idx)
935                    .ok_or_else(|| arrayfun_flow("arrayfun: index out of bounds"))?;
936                let char_array = CharArray::new(vec![ch], 1, 1)
937                    .map_err(|e| arrayfun_flow(format!("arrayfun: {e}")))?;
938                Ok(Value::CharArray(char_array))
939            }
940            ArrayData::String(sa) => {
941                Ok(Value::String(sa.data.get(idx).cloned().ok_or_else(
942                    || arrayfun_flow("arrayfun: index out of bounds"),
943                )?))
944            }
945            ArrayData::Scalar(v) => Ok(v.clone()),
946        }
947    }
948}
949
950#[derive(Clone)]
951enum Callable {
952    Builtin { name: String },
953    ExternalName { name: String },
954    Closure(Closure),
955}
956
957impl Callable {
958    fn resolved_semantic_handle(name: &str) -> Option<Self> {
959        let function = user_functions::resolve_semantic_function_by_name(name)?;
960        Some(Callable::Closure(Closure {
961            function_name: name.to_string(),
962            bound_function: Some(function),
963            captures: Vec::new(),
964        }))
965    }
966
967    fn from_function(value: Value) -> BuiltinResult<Self> {
968        match value {
969            Value::String(text) => Self::from_text(&text),
970            Value::CharArray(ca) => {
971                if ca.rows != 1 {
972                    Err(arrayfun_flow(
973                        "arrayfun: function name must be a character vector or string scalar",
974                    ))
975                } else {
976                    let text: String = ca.data.iter().collect();
977                    Self::from_text(&text)
978                }
979            }
980            Value::StringArray(sa) if sa.data.len() == 1 => Self::from_text(&sa.data[0]),
981            Value::FunctionHandle(name) => Self::from_text(&name),
982            Value::ExternalFunctionHandle(name) => {
983                if let Some(callable) = Self::resolved_semantic_handle(&name) {
984                    Ok(callable)
985                } else if crate::is_well_formed_qualified_name(&name) {
986                    Ok(Callable::ExternalName { name })
987                } else {
988                    Ok(Callable::Builtin { name })
989                }
990            }
991            Value::BoundFunctionHandle { name, function } => Ok(Callable::Closure(Closure {
992                function_name: name,
993                bound_function: Some(function),
994                captures: Vec::new(),
995            })),
996            Value::Closure(mut closure) => {
997                if closure.bound_function.is_none() {
998                    if let Some(function) =
999                        user_functions::resolve_semantic_function_by_name(&closure.function_name)
1000                    {
1001                        closure.bound_function = Some(function);
1002                    }
1003                }
1004                Ok(Callable::Closure(closure))
1005            }
1006            Value::Num(_) | Value::Int(_) | Value::Bool(_) => Err(arrayfun_flow(
1007                "arrayfun: expected function handle or builtin name, not a scalar value",
1008            )),
1009            other => Err(arrayfun_flow(format!(
1010                "arrayfun: expected function handle or builtin name, got {other:?}"
1011            ))),
1012        }
1013    }
1014
1015    fn from_text(text: &str) -> BuiltinResult<Self> {
1016        let trimmed = text.trim();
1017        if trimmed.is_empty() {
1018            return Err(arrayfun_flow(
1019                "arrayfun: expected function handle or builtin name, got empty string",
1020            ));
1021        }
1022        if let Some(rest) = trimmed.strip_prefix('@') {
1023            let name = rest.trim();
1024            if name.is_empty() {
1025                Err(arrayfun_flow("arrayfun: empty function handle"))
1026            } else {
1027                if let Some(callable) = Self::resolved_semantic_handle(name) {
1028                    return Ok(callable);
1029                }
1030                if crate::is_well_formed_qualified_name(name) {
1031                    return Ok(Callable::ExternalName {
1032                        name: name.to_string(),
1033                    });
1034                }
1035                Ok(Callable::Builtin {
1036                    name: name.to_string(),
1037                })
1038            }
1039        } else {
1040            let name = trimmed.to_ascii_lowercase();
1041            if let Some(callable) = Self::resolved_semantic_handle(&name) {
1042                return Ok(callable);
1043            }
1044            if crate::is_well_formed_qualified_name(&name) {
1045                return Ok(Callable::ExternalName { name });
1046            }
1047            Ok(Callable::Builtin { name })
1048        }
1049    }
1050
1051    fn builtin_name(&self) -> Option<&str> {
1052        match self {
1053            Callable::Builtin { name } => Some(name.as_str()),
1054            Callable::ExternalName { .. } | Callable::Closure(_) => None,
1055        }
1056    }
1057
1058    async fn call(&self, args: &[Value]) -> crate::BuiltinResult<Value> {
1059        match self {
1060            Callable::Builtin { name } => {
1061                let request = user_functions::CallableRequest::resolved(
1062                    runmat_types::CallableIdentity::DynamicName(runmat_types::SymbolName(
1063                        name.clone(),
1064                    )),
1065                    runmat_types::CallableFallbackPolicy::RuntimeNameResolution,
1066                    args.to_vec(),
1067                    1,
1068                );
1069                if let Some(result) = user_functions::try_call_semantic_descriptor(request).await {
1070                    return result;
1071                }
1072                crate::call_builtin_async(name, args).await
1073            }
1074            Callable::ExternalName { name } => {
1075                let identity = crate::external_callable_identity_for_name(name);
1076                let request = user_functions::CallableRequest::resolved(
1077                    identity.clone(),
1078                    runmat_types::CallableFallbackPolicy::ExternalBoundary,
1079                    args.to_vec(),
1080                    1,
1081                );
1082                if let Some(result) = user_functions::try_call_semantic_descriptor(request).await {
1083                    return result;
1084                }
1085                Err(arrayfun_error_with_message(
1086                    format!("Undefined function for callable identity {identity:?}"),
1087                    &ARRAYFUN_ERROR_UNDEFINED_FUNCTION,
1088                ))
1089            }
1090            Callable::Closure(c) => {
1091                let mut merged = c.captures.clone();
1092                merged.extend_from_slice(args);
1093                if let Some(function) = c.bound_function {
1094                    let request =
1095                        user_functions::CallableRequest::semantic(function, merged.clone(), 1);
1096                    if let Some(result) =
1097                        user_functions::try_call_semantic_descriptor(request).await
1098                    {
1099                        return result;
1100                    }
1101                    return Err(arrayfun_error_with_detail(
1102                        &ARRAYFUN_ERROR_CALLBACK_FAILED,
1103                        format!(
1104                            "semantic closure '{}' ({function}) is unavailable",
1105                            c.function_name
1106                        ),
1107                    ));
1108                }
1109                if let Some(function) =
1110                    user_functions::resolve_semantic_function_by_name(&c.function_name)
1111                {
1112                    let request =
1113                        user_functions::CallableRequest::semantic(function, merged.clone(), 1);
1114                    if let Some(result) =
1115                        user_functions::try_call_semantic_descriptor(request).await
1116                    {
1117                        return result;
1118                    }
1119                }
1120                crate::call_builtin_async(&c.function_name, &merged).await
1121            }
1122        }
1123    }
1124}
1125
1126async fn try_gpu_fast_path(
1127    callable: &Callable,
1128    inputs: &[Value],
1129    error_handler: Option<&Callable>,
1130) -> BuiltinResult<Option<Value>> {
1131    if inputs.is_empty() || error_handler.is_some() {
1132        return Ok(None);
1133    }
1134    if !inputs
1135        .iter()
1136        .all(|value| matches!(value, Value::GpuTensor(_)))
1137    {
1138        return Ok(None);
1139    }
1140
1141    let provider = match runmat_accelerate_api::provider() {
1142        Some(p) => p,
1143        None => return Ok(None),
1144    };
1145
1146    let Some(name_raw) = callable.builtin_name() else {
1147        return Ok(None);
1148    };
1149    let name = name_raw.to_ascii_lowercase();
1150
1151    let mut handles: Vec<GpuTensorHandle> = Vec::with_capacity(inputs.len());
1152    for value in inputs {
1153        if let Value::GpuTensor(handle) = value {
1154            handles.push(handle.clone());
1155        }
1156    }
1157
1158    if handles.len() >= 2 {
1159        let base_shape = handles[0].shape.clone();
1160        if handles
1161            .iter()
1162            .skip(1)
1163            .any(|handle| handle.shape != base_shape)
1164        {
1165            return Ok(None);
1166        }
1167    }
1168
1169    let result = match name.as_str() {
1170        "sin" if handles.len() == 1 => provider.unary_sin(&handles[0]).await,
1171        "cos" if handles.len() == 1 => provider.unary_cos(&handles[0]).await,
1172        "abs" if handles.len() == 1 => provider.unary_abs(&handles[0]).await,
1173        "exp" if handles.len() == 1 => provider.unary_exp(&handles[0]).await,
1174        "log" if handles.len() == 1 => provider.unary_log(&handles[0]).await,
1175        "sqrt" if handles.len() == 1 => provider.unary_sqrt(&handles[0]).await,
1176        "plus" if handles.len() == 2 => provider.elem_add(&handles[0], &handles[1]).await,
1177        "minus" if handles.len() == 2 => provider.elem_sub(&handles[0], &handles[1]).await,
1178        "times" if handles.len() == 2 => provider.elem_mul(&handles[0], &handles[1]).await,
1179        "rdivide" if handles.len() == 2 => provider.elem_div(&handles[0], &handles[1]).await,
1180        "ldivide" if handles.len() == 2 => provider.elem_div(&handles[1], &handles[0]).await,
1181        _ => return Ok(None),
1182    };
1183
1184    match result {
1185        Ok(handle) => Ok(Some(Value::GpuTensor(handle))),
1186        Err(_) => Ok(None),
1187    }
1188}
1189
1190enum UniformCollector {
1191    Pending,
1192    F64(Vec<f64>),
1193    F32(Vec<f32>),
1194    Integer {
1195        prototype: IntegerStorage,
1196        values: Vec<IntValue>,
1197    },
1198    Logical(Vec<u8>),
1199    ComplexF64(Vec<(f64, f64)>),
1200    ComplexF32(Vec<(f32, f32)>),
1201    IntegerComplex {
1202        real_prototype: IntegerStorage,
1203        imag_prototype: IntegerStorage,
1204        real_values: Vec<IntValue>,
1205        imag_values: Vec<IntValue>,
1206    },
1207    Char(Vec<char>),
1208}
1209
1210fn heterogeneous_uniform_output() -> RuntimeError {
1211    arrayfun_error_with_detail(
1212        &ARRAYFUN_ERROR_UNIFORM_OUTPUT_TYPE,
1213        "callback outputs with UniformOutput=true must have the same data type on every invocation",
1214    )
1215}
1216
1217impl UniformCollector {
1218    fn push(&mut self, value: &Value) -> BuiltinResult<()> {
1219        match self {
1220            UniformCollector::Pending => match classify_value(value)? {
1221                ClassifiedValue::Logical(b) => {
1222                    *self = UniformCollector::Logical(vec![b as u8]);
1223                    Ok(())
1224                }
1225                ClassifiedValue::F64(value) => {
1226                    *self = UniformCollector::F64(vec![value]);
1227                    Ok(())
1228                }
1229                ClassifiedValue::F32(value) => {
1230                    *self = UniformCollector::F32(vec![value]);
1231                    Ok(())
1232                }
1233                ClassifiedValue::Integer(value) => {
1234                    *self = UniformCollector::Integer {
1235                        prototype: IntegerStorage::from_scalar(value.clone()),
1236                        values: vec![value],
1237                    };
1238                    Ok(())
1239                }
1240                ClassifiedValue::ComplexF64(value) => {
1241                    *self = UniformCollector::ComplexF64(vec![value]);
1242                    Ok(())
1243                }
1244                ClassifiedValue::ComplexF32(value) => {
1245                    *self = UniformCollector::ComplexF32(vec![value]);
1246                    Ok(())
1247                }
1248                ClassifiedValue::IntegerComplex(real, imag) => {
1249                    *self = UniformCollector::IntegerComplex {
1250                        real_prototype: IntegerStorage::from_scalar(real.clone()),
1251                        imag_prototype: IntegerStorage::from_scalar(imag.clone()),
1252                        real_values: vec![real],
1253                        imag_values: vec![imag],
1254                    };
1255                    Ok(())
1256                }
1257                ClassifiedValue::Char(ch) => {
1258                    *self = UniformCollector::Char(vec![ch]);
1259                    Ok(())
1260                }
1261            },
1262            UniformCollector::Logical(bits) => match classify_value(value)? {
1263                ClassifiedValue::Logical(b) => {
1264                    bits.push(b as u8);
1265                    Ok(())
1266                }
1267                _ => Err(heterogeneous_uniform_output()),
1268            },
1269            UniformCollector::F64(data) => match classify_value(value)? {
1270                ClassifiedValue::F64(value) => {
1271                    data.push(value);
1272                    Ok(())
1273                }
1274                ClassifiedValue::ComplexF64(value) => {
1275                    let mut entries: Vec<(f64, f64)> = std::mem::take(data)
1276                        .into_iter()
1277                        .map(|value| (value, 0.0))
1278                        .collect();
1279                    entries.push(value);
1280                    *self = UniformCollector::ComplexF64(entries);
1281                    Ok(())
1282                }
1283                _ => Err(heterogeneous_uniform_output()),
1284            },
1285            UniformCollector::F32(data) => match classify_value(value)? {
1286                ClassifiedValue::F32(value) => {
1287                    data.push(value);
1288                    Ok(())
1289                }
1290                ClassifiedValue::ComplexF32(value) => {
1291                    let mut entries: Vec<(f32, f32)> = std::mem::take(data)
1292                        .into_iter()
1293                        .map(|value| (value, 0.0))
1294                        .collect();
1295                    entries.push(value);
1296                    *self = UniformCollector::ComplexF32(entries);
1297                    Ok(())
1298                }
1299                _ => Err(heterogeneous_uniform_output()),
1300            },
1301            UniformCollector::Integer { prototype, values } => match classify_value(value)? {
1302                ClassifiedValue::Integer(value) => {
1303                    let candidate = IntegerStorage::from_scalar(value.clone());
1304                    if candidate.numeric_dtype() != prototype.numeric_dtype() {
1305                        return Err(heterogeneous_uniform_output());
1306                    }
1307                    values.push(value);
1308                    Ok(())
1309                }
1310                _ => Err(heterogeneous_uniform_output()),
1311            },
1312            UniformCollector::ComplexF64(data) => match classify_value(value)? {
1313                ClassifiedValue::F64(value) => {
1314                    data.push((value, 0.0));
1315                    Ok(())
1316                }
1317                ClassifiedValue::ComplexF64(value) => {
1318                    data.push(value);
1319                    Ok(())
1320                }
1321                _ => Err(heterogeneous_uniform_output()),
1322            },
1323            UniformCollector::ComplexF32(data) => match classify_value(value)? {
1324                ClassifiedValue::F32(value) => {
1325                    data.push((value, 0.0));
1326                    Ok(())
1327                }
1328                ClassifiedValue::ComplexF32(value) => {
1329                    data.push(value);
1330                    Ok(())
1331                }
1332                _ => Err(heterogeneous_uniform_output()),
1333            },
1334            UniformCollector::IntegerComplex {
1335                real_prototype,
1336                imag_prototype,
1337                real_values,
1338                imag_values,
1339            } => match classify_value(value)? {
1340                ClassifiedValue::IntegerComplex(real, imag) => {
1341                    if IntegerStorage::from_scalar(real.clone()).numeric_dtype()
1342                        != real_prototype.numeric_dtype()
1343                        || IntegerStorage::from_scalar(imag.clone()).numeric_dtype()
1344                            != imag_prototype.numeric_dtype()
1345                    {
1346                        return Err(heterogeneous_uniform_output());
1347                    }
1348                    real_values.push(real);
1349                    imag_values.push(imag);
1350                    Ok(())
1351                }
1352                _ => Err(heterogeneous_uniform_output()),
1353            },
1354            UniformCollector::Char(chars) => match classify_value(value)? {
1355                ClassifiedValue::Char(ch) => {
1356                    chars.push(ch);
1357                    Ok(())
1358                }
1359                _ => Err(heterogeneous_uniform_output()),
1360            },
1361        }
1362    }
1363
1364    fn finish(self, shape: &[usize]) -> BuiltinResult<Value> {
1365        match self {
1366            UniformCollector::Pending => {
1367                let total = shape.iter().product();
1368                let tensor = Tensor::new(vec![0.0; total], shape.to_vec())
1369                    .map_err(|e| arrayfun_internal(format!("arrayfun: {e}")))?;
1370                Ok(Value::Tensor(tensor))
1371            }
1372            UniformCollector::F64(data) => {
1373                let tensor = Tensor::new(data, shape.to_vec())
1374                    .map_err(|e| arrayfun_internal(format!("arrayfun: {e}")))?;
1375                Ok(Value::Tensor(tensor))
1376            }
1377            UniformCollector::F32(data) => {
1378                let tensor = Tensor::from_f32(data, shape.to_vec())
1379                    .map_err(|e| arrayfun_internal(format!("arrayfun: {e}")))?;
1380                Ok(Value::Tensor(tensor))
1381            }
1382            UniformCollector::Integer { prototype, values } => {
1383                let storage = prototype
1384                    .from_same_class_values(values)
1385                    .map_err(|e| arrayfun_internal(format!("arrayfun: {e}")))?;
1386                let tensor = Tensor::new_integer(storage, shape.to_vec())
1387                    .map_err(|e| arrayfun_internal(format!("arrayfun: {e}")))?;
1388                Ok(Value::Tensor(tensor))
1389            }
1390            UniformCollector::Logical(bits) => {
1391                let logical = LogicalArray::new(bits, shape.to_vec())
1392                    .map_err(|e| arrayfun_internal(format!("arrayfun: {e}")))?;
1393                Ok(Value::LogicalArray(logical))
1394            }
1395            UniformCollector::ComplexF64(entries) => {
1396                let tensor = ComplexTensor::new(entries, shape.to_vec())
1397                    .map_err(|e| arrayfun_internal(format!("arrayfun: {e}")))?;
1398                Ok(Value::ComplexTensor(tensor))
1399            }
1400            UniformCollector::ComplexF32(entries) => {
1401                let tensor = ComplexTensor::from_f32(entries, shape.to_vec())
1402                    .map_err(|e| arrayfun_internal(format!("arrayfun: {e}")))?;
1403                Ok(Value::ComplexTensor(tensor))
1404            }
1405            UniformCollector::IntegerComplex {
1406                real_prototype,
1407                imag_prototype,
1408                real_values,
1409                imag_values,
1410            } => {
1411                let real = real_prototype
1412                    .from_same_class_values(real_values)
1413                    .map_err(|e| arrayfun_internal(format!("arrayfun: {e}")))?;
1414                let imag = imag_prototype
1415                    .from_same_class_values(imag_values)
1416                    .map_err(|e| arrayfun_internal(format!("arrayfun: {e}")))?;
1417                let storage = IntegerComplexStorage::new(real, imag)
1418                    .map_err(|e| arrayfun_internal(format!("arrayfun: {e}")))?;
1419                let tensor = ComplexTensor::new_integer(storage, shape.to_vec())
1420                    .map_err(|e| arrayfun_internal(format!("arrayfun: {e}")))?;
1421                Ok(Value::ComplexTensor(tensor))
1422            }
1423            UniformCollector::Char(chars) => {
1424                let normalized_shape = if shape.is_empty() {
1425                    vec![1, 1]
1426                } else {
1427                    shape.to_vec()
1428                };
1429
1430                if normalized_shape.len() > 2 {
1431                    return Err(arrayfun_error_with_detail(
1432                        &ARRAYFUN_ERROR_UNIFORM_OUTPUT_TYPE,
1433                        "character outputs with UniformOutput=true must be 2-D",
1434                    ));
1435                }
1436
1437                let rows = normalized_shape.first().copied().unwrap_or(1);
1438                let cols = normalized_shape.get(1).copied().unwrap_or(1);
1439                let expected = rows.checked_mul(cols).ok_or_else(|| {
1440                    arrayfun_internal("arrayfun: character output size exceeds platform limits")
1441                })?;
1442
1443                if expected != chars.len() {
1444                    return Err(arrayfun_error_with_detail(
1445                        &ARRAYFUN_ERROR_UNIFORM_OUTPUT_TYPE,
1446                        "callback returned the wrong number of characters",
1447                    ));
1448                }
1449
1450                let mut row_major = vec!['\0'; expected];
1451                for col in 0..cols {
1452                    for row in 0..rows {
1453                        let col_major_idx = row + col * rows;
1454                        let row_major_idx = row * cols + col;
1455                        row_major[row_major_idx] = chars[col_major_idx];
1456                    }
1457                }
1458
1459                let array = CharArray::new(row_major, rows, cols)
1460                    .map_err(|e| arrayfun_internal(format!("arrayfun: {e}")))?;
1461                Ok(Value::CharArray(array))
1462            }
1463        }
1464    }
1465}
1466
1467enum ClassifiedValue {
1468    Logical(bool),
1469    F64(f64),
1470    F32(f32),
1471    Integer(IntValue),
1472    ComplexF64((f64, f64)),
1473    ComplexF32((f32, f32)),
1474    IntegerComplex(IntValue, IntValue),
1475    Char(char),
1476}
1477
1478fn classify_value(value: &Value) -> BuiltinResult<ClassifiedValue> {
1479    match value {
1480        Value::Bool(b) => Ok(ClassifiedValue::Logical(*b)),
1481        Value::LogicalArray(la) if la.len() == 1 => Ok(ClassifiedValue::Logical(la.data[0] != 0)),
1482        Value::Int(value) => Ok(ClassifiedValue::Integer(value.clone())),
1483        Value::Num(n) => Ok(ClassifiedValue::F64(*n)),
1484        Value::Tensor(t) if tensor::is_scalar_tensor(t) => {
1485            match t.numeric_value_at(0).ok_or_else(|| {
1486                arrayfun_internal("arrayfun: scalar tensor has no numeric storage value")
1487            })? {
1488                NumericScalar::F64(value) => Ok(ClassifiedValue::F64(value)),
1489                NumericScalar::F32(value) => Ok(ClassifiedValue::F32(value)),
1490                value => Ok(ClassifiedValue::Integer(
1491                    value.into_int_value().ok_or_else(|| {
1492                        arrayfun_internal("arrayfun: integer scalar classification failed")
1493                    })?,
1494                )),
1495            }
1496        }
1497        Value::Complex(re, im) => Ok(ClassifiedValue::ComplexF64((*re, *im))),
1498        Value::ComplexTensor(t) if tensor::is_scalar_complex_tensor(t) => {
1499            let (real, imag) = t.numeric_value_at(0).ok_or_else(|| {
1500                arrayfun_internal("arrayfun: scalar complex tensor has no storage value")
1501            })?;
1502            match (real, imag) {
1503                (NumericScalar::F64(real), NumericScalar::F64(imag)) => {
1504                    Ok(ClassifiedValue::ComplexF64((real, imag)))
1505                }
1506                (NumericScalar::F32(real), NumericScalar::F32(imag)) => {
1507                    Ok(ClassifiedValue::ComplexF32((real, imag)))
1508                }
1509                (real, imag) => Ok(ClassifiedValue::IntegerComplex(
1510                    real.into_int_value().ok_or_else(|| {
1511                        arrayfun_internal(
1512                            "arrayfun: complex callback result has inconsistent component classes",
1513                        )
1514                    })?,
1515                    imag.into_int_value().ok_or_else(|| {
1516                        arrayfun_internal(
1517                            "arrayfun: complex callback result has inconsistent component classes",
1518                        )
1519                    })?,
1520                )),
1521            }
1522        }
1523        Value::CharArray(ca) if ca.rows * ca.cols == 1 => {
1524            let ch = ca.data.first().copied().unwrap_or('\0');
1525            Ok(ClassifiedValue::Char(ch))
1526        }
1527        other => Err(arrayfun_error_with_detail(
1528            &ARRAYFUN_ERROR_UNIFORM_OUTPUT_TYPE,
1529            format!(
1530                "callback must return scalar numeric, logical, character, or complex values for UniformOutput=true (got {other:?})"
1531            ),
1532        )),
1533    }
1534}
1535
1536fn make_error_struct(raw_error: &str, linear_index: usize) -> Value {
1537    let (identifier, message) = split_error_message(raw_error);
1538    let mut st = runmat_value::StructValue::new();
1539    st.fields
1540        .insert("identifier".to_string(), Value::String(identifier));
1541    st.fields
1542        .insert("message".to_string(), Value::String(message));
1543    st.fields
1544        .insert("index".to_string(), Value::Num((linear_index + 1) as f64));
1545    Value::Struct(st)
1546}
1547
1548fn split_error_message(raw: &str) -> (String, String) {
1549    let trimmed = raw.trim();
1550    let mut indices = trimmed.match_indices(':');
1551    if let Some((_, _)) = indices.next() {
1552        if let Some((second_idx, _)) = indices.next() {
1553            let identifier = trimmed[..second_idx].trim().to_string();
1554            let message = trimmed[second_idx + 1..].trim().to_string();
1555            if !identifier.is_empty() && identifier.contains(':') {
1556                return (
1557                    identifier,
1558                    if message.is_empty() {
1559                        trimmed.to_string()
1560                    } else {
1561                        message
1562                    },
1563                );
1564            }
1565        } else if trimmed.len() >= 7
1566            && (trimmed[..7].eq_ignore_ascii_case("matlab:")
1567                || trimmed[..7].eq_ignore_ascii_case("runmat:"))
1568        {
1569            return (trimmed.to_string(), String::new());
1570        }
1571    }
1572    (
1573        "RunMat:arrayfun:FunctionError".to_string(),
1574        trimmed.to_string(),
1575    )
1576}
1577
1578#[cfg(test)]
1579pub(crate) mod tests {
1580    use super::*;
1581    use crate::builtins::common::test_support;
1582    use futures::executor::block_on;
1583    use runmat_builtins::{ResolveContext, Type};
1584    use runmat_value::{ComplexTensor, IntegerComplexStorage, IntegerStorage, Tensor, Value};
1585    use std::sync::Arc;
1586
1587    fn call(func: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
1588        block_on(arrayfun_builtin(func, rest))
1589    }
1590
1591    fn values(tensor: &Tensor) -> Vec<f64> {
1592        tensor.materialize_f64()
1593    }
1594
1595    #[test]
1596    fn typed_integer_input_elements_are_extracted_from_exact_storage() {
1597        let tensor =
1598            Tensor::new_integer(IntegerStorage::U64(vec![9_007_199_254_740_993]), vec![1, 1])
1599                .expect("integer tensor");
1600        let input = ArrayInput {
1601            data: ArrayData::Tensor(tensor),
1602            shape: vec![1, 1],
1603            strides: vec![1, 1],
1604        };
1605
1606        assert_eq!(
1607            input.value_at(0, &[1, 1]).expect("value"),
1608            Value::Int(runmat_value::IntValue::U64(9_007_199_254_740_993))
1609        );
1610    }
1611
1612    #[test]
1613    fn uniform_classifier_preserves_typed_integer_tensor_storage() {
1614        let tensor =
1615            Tensor::new_integer(IntegerStorage::U64(vec![9_007_199_254_740_993]), vec![1, 1])
1616                .expect("integer tensor");
1617
1618        match classify_value(&Value::Tensor(tensor)).expect("classify") {
1619            ClassifiedValue::Integer(value) => {
1620                assert_eq!(value, IntValue::U64(9_007_199_254_740_993));
1621            }
1622            _ => panic!("expected integer classification"),
1623        }
1624    }
1625
1626    #[test]
1627    fn uniform_classifier_preserves_wide_integer_scalar() {
1628        match classify_value(&Value::Int(IntValue::I64(i64::MIN))).expect("classify") {
1629            ClassifiedValue::Integer(value) => assert_eq!(value, IntValue::I64(i64::MIN)),
1630            _ => panic!("expected integer classification"),
1631        }
1632    }
1633
1634    #[test]
1635    fn uniform_collector_preserves_every_integer_class() {
1636        let cases = [
1637            (
1638                IntValue::I8(i8::MIN),
1639                IntValue::I8(i8::MAX),
1640                IntegerStorage::I8(vec![i8::MIN, i8::MAX]),
1641            ),
1642            (
1643                IntValue::I16(i16::MIN),
1644                IntValue::I16(i16::MAX),
1645                IntegerStorage::I16(vec![i16::MIN, i16::MAX]),
1646            ),
1647            (
1648                IntValue::I32(i32::MIN),
1649                IntValue::I32(i32::MAX),
1650                IntegerStorage::I32(vec![i32::MIN, i32::MAX]),
1651            ),
1652            (
1653                IntValue::I64(i64::MIN),
1654                IntValue::I64(i64::MAX),
1655                IntegerStorage::I64(vec![i64::MIN, i64::MAX]),
1656            ),
1657            (
1658                IntValue::U8(u8::MIN),
1659                IntValue::U8(u8::MAX),
1660                IntegerStorage::U8(vec![u8::MIN, u8::MAX]),
1661            ),
1662            (
1663                IntValue::U16(u16::MIN),
1664                IntValue::U16(u16::MAX),
1665                IntegerStorage::U16(vec![u16::MIN, u16::MAX]),
1666            ),
1667            (
1668                IntValue::U32(u32::MIN),
1669                IntValue::U32(u32::MAX),
1670                IntegerStorage::U32(vec![u32::MIN, u32::MAX]),
1671            ),
1672            (
1673                IntValue::U64(9_007_199_254_740_993),
1674                IntValue::U64(u64::MAX),
1675                IntegerStorage::U64(vec![9_007_199_254_740_993, u64::MAX]),
1676            ),
1677        ];
1678
1679        for (first, second, expected) in cases {
1680            let mut collector = UniformCollector::Pending;
1681            collector.push(&Value::Int(first)).expect("first value");
1682            collector.push(&Value::Int(second)).expect("second value");
1683            let Value::Tensor(tensor) = collector.finish(&[1, 2]).expect("finish") else {
1684                panic!("expected integer tensor");
1685            };
1686            assert_eq!(tensor.integer_storage(), Some(&expected));
1687        }
1688    }
1689
1690    #[test]
1691    fn uniform_collector_rejects_mixed_integer_classes() {
1692        let mut collector = UniformCollector::Pending;
1693        collector
1694            .push(&Value::Int(IntValue::U8(1)))
1695            .expect("first value");
1696        let error = collector
1697            .push(&Value::Int(IntValue::U16(2)))
1698            .expect_err("mixed integer classes must fail");
1699        assert_eq!(
1700            error.identifier(),
1701            ARRAYFUN_ERROR_UNIFORM_OUTPUT_TYPE.identifier
1702        );
1703    }
1704
1705    #[test]
1706    fn arrayfun_preserves_every_integer_input_and_uniform_output_class() {
1707        for (storage, callback) in [
1708            (IntegerStorage::I8(vec![i8::MIN, i8::MAX]), "int8"),
1709            (IntegerStorage::I16(vec![i16::MIN, i16::MAX]), "int16"),
1710            (IntegerStorage::I32(vec![i32::MIN, i32::MAX]), "int32"),
1711            (IntegerStorage::I64(vec![i64::MIN, i64::MAX]), "int64"),
1712            (IntegerStorage::U8(vec![0, u8::MAX]), "uint8"),
1713            (IntegerStorage::U16(vec![0, u16::MAX]), "uint16"),
1714            (IntegerStorage::U32(vec![0, u32::MAX]), "uint32"),
1715            (
1716                IntegerStorage::U64(vec![9_007_199_254_740_993, u64::MAX]),
1717                "uint64",
1718            ),
1719        ] {
1720            let result = call(
1721                Value::FunctionHandle(callback.to_string()),
1722                vec![Value::Tensor(
1723                    Tensor::new_integer(storage.clone(), vec![1, 2]).expect("integer input"),
1724                )],
1725            )
1726            .expect("arrayfun integer identity cast");
1727            let Value::Tensor(result) = result else {
1728                panic!("expected typed integer tensor");
1729            };
1730            assert_eq!(result.integer_storage(), Some(&storage));
1731        }
1732    }
1733
1734    #[test]
1735    fn arrayfun_preserves_native_single_and_complex_single_scalars() {
1736        let input =
1737            ArrayData::Tensor(Tensor::from_f32(vec![0.25, -1.5], vec![1, 2]).expect("single"));
1738        let first = input.value_at(0).expect("first single");
1739        let Value::Tensor(first) = first else {
1740            panic!("single element must remain a scalar tensor");
1741        };
1742        assert_eq!(first.as_f32_slice(), Some(&[0.25][..]));
1743
1744        let mut real_collector = UniformCollector::Pending;
1745        real_collector
1746            .push(&Value::Tensor(first))
1747            .expect("single result");
1748        real_collector
1749            .push(&Value::Tensor(
1750                Tensor::from_f32(vec![-1.5], vec![1, 1]).expect("single scalar"),
1751            ))
1752            .expect("second single result");
1753        let Value::Tensor(real) = real_collector.finish(&[1, 2]).expect("single output") else {
1754            panic!("expected native single output");
1755        };
1756        assert_eq!(real.as_f32_slice(), Some(&[0.25, -1.5][..]));
1757
1758        let mut complex_collector = UniformCollector::Pending;
1759        for value in [(1.25_f32, -2.5_f32), (3.5_f32, 4.75_f32)] {
1760            complex_collector
1761                .push(&Value::ComplexTensor(
1762                    ComplexTensor::from_f32(vec![value], vec![1, 1])
1763                        .expect("complex single scalar"),
1764                ))
1765                .expect("complex single result");
1766        }
1767        let Value::ComplexTensor(complex) =
1768            complex_collector.finish(&[1, 2]).expect("complex output")
1769        else {
1770            panic!("expected complex single output");
1771        };
1772        assert_eq!(
1773            complex.as_f32_slice(),
1774            Some(&[(1.25, -2.5), (3.5, 4.75)][..])
1775        );
1776    }
1777
1778    #[test]
1779    fn uniform_collector_preserves_exact_complex_integer_storage() {
1780        let mut collector = UniformCollector::Pending;
1781        for (real, imag) in [
1782            (
1783                IntValue::I64(9_007_199_254_740_993),
1784                IntValue::I64(-9_007_199_254_740_993),
1785            ),
1786            (IntValue::I64(i64::MAX), IntValue::I64(i64::MIN)),
1787        ] {
1788            let storage = IntegerComplexStorage::new(
1789                IntegerStorage::from_scalar(real),
1790                IntegerStorage::from_scalar(imag),
1791            )
1792            .expect("integer complex scalar");
1793            collector
1794                .push(&Value::ComplexTensor(
1795                    ComplexTensor::new_integer(storage, vec![1, 1])
1796                        .expect("integer complex tensor"),
1797                ))
1798                .expect("integer complex result");
1799        }
1800        let Value::ComplexTensor(output) =
1801            collector.finish(&[1, 2]).expect("integer complex output")
1802        else {
1803            panic!("expected integer complex tensor");
1804        };
1805        let storage = output.integer_storage().expect("exact component storage");
1806        assert_eq!(
1807            storage.real,
1808            IntegerStorage::I64(vec![9_007_199_254_740_993, i64::MAX])
1809        );
1810        assert_eq!(
1811            storage.imag,
1812            IntegerStorage::I64(vec![-9_007_199_254_740_993, i64::MIN])
1813        );
1814    }
1815
1816    #[test]
1817    fn uniform_collector_rejects_different_noninteger_classes() {
1818        for second in [
1819            Value::Num(1.0),
1820            Value::Tensor(Tensor::from_f32(vec![1.0], vec![1, 1]).expect("single")),
1821            Value::CharArray(CharArray::new(vec!['1'], 1, 1).expect("char")),
1822        ] {
1823            let mut collector = UniformCollector::Pending;
1824            collector.push(&Value::Bool(true)).expect("logical");
1825            let error = collector
1826                .push(&second)
1827                .expect_err("heterogeneous uniform result must reject");
1828            assert_eq!(
1829                error.identifier(),
1830                ARRAYFUN_ERROR_UNIFORM_OUTPUT_TYPE.identifier
1831            );
1832        }
1833    }
1834
1835    #[test]
1836    fn arrayfun_rejects_every_typed_integer_uniform_output_control() {
1837        let input = Value::Tensor(Tensor::new(vec![1.0], vec![1, 1]).expect("input"));
1838        for storage in [
1839            IntegerStorage::I8(vec![1]),
1840            IntegerStorage::I16(vec![1]),
1841            IntegerStorage::I32(vec![1]),
1842            IntegerStorage::I64(vec![1]),
1843            IntegerStorage::U8(vec![1]),
1844            IntegerStorage::U16(vec![1]),
1845            IntegerStorage::U32(vec![1]),
1846            IntegerStorage::U64(vec![1]),
1847        ] {
1848            for control in [
1849                Value::Int(storage.value_at(0).expect("scalar")),
1850                Value::Tensor(
1851                    Tensor::new_integer(storage.clone(), vec![1, 1]).expect("typed control"),
1852                ),
1853            ] {
1854                let error = call(
1855                    Value::FunctionHandle("sin".to_string()),
1856                    vec![input.clone(), Value::from("UniformOutput"), control],
1857                )
1858                .expect_err("typed integer control must reject");
1859                assert_eq!(
1860                    error.identifier(),
1861                    ARRAYFUN_ERROR_UNIFORM_OUTPUT_OPTION.identifier
1862                );
1863            }
1864        }
1865    }
1866
1867    #[test]
1868    fn arrayfun_text_callable_and_host_scalar_expansion_are_mode_gated() {
1869        let input = Value::Tensor(Tensor::new(vec![1.0, 2.0], vec![1, 2]).expect("input"));
1870        {
1871            let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
1872            let error = call(Value::from("sin"), vec![input.clone()])
1873                .expect_err("text callable must reject in compatible mode");
1874            assert_eq!(
1875                error.identifier(),
1876                ARRAYFUN_TEXT_CALLABLE_EXTENSION.error_identifier
1877            );
1878            let error = call(
1879                Value::FunctionHandle("atan2".to_string()),
1880                vec![input.clone(), Value::Num(1.0)],
1881            )
1882            .expect_err("host scalar expansion must reject in compatible mode");
1883            assert_eq!(
1884                error.identifier(),
1885                ARRAYFUN_HOST_SCALAR_EXPANSION_EXTENSION.error_identifier
1886            );
1887        }
1888        {
1889            let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1890            assert!(call(Value::from("sin"), vec![input.clone()]).is_ok());
1891            assert!(call(
1892                Value::FunctionHandle("atan2".to_string()),
1893                vec![input, Value::Num(1.0)],
1894            )
1895            .is_ok());
1896        }
1897    }
1898
1899    #[test]
1900    fn arrayfun_error_struct_has_only_documented_fields() {
1901        let Value::Struct(error) = make_error_struct("RunMat:test:Failure: detail", 4) else {
1902            panic!("expected error struct");
1903        };
1904        let mut fields: Vec<_> = error.fields.keys().map(String::as_str).collect();
1905        fields.sort_unstable();
1906        assert_eq!(fields, vec!["identifier", "index", "message"]);
1907        assert_eq!(error.fields.get("index"), Some(&Value::Num(5.0)));
1908    }
1909
1910    #[test]
1911    fn uniform_classifier_reads_typed_complex_integer_tensor_storage_exactly() {
1912        let storage = IntegerComplexStorage::new(
1913            IntegerStorage::I64(vec![9_007_199_254_740_993]),
1914            IntegerStorage::I64(vec![-9_007_199_254_740_993]),
1915        )
1916        .expect("complex integer storage");
1917        let tensor = ComplexTensor::new_integer(storage, vec![1, 1]).expect("complex tensor");
1918
1919        match classify_value(&Value::ComplexTensor(tensor)).expect("classify") {
1920            ClassifiedValue::IntegerComplex(re, im) => {
1921                assert_eq!(re, IntValue::I64(9_007_199_254_740_993));
1922                assert_eq!(im, IntValue::I64(-9_007_199_254_740_993));
1923            }
1924            _ => panic!("expected exact integer-complex classification"),
1925        }
1926    }
1927
1928    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1929    #[test]
1930    fn arrayfun_basic_sin() {
1931        let tensor = Tensor::new(vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0], vec![2, 3]).unwrap();
1932        let expected: Vec<f64> = values(&tensor).into_iter().map(f64::sin).collect();
1933        let result = call(
1934            Value::FunctionHandle("sin".to_string()),
1935            vec![Value::Tensor(tensor.clone())],
1936        )
1937        .expect("arrayfun");
1938        match result {
1939            Value::Tensor(out) => {
1940                assert_eq!(out.shape, vec![2, 3]);
1941                assert_eq!(values(&out), expected);
1942            }
1943            other => panic!("expected tensor, got {other:?}"),
1944        }
1945    }
1946
1947    #[test]
1948    fn arrayfun_semantic_function_handle_uses_semantic_invoker() {
1949        let _guard = crate::user_functions::install_semantic_function_invoker(Some(Arc::new(
1950            |function, args, requested_outputs| {
1951                assert_eq!(function, 78);
1952                assert_eq!(requested_outputs, 1);
1953                let [Value::Num(value)] = args else {
1954                    panic!("expected scalar numeric argument, got {args:?}");
1955                };
1956                let value = *value;
1957                Box::pin(async move { Ok(Value::Num(value + 10.0)) })
1958            },
1959        )));
1960        let tensor = Tensor::new(vec![1.0, 2.0], vec![1, 2]).expect("tensor");
1961        let handle = Value::BoundFunctionHandle {
1962            name: "arrayfun_target".to_string(),
1963            function: 78,
1964        };
1965
1966        let result = call(handle, vec![Value::Tensor(tensor)]).expect("semantic arrayfun");
1967        match result {
1968            Value::Tensor(out) => {
1969                assert_eq!(out.shape, vec![1, 2]);
1970                assert_eq!(values(&out), vec![11.0, 12.0]);
1971            }
1972            other => panic!("expected tensor, got {other:?}"),
1973        }
1974    }
1975
1976    #[test]
1977    fn arrayfun_name_only_callback_uses_semantic_resolver() {
1978        let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1979        let _resolver_guard =
1980            crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
1981                (name == "resolved_arrayfun_target").then_some(80)
1982            })));
1983        let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
1984            Arc::new(|function, args, requested_outputs| {
1985                assert_eq!(function, 80);
1986                assert_eq!(requested_outputs, 1);
1987                let [Value::Num(value)] = args else {
1988                    panic!("expected scalar numeric argument, got {args:?}");
1989                };
1990                let value = *value;
1991                Box::pin(async move { Ok(Value::Num(value + 20.0)) })
1992            }),
1993        ));
1994        let tensor = Tensor::new(vec![1.0, 2.0], vec![1, 2]).expect("tensor");
1995
1996        let result = call(
1997            Value::String("resolved_arrayfun_target".to_string()),
1998            vec![Value::Tensor(tensor)],
1999        )
2000        .expect("resolved name-only arrayfun");
2001        match result {
2002            Value::Tensor(out) => {
2003                assert_eq!(out.shape, vec![1, 2]);
2004                assert_eq!(values(&out), vec![21.0, 22.0]);
2005            }
2006            other => panic!("expected tensor, got {other:?}"),
2007        }
2008    }
2009
2010    #[test]
2011    fn arrayfun_qualified_text_callback_classifies_as_external_name() {
2012        let callable =
2013            Callable::from_text("pkg.callback").expect("qualified arrayfun callback should parse");
2014        assert!(matches!(
2015            callable,
2016            Callable::ExternalName { name } if name == "pkg.callback"
2017        ));
2018    }
2019
2020    #[test]
2021    fn arrayfun_external_handle_uses_semantic_resolver() {
2022        let _resolver_guard =
2023            crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2024                (name == "pkg.callback").then_some(87)
2025            })));
2026        let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
2027            Arc::new(|function, args, requested_outputs| {
2028                assert_eq!(function, 87);
2029                assert_eq!(requested_outputs, 1);
2030                let [Value::Num(value)] = args else {
2031                    panic!("expected scalar numeric argument, got {args:?}");
2032                };
2033                let value = *value;
2034                Box::pin(async move { Ok(Value::Num(value + 30.0)) })
2035            }),
2036        ));
2037        let tensor = Tensor::new(vec![1.0, 2.0], vec![1, 2]).expect("tensor");
2038
2039        let result = call(
2040            Value::ExternalFunctionHandle("pkg.callback".to_string()),
2041            vec![Value::Tensor(tensor)],
2042        )
2043        .expect("resolved external-handle arrayfun");
2044        match result {
2045            Value::Tensor(out) => {
2046                assert_eq!(out.shape, vec![1, 2]);
2047                assert_eq!(values(&out), vec![31.0, 32.0]);
2048            }
2049            other => panic!("expected tensor, got {other:?}"),
2050        }
2051    }
2052
2053    #[test]
2054    fn arrayfun_single_segment_external_handle_uses_runtime_name_resolution() {
2055        let _resolver_guard =
2056            crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2057                (name == "callback").then_some(887)
2058            })));
2059        let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
2060            Arc::new(|function, args, requested_outputs| {
2061                assert_eq!(function, 887);
2062                assert_eq!(requested_outputs, 1);
2063                let [Value::Num(value)] = args else {
2064                    panic!("expected scalar numeric argument, got {args:?}");
2065                };
2066                let value = *value;
2067                Box::pin(async move { Ok(Value::Num(value + 40.0)) })
2068            }),
2069        ));
2070        let tensor = Tensor::new(vec![1.0, 2.0], vec![1, 2]).expect("tensor");
2071
2072        let result = call(
2073            Value::ExternalFunctionHandle("callback".to_string()),
2074            vec![Value::Tensor(tensor)],
2075        )
2076        .expect("single-segment external-handle arrayfun should resolve via runtime-name policy");
2077        match result {
2078            Value::Tensor(out) => {
2079                assert_eq!(out.shape, vec![1, 2]);
2080                assert_eq!(values(&out), vec![41.0, 42.0]);
2081            }
2082            other => panic!("expected tensor, got {other:?}"),
2083        }
2084    }
2085
2086    #[test]
2087    fn arrayfun_external_handle_prefers_semantic_handle_binding_when_resolved() {
2088        let _resolver_guard =
2089            crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2090                (name == "pkg.callback").then_some(87)
2091            })));
2092        let callable =
2093            Callable::from_function(Value::ExternalFunctionHandle("pkg.callback".to_string()))
2094                .expect("external handle should parse");
2095        assert!(matches!(
2096            callable,
2097            Callable::Closure(Closure {
2098                function_name,
2099                bound_function: Some(87),
2100                ..
2101            }) if function_name == "pkg.callback"
2102        ));
2103    }
2104
2105    #[test]
2106    fn arrayfun_name_only_closure_prefers_semantic_handle_binding_when_resolved() {
2107        let _resolver_guard =
2108            crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2109                (name == "pkg.callback").then_some(187)
2110            })));
2111        let callable = Callable::from_function(Value::Closure(Closure {
2112            function_name: "pkg.callback".to_string(),
2113            bound_function: None,
2114            captures: vec![Value::Num(5.0)],
2115        }))
2116        .expect("closure callback should parse");
2117        assert!(matches!(
2118            callable,
2119            Callable::Closure(Closure {
2120                function_name,
2121                bound_function: Some(187),
2122                captures
2123            }) if function_name == "pkg.callback" && captures == vec![Value::Num(5.0)]
2124        ));
2125    }
2126
2127    #[test]
2128    fn arrayfun_name_only_closure_call_uses_semantic_resolver_when_unbound() {
2129        let _resolver_guard =
2130            crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2131                (name == "pkg.callback").then_some(287)
2132            })));
2133        let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
2134            Arc::new(|function, args, requested_outputs| {
2135                assert_eq!(function, 287);
2136                assert_eq!(requested_outputs, 1);
2137                assert_eq!(args, &[Value::Num(5.0), Value::Num(4.0)]);
2138                Box::pin(async { Ok(Value::Num(9.0)) })
2139            }),
2140        ));
2141        let callable = Callable::Closure(Closure {
2142            function_name: "pkg.callback".to_string(),
2143            bound_function: None,
2144            captures: vec![Value::Num(5.0)],
2145        });
2146        let value = block_on(callable.call(&[Value::Num(4.0)])).expect("closure call");
2147        assert_eq!(value, Value::Num(9.0));
2148    }
2149
2150    #[test]
2151    fn arrayfun_external_handle_errors_as_undefined_when_unresolved() {
2152        let _resolver_guard =
2153            crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|_| None)));
2154        let tensor = Tensor::new(vec![1.0], vec![1, 1]).expect("tensor");
2155
2156        let err = call(
2157            Value::ExternalFunctionHandle("pkg.callback".to_string()),
2158            vec![Value::Tensor(tensor)],
2159        )
2160        .expect_err("unresolved external callback should error");
2161        assert_eq!(
2162            err.identifier(),
2163            ARRAYFUN_ERROR_UNDEFINED_FUNCTION.identifier,
2164            "unexpected error: {}",
2165            err.message()
2166        );
2167        assert!(
2168            err.message().contains("ExternalName(QualifiedName"),
2169            "unexpected error: {err:?}"
2170        );
2171        assert!(
2172            !err.message().contains("Undefined function 'pkg.callback'"),
2173            "well-formed external callback should report typed identity: {err:?}"
2174        );
2175    }
2176
2177    #[test]
2178    fn arrayfun_malformed_external_handle_errors_as_undefined_when_unresolved() {
2179        let _resolver_guard =
2180            crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|_| None)));
2181        let tensor = Tensor::new(vec![1.0], vec![1, 1]).expect("tensor");
2182
2183        let err = call(
2184            Value::ExternalFunctionHandle("pkg..callback".to_string()),
2185            vec![Value::Tensor(tensor)],
2186        )
2187        .expect_err("malformed unresolved external callback should error");
2188        assert_eq!(
2189            err.identifier(),
2190            ARRAYFUN_ERROR_UNDEFINED_FUNCTION.identifier,
2191            "unexpected error: {}",
2192            err.message()
2193        );
2194        assert!(
2195            err.message().contains("pkg..callback"),
2196            "unexpected error: {err:?}"
2197        );
2198    }
2199
2200    #[test]
2201    fn arrayfun_type_tracks_function_returns() {
2202        let func = Type::Function {
2203            params: vec![Type::Num],
2204            returns: Box::new(Type::Num),
2205        };
2206        assert_eq!(
2207            arrayfun_type(&[func, Type::tensor()], &ResolveContext::new(Vec::new())),
2208            Type::tensor()
2209        );
2210    }
2211
2212    #[test]
2213    fn arrayfun_type_uses_logical_returns() {
2214        let func = Type::Function {
2215            params: vec![Type::Num],
2216            returns: Box::new(Type::Bool),
2217        };
2218        assert_eq!(
2219            arrayfun_type(&[func, Type::tensor()], &ResolveContext::new(Vec::new())),
2220            Type::logical()
2221        );
2222    }
2223
2224    #[test]
2225    fn arrayfun_type_with_text_args_stays_unknown() {
2226        let func = Type::Function {
2227            params: vec![Type::Num],
2228            returns: Box::new(Type::Num),
2229        };
2230        assert_eq!(
2231            arrayfun_type(
2232                &[func, Type::tensor(), Type::String, Type::Bool],
2233                &ResolveContext::new(Vec::new()),
2234            ),
2235            Type::Unknown
2236        );
2237    }
2238
2239    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2240    #[test]
2241    fn arrayfun_additional_scalar_argument() {
2242        let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
2243        let tensor = Tensor::new(vec![0.5, 1.0, -1.0], vec![3, 1]).unwrap();
2244        let expected: Vec<f64> = values(&tensor).into_iter().map(|y| y.atan2(1.0)).collect();
2245        let result = call(
2246            Value::FunctionHandle("atan2".to_string()),
2247            vec![Value::Tensor(tensor), Value::Num(1.0)],
2248        )
2249        .expect("arrayfun");
2250        match result {
2251            Value::Tensor(out) => {
2252                assert_eq!(values(&out), expected);
2253            }
2254            other => panic!("expected tensor, got {other:?}"),
2255        }
2256    }
2257
2258    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2259    #[test]
2260    fn arrayfun_uniform_false_returns_cell() {
2261        let tensor = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
2262        let expected: Vec<Value> = values(&tensor)
2263            .into_iter()
2264            .map(|x| Value::Num(x.sin()))
2265            .collect();
2266        let result = call(
2267            Value::FunctionHandle("sin".to_string()),
2268            vec![
2269                Value::Tensor(tensor),
2270                Value::String("UniformOutput".into()),
2271                Value::Bool(false),
2272            ],
2273        )
2274        .expect("arrayfun");
2275        let Value::Cell(cell) = result else {
2276            panic!("expected cell, got something else");
2277        };
2278        assert_eq!(cell.rows, 2);
2279        assert_eq!(cell.cols, 1);
2280        for (row, value) in expected.iter().enumerate() {
2281            assert_eq!(cell.get(row, 0).unwrap(), *value);
2282        }
2283    }
2284
2285    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2286    #[test]
2287    fn arrayfun_uniform_output_option_identifier() {
2288        let tensor = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
2289        let err = call(
2290            Value::FunctionHandle("sin".to_string()),
2291            vec![
2292                Value::Tensor(tensor),
2293                Value::String("UniformOutput".into()),
2294                Value::String("maybe".into()),
2295            ],
2296        )
2297        .expect_err("expected invalid uniform output option");
2298        assert_eq!(
2299            err.identifier(),
2300            ARRAYFUN_ERROR_UNIFORM_OUTPUT_OPTION.identifier
2301        );
2302    }
2303
2304    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2305    #[test]
2306    fn arrayfun_unknown_name_value_identifier() {
2307        let tensor = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
2308        let err = call(
2309            Value::FunctionHandle("sin".to_string()),
2310            vec![
2311                Value::Tensor(tensor),
2312                Value::String("MysteryFlag".into()),
2313                Value::Bool(true),
2314            ],
2315        )
2316        .expect_err("expected unknown name-value error");
2317        assert_eq!(err.identifier(), ARRAYFUN_ERROR_INVALID_INPUT.identifier);
2318    }
2319
2320    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2321    #[test]
2322    fn arrayfun_size_mismatch_errors() {
2323        let taller = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
2324        let shorter = Tensor::new(vec![4.0, 5.0], vec![2, 1]).unwrap();
2325        let err = call(
2326            Value::FunctionHandle("sin".to_string()),
2327            vec![Value::Tensor(taller), Value::Tensor(shorter)],
2328        )
2329        .expect_err("expected size mismatch error");
2330        let err = err.to_string();
2331        assert!(
2332            err.contains("does not match"),
2333            "expected size mismatch error, got {err}"
2334        );
2335    }
2336
2337    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2338    #[test]
2339    fn arrayfun_error_handler_recovers() {
2340        let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
2341        let handler = Value::Closure(Closure {
2342            function_name: "__arrayfun_test_handler".into(),
2343            bound_function: None,
2344            captures: vec![Value::Num(42.0)],
2345        });
2346        let result = call(
2347            Value::FunctionHandle("nonexistent_builtin".into()),
2348            vec![
2349                Value::Tensor(tensor),
2350                Value::String("ErrorHandler".into()),
2351                handler,
2352            ],
2353        )
2354        .expect("arrayfun error handler");
2355        match result {
2356            Value::Tensor(out) => {
2357                assert_eq!(out.shape, vec![3, 1]);
2358                assert_eq!(values(&out), vec![42.0, 42.0, 42.0]);
2359            }
2360            other => panic!("expected tensor, got {other:?}"),
2361        }
2362    }
2363
2364    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2365    #[test]
2366    fn arrayfun_error_without_handler_propagates_identifier() {
2367        let tensor = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
2368        let err = call(
2369            Value::FunctionHandle("nonexistent_builtin".into()),
2370            vec![Value::Tensor(tensor)],
2371        )
2372        .expect_err("expected unresolved function error");
2373        assert_eq!(
2374            err.identifier(),
2375            ARRAYFUN_ERROR_UNDEFINED_FUNCTION.identifier,
2376            "unexpected error: {}",
2377            err.message()
2378        );
2379    }
2380
2381    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2382    #[test]
2383    fn arrayfun_uniform_logical_result() {
2384        let tensor = Tensor::new(vec![1.0, f64::NAN, 0.0, f64::INFINITY], vec![4, 1]).unwrap();
2385        let result = call(
2386            Value::FunctionHandle("isfinite".to_string()),
2387            vec![Value::Tensor(tensor)],
2388        )
2389        .expect("arrayfun isfinite");
2390        match result {
2391            Value::LogicalArray(la) => {
2392                assert_eq!(la.shape, vec![4, 1]);
2393                assert_eq!(la.data, vec![1, 0, 1, 0]);
2394            }
2395            other => panic!("expected logical array, got {other:?}"),
2396        }
2397    }
2398
2399    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2400    #[test]
2401    fn arrayfun_uniform_character_result() {
2402        let tensor = Tensor::new(vec![65.0, 66.0, 67.0], vec![1, 3]).unwrap();
2403        let result = call(
2404            Value::FunctionHandle("char".to_string()),
2405            vec![Value::Tensor(tensor)],
2406        )
2407        .expect("arrayfun char");
2408        match result {
2409            Value::CharArray(ca) => {
2410                assert_eq!(ca.rows, 1);
2411                assert_eq!(ca.cols, 3);
2412                assert_eq!(ca.data, vec!['A', 'B', 'C']);
2413            }
2414            other => panic!("expected char array, got {other:?}"),
2415        }
2416    }
2417
2418    #[test]
2419    fn arrayfun_gpu_options_are_mode_gated() {
2420        test_support::with_test_provider(|provider| {
2421            let handle = gpu_helpers::upload_tensor(
2422                provider,
2423                &Tensor::new(vec![0.0, 1.0], vec![2, 1]).expect("input"),
2424            )
2425            .expect("upload");
2426            let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
2427            let error = call(
2428                Value::FunctionHandle("sin".to_string()),
2429                vec![
2430                    Value::GpuTensor(handle.clone()),
2431                    Value::from("UniformOutput"),
2432                    Value::Bool(false),
2433                ],
2434            )
2435            .expect_err("gpu options must reject in compatible mode");
2436            assert_eq!(
2437                error.identifier(),
2438                ARRAYFUN_GPU_OPTIONS_EXTENSION.error_identifier
2439            );
2440            let _ = provider.free(&handle);
2441        });
2442    }
2443
2444    #[test]
2445    fn arrayfun_provider_fallback_preserves_every_integer_class() {
2446        test_support::with_test_provider(|provider| {
2447            for (storage, callback) in [
2448                (IntegerStorage::I8(vec![i8::MIN, i8::MAX]), "int8"),
2449                (IntegerStorage::I16(vec![i16::MIN, i16::MAX]), "int16"),
2450                (IntegerStorage::I32(vec![i32::MIN, i32::MAX]), "int32"),
2451                (IntegerStorage::I64(vec![i64::MIN, i64::MAX]), "int64"),
2452                (IntegerStorage::U8(vec![0, u8::MAX]), "uint8"),
2453                (IntegerStorage::U16(vec![0, u16::MAX]), "uint16"),
2454                (IntegerStorage::U32(vec![0, u32::MAX]), "uint32"),
2455                (
2456                    IntegerStorage::U64(vec![9_007_199_254_740_993, u64::MAX]),
2457                    "uint64",
2458                ),
2459            ] {
2460                let input = Tensor::new_integer(storage.clone(), vec![1, 2]).expect("input");
2461                let handle = gpu_helpers::upload_tensor(provider, &input).expect("upload");
2462                let result = call(
2463                    Value::FunctionHandle(callback.to_string()),
2464                    vec![Value::GpuTensor(handle.clone())],
2465                )
2466                .expect("provider arrayfun");
2467                let Value::GpuTensor(output) = result else {
2468                    panic!("expected resident output");
2469                };
2470                let gathered =
2471                    test_support::gather(Value::GpuTensor(output.clone())).expect("gather output");
2472                assert_eq!(gathered.integer_storage(), Some(&storage));
2473                let _ = provider.free(&handle);
2474                let _ = provider.free(&output);
2475            }
2476        });
2477    }
2478
2479    #[test]
2480    fn arrayfun_gpu_overload_uses_documented_compatible_size_expansion_exactly() {
2481        test_support::with_test_provider(|provider| {
2482            let row_storage = IntegerStorage::U64(vec![
2483                9_007_199_254_740_993,
2484                9_007_199_254_740_994,
2485                9_007_199_254_740_995,
2486            ]);
2487            let row = Tensor::new_integer(row_storage, vec![1, 3]).expect("row");
2488            let row_handle = gpu_helpers::upload_tensor(provider, &row).expect("upload row");
2489            let column =
2490                Tensor::new_integer(IntegerStorage::U64(vec![10, 20]), vec![2, 1]).expect("column");
2491            let result = call(
2492                Value::FunctionHandle("plus".to_string()),
2493                vec![Value::GpuTensor(row_handle.clone()), Value::Tensor(column)],
2494            )
2495            .expect("compatible gpu arrayfun");
2496            let Value::GpuTensor(output) = result else {
2497                panic!("expected resident output");
2498            };
2499            let gathered = test_support::gather(Value::GpuTensor(output.clone())).expect("gather");
2500            assert_eq!(gathered.shape, vec![2, 3]);
2501            assert_eq!(
2502                gathered.integer_storage(),
2503                Some(&IntegerStorage::U64(vec![
2504                    9_007_199_254_741_003,
2505                    9_007_199_254_741_013,
2506                    9_007_199_254_741_004,
2507                    9_007_199_254_741_014,
2508                    9_007_199_254_741_005,
2509                    9_007_199_254_741_015,
2510                ]))
2511            );
2512            let _ = provider.free(&row_handle);
2513            let _ = provider.free(&output);
2514        });
2515    }
2516
2517    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2518    #[test]
2519    fn arrayfun_uniform_false_gpu_returns_cell() {
2520        test_support::with_test_provider(|provider| {
2521            let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
2522            let tensor = Tensor::new(vec![0.0, 1.0], vec![2, 1]).unwrap();
2523            let handle = gpu_helpers::upload_tensor(provider, &tensor).expect("upload");
2524            let result = call(
2525                Value::FunctionHandle("sin".to_string()),
2526                vec![
2527                    Value::GpuTensor(handle),
2528                    Value::String("UniformOutput".into()),
2529                    Value::Bool(false),
2530                ],
2531            )
2532            .expect("arrayfun");
2533            match result {
2534                Value::Cell(cell) => {
2535                    assert_eq!(cell.rows, 2);
2536                    assert_eq!(cell.cols, 1);
2537                    let first = cell.get(0, 0).expect("first cell");
2538                    let second = cell.get(1, 0).expect("second cell");
2539                    match (first, second) {
2540                        (Value::Num(a), Value::Num(b)) => {
2541                            assert!((a - 0.0f64.sin()).abs() < 1e-12);
2542                            assert!((b - 1.0f64.sin()).abs() < 1e-12);
2543                        }
2544                        other => panic!("expected numeric cells, got {other:?}"),
2545                    }
2546                }
2547                other => panic!("expected cell, got {other:?}"),
2548            }
2549        });
2550    }
2551
2552    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2553    #[test]
2554    fn arrayfun_gpu_roundtrip() {
2555        test_support::with_test_provider(|provider| {
2556            let tensor = Tensor::new(vec![0.0, 1.0, 2.0, 3.0], vec![4, 1]).unwrap();
2557            let handle = gpu_helpers::upload_tensor(provider, &tensor).expect("upload");
2558            let result = call(
2559                Value::FunctionHandle("sin".to_string()),
2560                vec![Value::GpuTensor(handle)],
2561            )
2562            .expect("arrayfun");
2563            match result {
2564                Value::GpuTensor(gpu) => {
2565                    let gathered = test_support::gather(Value::GpuTensor(gpu.clone())).unwrap();
2566                    let expected: Vec<f64> = values(&tensor).into_iter().map(f64::sin).collect();
2567                    assert_eq!(values(&gathered), expected);
2568                    let _ = provider.free(&gpu);
2569                }
2570                other => panic!("expected gpu tensor, got {other:?}"),
2571            }
2572        });
2573    }
2574
2575    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2576    #[test]
2577    #[cfg(feature = "wgpu")]
2578    fn arrayfun_wgpu_sin_matches_cpu() {
2579        if runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
2580            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
2581        )
2582        .is_err()
2583        {
2584            return;
2585        }
2586        let Some(provider) = runmat_accelerate_api::provider() else {
2587            return;
2588        };
2589
2590        let tensor = Tensor::new(vec![0.0, 1.0, 2.0, 3.0], vec![4, 1]).unwrap();
2591        let handle = gpu_helpers::upload_tensor(provider, &tensor).expect("upload");
2592        let result = call(
2593            Value::FunctionHandle("sin".into()),
2594            vec![Value::GpuTensor(handle.clone())],
2595        )
2596        .expect("arrayfun sin gpu");
2597        let Value::GpuTensor(out_handle) = result else {
2598            panic!("expected GPU tensor result");
2599        };
2600        let gathered = test_support::gather(Value::GpuTensor(out_handle.clone())).unwrap();
2601        let expected: Vec<f64> = values(&tensor).into_iter().map(f64::sin).collect();
2602        assert_eq!(gathered.shape, tensor.shape);
2603        let tol = match provider.precision() {
2604            runmat_accelerate_api::ProviderPrecision::F64 => 1e-12,
2605            runmat_accelerate_api::ProviderPrecision::F32 => 1e-5,
2606        };
2607        for (actual, expect) in values(&gathered).iter().zip(expected.iter()) {
2608            assert!(
2609                (actual - expect).abs() < tol,
2610                "expected {expect}, got {actual}"
2611            );
2612        }
2613        let _ = provider.free(&handle);
2614        let _ = provider.free(&out_handle);
2615    }
2616
2617    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2618    #[test]
2619    #[cfg(feature = "wgpu")]
2620    fn arrayfun_wgpu_plus_matches_cpu() {
2621        if runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
2622            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
2623        )
2624        .is_err()
2625        {
2626            return;
2627        }
2628        let Some(provider) = runmat_accelerate_api::provider() else {
2629            return;
2630        };
2631
2632        let a = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]).unwrap();
2633        let b = Tensor::new(vec![4.0, 3.0, 2.0, 1.0], vec![2, 2]).unwrap();
2634        let handle_a = gpu_helpers::upload_tensor(provider, &a).expect("upload a");
2635        let handle_b = gpu_helpers::upload_tensor(provider, &b).expect("upload b");
2636        let result = call(
2637            Value::FunctionHandle("plus".into()),
2638            vec![
2639                Value::GpuTensor(handle_a.clone()),
2640                Value::GpuTensor(handle_b.clone()),
2641            ],
2642        )
2643        .expect("arrayfun plus gpu");
2644
2645        let Value::GpuTensor(out_handle) = result else {
2646            panic!("expected GPU tensor result");
2647        };
2648        let gathered = test_support::gather(Value::GpuTensor(out_handle.clone())).unwrap();
2649        let expected: Vec<f64> = values(&a)
2650            .iter()
2651            .zip(values(&b).iter())
2652            .map(|(x, y)| x + y)
2653            .collect();
2654        assert_eq!(gathered.shape, a.shape);
2655        let tol = match provider.precision() {
2656            runmat_accelerate_api::ProviderPrecision::F64 => 1e-12,
2657            runmat_accelerate_api::ProviderPrecision::F32 => 1e-5,
2658        };
2659        for (actual, expect) in values(&gathered).iter().zip(expected.iter()) {
2660            assert!(
2661                (actual - expect).abs() < tol,
2662                "expected {expect}, got {actual}"
2663            );
2664        }
2665        let _ = provider.free(&handle_a);
2666        let _ = provider.free(&handle_b);
2667        let _ = provider.free(&out_handle);
2668    }
2669
2670    #[test]
2671    #[cfg(feature = "wgpu")]
2672    fn arrayfun_wgpu_fallback_preserves_every_integer_class() {
2673        let _guard = test_support::accel_test_lock();
2674        if runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
2675            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
2676        )
2677        .is_err()
2678        {
2679            return;
2680        }
2681        let Some(provider) = runmat_accelerate_api::provider() else {
2682            return;
2683        };
2684        for (storage, callback) in [
2685            (IntegerStorage::I8(vec![i8::MIN, i8::MAX]), "int8"),
2686            (IntegerStorage::I16(vec![i16::MIN, i16::MAX]), "int16"),
2687            (IntegerStorage::I32(vec![i32::MIN, i32::MAX]), "int32"),
2688            (IntegerStorage::I64(vec![i64::MIN, i64::MAX]), "int64"),
2689            (IntegerStorage::U8(vec![0, u8::MAX]), "uint8"),
2690            (IntegerStorage::U16(vec![0, u16::MAX]), "uint16"),
2691            (IntegerStorage::U32(vec![0, u32::MAX]), "uint32"),
2692            (
2693                IntegerStorage::U64(vec![9_007_199_254_740_993, u64::MAX]),
2694                "uint64",
2695            ),
2696        ] {
2697            let handle = gpu_helpers::upload_tensor(
2698                provider,
2699                &Tensor::new_integer(storage.clone(), vec![1, 2]).expect("input"),
2700            )
2701            .expect("upload");
2702            let result = call(
2703                Value::FunctionHandle(callback.to_string()),
2704                vec![Value::GpuTensor(handle.clone())],
2705            )
2706            .expect("wgpu arrayfun");
2707            let Value::GpuTensor(output) = result else {
2708                panic!("expected resident output");
2709            };
2710            let gathered = test_support::gather(Value::GpuTensor(output.clone())).expect("gather");
2711            assert_eq!(gathered.integer_storage(), Some(&storage));
2712            let _ = provider.free(&handle);
2713            let _ = provider.free(&output);
2714        }
2715
2716        let row = Tensor::new_integer(
2717            IntegerStorage::U64(vec![
2718                9_007_199_254_740_993,
2719                9_007_199_254_740_994,
2720                9_007_199_254_740_995,
2721            ]),
2722            vec![1, 3],
2723        )
2724        .expect("row");
2725        let row_handle = gpu_helpers::upload_tensor(provider, &row).expect("upload row");
2726        let column =
2727            Tensor::new_integer(IntegerStorage::U64(vec![10, 20]), vec![2, 1]).expect("column");
2728        let result = call(
2729            Value::FunctionHandle("plus".to_string()),
2730            vec![Value::GpuTensor(row_handle.clone()), Value::Tensor(column)],
2731        )
2732        .expect("compatible wgpu arrayfun");
2733        let Value::GpuTensor(output) = result else {
2734            panic!("expected resident output");
2735        };
2736        let gathered = test_support::gather(Value::GpuTensor(output.clone())).expect("gather");
2737        assert_eq!(gathered.shape, vec![2, 3]);
2738        assert_eq!(
2739            gathered.integer_storage(),
2740            Some(&IntegerStorage::U64(vec![
2741                9_007_199_254_741_003,
2742                9_007_199_254_741_013,
2743                9_007_199_254_741_004,
2744                9_007_199_254_741_014,
2745                9_007_199_254_741_005,
2746                9_007_199_254_741_015,
2747            ]))
2748        );
2749        let _ = provider.free(&row_handle);
2750        let _ = provider.free(&output);
2751    }
2752
2753    #[test]
2754    fn arrayfun_metadata_classifies_integer_and_extension_forms() {
2755        assert_eq!(ARRAYFUN_INTEGER_CAPABILITIES.len(), 3);
2756        assert_eq!(
2757            ARRAYFUN_INTEGER_CAPABILITIES[1].output_class,
2758            BuiltinIntegerOutputClassRule::PreserveInput
2759        );
2760        assert_eq!(
2761            ARRAYFUN_INTEGER_CAPABILITIES[2].inputs[0].availability,
2762            BuiltinIntegerInputAvailability::Rejected
2763        );
2764        assert_eq!(
2765            ARRAYFUN_EXTENSIONS,
2766            [
2767                ARRAYFUN_TEXT_CALLABLE_EXTENSION,
2768                ARRAYFUN_HOST_SCALAR_EXPANSION_EXTENSION,
2769                ARRAYFUN_GPU_OPTIONS_EXTENSION
2770            ]
2771        );
2772    }
2773
2774    const ARRAYFUN_TEST_HELPER_ERRORS: [BuiltinErrorDescriptor; 0] = [];
2775    const ARRAYFUN_TEST_HELPER_OUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
2776        name: "out",
2777        ty: BuiltinParamType::Any,
2778        arity: BuiltinParamArity::Required,
2779        default: None,
2780        description: "Helper output value.",
2781    }];
2782    const ARRAYFUN_TEST_HANDLER_INPUTS: [BuiltinParamDescriptor; 3] = [
2783        BuiltinParamDescriptor {
2784            name: "seed",
2785            ty: BuiltinParamType::Any,
2786            arity: BuiltinParamArity::Required,
2787            default: None,
2788            description: "Seed value.",
2789        },
2790        BuiltinParamDescriptor {
2791            name: "err",
2792            ty: BuiltinParamType::Any,
2793            arity: BuiltinParamArity::Required,
2794            default: None,
2795            description: "Error context placeholder.",
2796        },
2797        BuiltinParamDescriptor {
2798            name: "rest",
2799            ty: BuiltinParamType::Any,
2800            arity: BuiltinParamArity::Variadic,
2801            default: None,
2802            description: "Additional values.",
2803        },
2804    ];
2805    const ARRAYFUN_TEST_HANDLER_SIGNATURES: [BuiltinSignatureDescriptor; 1] =
2806        [BuiltinSignatureDescriptor {
2807            label: "out = __arrayfun_test_handler(seed, err, ...)",
2808            inputs: &ARRAYFUN_TEST_HANDLER_INPUTS,
2809            outputs: &ARRAYFUN_TEST_HELPER_OUT,
2810        }];
2811    const ARRAYFUN_TEST_HANDLER_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
2812        signatures: &ARRAYFUN_TEST_HANDLER_SIGNATURES,
2813        output_mode: BuiltinOutputMode::Fixed,
2814        completion_policy: BuiltinCompletionPolicy::HiddenInternal,
2815        errors: &ARRAYFUN_TEST_HELPER_ERRORS,
2816    };
2817
2818    #[runmat_macros::runtime_builtin(
2819        name = "__arrayfun_test_handler",
2820        descriptor(
2821            crate::builtins::acceleration::gpu::arrayfun::tests::ARRAYFUN_TEST_HANDLER_DESCRIPTOR
2822        ),
2823        type_resolver(arrayfun_type),
2824        builtin_path = "crate::builtins::acceleration::gpu::arrayfun::tests"
2825    )]
2826    async fn arrayfun_test_handler(
2827        seed: Value,
2828        _err: Value,
2829        rest: Vec<Value>,
2830    ) -> crate::BuiltinResult<Value> {
2831        let _ = rest;
2832        Ok(seed)
2833    }
2834}