Skip to main content

runmat_runtime/builtins/acceleration/gpu/
arrayfun.rs

1//! MATLAB-compatible `arrayfun` builtin with GPU-aware semantics.
2//!
3//! This implementation supports applying a scalar MATLAB function to every element
4//! of one or more array inputs. When invoked with `gpuArray` inputs the builtin
5//! executes on the host today and uploads the uniform output back to the device so
6//! downstream code continues to see GPU residency. Future provider hooks can swap
7//! in a device kernel without affecting the public API.
8
9use crate::builtins::acceleration::gpu::type_resolvers::arrayfun_type;
10use crate::builtins::common::spec::{
11    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
12    ProviderHook, ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
13};
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, HostTensorView};
19use runmat_builtins::{
20    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
21    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
22    CharArray, Closure, ComplexTensor, LogicalArray, StringArray, Tensor, Value,
23};
24use runmat_macros::runtime_builtin;
25
26#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::acceleration::gpu::arrayfun")]
27pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
28    name: "arrayfun",
29    op_kind: GpuOpKind::Elementwise,
30    supported_precisions: &[ScalarType::F32, ScalarType::F64],
31    broadcast: BroadcastSemantics::Matlab,
32    provider_hooks: &[
33        ProviderHook::Unary { name: "unary_sin" },
34        ProviderHook::Unary { name: "unary_cos" },
35        ProviderHook::Unary { name: "unary_abs" },
36        ProviderHook::Unary { name: "unary_exp" },
37        ProviderHook::Unary { name: "unary_log" },
38        ProviderHook::Unary { name: "unary_sqrt" },
39        ProviderHook::Binary {
40            name: "elem_add",
41            commutative: true,
42        },
43        ProviderHook::Binary {
44            name: "elem_sub",
45            commutative: false,
46        },
47        ProviderHook::Binary {
48            name: "elem_mul",
49            commutative: true,
50        },
51        ProviderHook::Binary {
52            name: "elem_div",
53            commutative: false,
54        },
55    ],
56    constant_strategy: ConstantStrategy::InlineLiteral,
57    residency: ResidencyPolicy::NewHandle,
58    nan_mode: ReductionNaN::Include,
59    two_pass_threshold: None,
60    workgroup_size: None,
61    accepts_nan_mode: false,
62    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.",
63};
64
65#[runmat_macros::register_fusion_spec(
66    builtin_path = "crate::builtins::acceleration::gpu::arrayfun"
67)]
68pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
69    name: "arrayfun",
70    shape: ShapeRequirements::Any,
71    constant_strategy: ConstantStrategy::InlineLiteral,
72    elementwise: None,
73    reduction: None,
74    emits_nan: false,
75    notes: "Acts as a fusion barrier because the callback can run arbitrary MATLAB code.",
76};
77
78const BUILTIN_NAME: &str = "arrayfun";
79
80const ARRAYFUN_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
81    name: "B",
82    ty: BuiltinParamType::Any,
83    arity: BuiltinParamArity::Required,
84    default: None,
85    description: "Element-wise callback result (uniform array or cell array).",
86}];
87
88const ARRAYFUN_INPUTS_BASE: [BuiltinParamDescriptor; 3] = [
89    BuiltinParamDescriptor {
90        name: "func",
91        ty: BuiltinParamType::Any,
92        arity: BuiltinParamArity::Required,
93        default: None,
94        description: "Function handle or callable name.",
95    },
96    BuiltinParamDescriptor {
97        name: "A1",
98        ty: BuiltinParamType::Any,
99        arity: BuiltinParamArity::Required,
100        default: None,
101        description: "First input array.",
102    },
103    BuiltinParamDescriptor {
104        name: "An",
105        ty: BuiltinParamType::Any,
106        arity: BuiltinParamArity::Variadic,
107        default: None,
108        description: "Additional input arrays.",
109    },
110];
111
112const ARRAYFUN_INPUTS_UNIFORM: [BuiltinParamDescriptor; 5] = [
113    BuiltinParamDescriptor {
114        name: "func",
115        ty: BuiltinParamType::Any,
116        arity: BuiltinParamArity::Required,
117        default: None,
118        description: "Function handle or callable name.",
119    },
120    BuiltinParamDescriptor {
121        name: "A1",
122        ty: BuiltinParamType::Any,
123        arity: BuiltinParamArity::Required,
124        default: None,
125        description: "First input array.",
126    },
127    BuiltinParamDescriptor {
128        name: "An",
129        ty: BuiltinParamType::Any,
130        arity: BuiltinParamArity::Variadic,
131        default: None,
132        description: "Additional input arrays.",
133    },
134    BuiltinParamDescriptor {
135        name: "UniformOutput",
136        ty: BuiltinParamType::PropertyName,
137        arity: BuiltinParamArity::Required,
138        default: Some("\"UniformOutput\""),
139        description: "Name-value key that toggles uniform output collection.",
140    },
141    BuiltinParamDescriptor {
142        name: "tf",
143        ty: BuiltinParamType::Any,
144        arity: BuiltinParamArity::Required,
145        default: Some("true"),
146        description: "Logical true/false value for UniformOutput.",
147    },
148];
149
150const ARRAYFUN_INPUTS_HANDLER: [BuiltinParamDescriptor; 5] = [
151    BuiltinParamDescriptor {
152        name: "func",
153        ty: BuiltinParamType::Any,
154        arity: BuiltinParamArity::Required,
155        default: None,
156        description: "Function handle or callable name.",
157    },
158    BuiltinParamDescriptor {
159        name: "A1",
160        ty: BuiltinParamType::Any,
161        arity: BuiltinParamArity::Required,
162        default: None,
163        description: "First input array.",
164    },
165    BuiltinParamDescriptor {
166        name: "An",
167        ty: BuiltinParamType::Any,
168        arity: BuiltinParamArity::Variadic,
169        default: None,
170        description: "Additional input arrays.",
171    },
172    BuiltinParamDescriptor {
173        name: "ErrorHandler",
174        ty: BuiltinParamType::PropertyName,
175        arity: BuiltinParamArity::Required,
176        default: Some("\"ErrorHandler\""),
177        description: "Name-value key that provides fallback callback on per-element failures.",
178    },
179    BuiltinParamDescriptor {
180        name: "handler",
181        ty: BuiltinParamType::Any,
182        arity: BuiltinParamArity::Required,
183        default: None,
184        description: "Callback invoked with error struct and original scalar arguments.",
185    },
186];
187
188const ARRAYFUN_INPUTS_OPTIONS: [BuiltinParamDescriptor; 4] = [
189    BuiltinParamDescriptor {
190        name: "func",
191        ty: BuiltinParamType::Any,
192        arity: BuiltinParamArity::Required,
193        default: None,
194        description: "Function handle or callable name.",
195    },
196    BuiltinParamDescriptor {
197        name: "A1",
198        ty: BuiltinParamType::Any,
199        arity: BuiltinParamArity::Required,
200        default: None,
201        description: "First input array.",
202    },
203    BuiltinParamDescriptor {
204        name: "An",
205        ty: BuiltinParamType::Any,
206        arity: BuiltinParamArity::Variadic,
207        default: None,
208        description: "Additional input arrays.",
209    },
210    BuiltinParamDescriptor {
211        name: "nameValue",
212        ty: BuiltinParamType::Any,
213        arity: BuiltinParamArity::Variadic,
214        default: None,
215        description: "Name-value option pairs including UniformOutput and ErrorHandler.",
216    },
217];
218
219const ARRAYFUN_SIGNATURES: [BuiltinSignatureDescriptor; 4] = [
220    BuiltinSignatureDescriptor {
221        label: "B = arrayfun(func, A1, An...)",
222        inputs: &ARRAYFUN_INPUTS_BASE,
223        outputs: &ARRAYFUN_OUTPUT,
224    },
225    BuiltinSignatureDescriptor {
226        label: "B = arrayfun(func, A1, An..., \"UniformOutput\", tf)",
227        inputs: &ARRAYFUN_INPUTS_UNIFORM,
228        outputs: &ARRAYFUN_OUTPUT,
229    },
230    BuiltinSignatureDescriptor {
231        label: "B = arrayfun(func, A1, An..., \"ErrorHandler\", handler)",
232        inputs: &ARRAYFUN_INPUTS_HANDLER,
233        outputs: &ARRAYFUN_OUTPUT,
234    },
235    BuiltinSignatureDescriptor {
236        label: "B = arrayfun(func, A1, An..., nameValue...)",
237        inputs: &ARRAYFUN_INPUTS_OPTIONS,
238        outputs: &ARRAYFUN_OUTPUT,
239    },
240];
241
242const ARRAYFUN_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
243    code: "RM.ARRAYFUN.INVALID_INPUT",
244    identifier: Some("RunMat:arrayfun:InvalidInput"),
245    when: "Inputs, callable forms, or option tails violate arrayfun argument requirements.",
246    message: "arrayfun: invalid input arguments",
247};
248
249const ARRAYFUN_ERROR_UNIFORM_OUTPUT_OPTION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
250    code: "RM.ARRAYFUN.UNIFORM_OUTPUT_OPTION",
251    identifier: Some("RunMat:arrayfun:UniformOutputOption"),
252    when: "UniformOutput option value is not interpretable as logical true/false.",
253    message: "arrayfun: UniformOutput must be logical true or false",
254};
255
256const ARRAYFUN_ERROR_CALLBACK_FAILED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
257    code: "RM.ARRAYFUN.CALLBACK_FAILED",
258    identifier: Some("RunMat:arrayfun:CallbackFailed"),
259    when: "Callback invocation fails and no ErrorHandler recovers the element.",
260    message: "arrayfun: callback execution failed",
261};
262
263const ARRAYFUN_ERROR_UNIFORM_OUTPUT_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
264    code: "RM.ARRAYFUN.UNIFORM_OUTPUT_TYPE",
265    identifier: Some("RunMat:arrayfun:UniformOutputType"),
266    when: "UniformOutput=true callback result is not a supported scalar type.",
267    message: "arrayfun: callback must return scalar values for UniformOutput=true",
268};
269
270const ARRAYFUN_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
271    code: "RM.ARRAYFUN.INTERNAL",
272    identifier: Some("RunMat:arrayfun:InternalError"),
273    when: "Internal shape/index/materialization/upload path fails.",
274    message: "arrayfun: internal error",
275};
276
277const ARRAYFUN_ERROR_UNDEFINED_FUNCTION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
278    code: "RM.ARRAYFUN.UNDEFINED_FUNCTION",
279    identifier: Some("RunMat:UndefinedFunction"),
280    when: "External callable identity cannot be resolved in semantic/runtime boundaries.",
281    message: "arrayfun: undefined function",
282};
283
284const ARRAYFUN_ERRORS: [BuiltinErrorDescriptor; 6] = [
285    ARRAYFUN_ERROR_INVALID_INPUT,
286    ARRAYFUN_ERROR_UNIFORM_OUTPUT_OPTION,
287    ARRAYFUN_ERROR_CALLBACK_FAILED,
288    ARRAYFUN_ERROR_UNIFORM_OUTPUT_TYPE,
289    ARRAYFUN_ERROR_INTERNAL,
290    ARRAYFUN_ERROR_UNDEFINED_FUNCTION,
291];
292
293pub const ARRAYFUN_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
294    signatures: &ARRAYFUN_SIGNATURES,
295    output_mode: BuiltinOutputMode::Fixed,
296    completion_policy: BuiltinCompletionPolicy::Public,
297    errors: &ARRAYFUN_ERRORS,
298};
299
300fn arrayfun_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
301    arrayfun_error_with_message(error.message, error)
302}
303
304fn arrayfun_error_with_message(
305    message: impl Into<String>,
306    error: &'static BuiltinErrorDescriptor,
307) -> RuntimeError {
308    let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
309    if let Some(identifier) = error.identifier {
310        builder = builder.with_identifier(identifier);
311    }
312    builder.build()
313}
314
315fn arrayfun_error_with_detail(
316    error: &'static BuiltinErrorDescriptor,
317    detail: impl AsRef<str>,
318) -> RuntimeError {
319    arrayfun_error_with_message(format!("{}: {}", error.message, detail.as_ref()), error)
320}
321
322fn arrayfun_error_with_source(
323    message: impl Into<String>,
324    error: &'static BuiltinErrorDescriptor,
325    source: RuntimeError,
326) -> RuntimeError {
327    let identifier = source.identifier().map(str::to_string);
328    let mut builder = build_runtime_error(message.into())
329        .with_builtin(BUILTIN_NAME)
330        .with_source(source);
331    if let Some(identifier) = identifier.as_deref().or(error.identifier) {
332        builder = builder.with_identifier(identifier);
333    }
334    builder.build()
335}
336
337fn arrayfun_flow(message: impl Into<String>) -> RuntimeError {
338    arrayfun_error_with_message(message, &ARRAYFUN_ERROR_INVALID_INPUT)
339}
340
341fn arrayfun_internal(message: impl Into<String>) -> RuntimeError {
342    arrayfun_error_with_message(message, &ARRAYFUN_ERROR_INTERNAL)
343}
344
345fn arrayfun_flow_with_source(message: impl Into<String>, source: RuntimeError) -> RuntimeError {
346    arrayfun_error_with_source(message, &ARRAYFUN_ERROR_CALLBACK_FAILED, source)
347}
348
349fn format_handler_error(err: &RuntimeError) -> String {
350    if let Some(identifier) = err.identifier() {
351        if err.message().is_empty() {
352            return identifier.to_string();
353        }
354        if err.message().starts_with(identifier) {
355            return err.message().to_string();
356        }
357        return format!("{identifier}: {}", err.message());
358    }
359    err.message().to_string()
360}
361
362#[runtime_builtin(
363    name = "arrayfun",
364    category = "acceleration/gpu",
365    summary = "Apply a function element-wise across array inputs.",
366    keywords = "arrayfun,gpu,array,map,functional",
367    accel = "host",
368    type_resolver(arrayfun_type),
369    descriptor(crate::builtins::acceleration::gpu::arrayfun::ARRAYFUN_DESCRIPTOR),
370    builtin_path = "crate::builtins::acceleration::gpu::arrayfun"
371)]
372async fn arrayfun_builtin(func: Value, mut rest: Vec<Value>) -> crate::BuiltinResult<Value> {
373    let callable = Callable::from_function(func)?;
374
375    let mut uniform_output = true;
376    let mut error_handler: Option<Callable> = None;
377
378    while rest.len() >= 2 {
379        let key_candidate = rest[rest.len() - 2].clone();
380        let Some(name) = extract_string(&key_candidate) else {
381            break;
382        };
383        let value = rest.pop().expect("value present");
384        rest.pop();
385        match name.trim().to_ascii_lowercase().as_str() {
386            "uniformoutput" => uniform_output = parse_uniform_output(value)?,
387            "errorhandler" => error_handler = Some(Callable::from_function(value)?),
388            other => {
389                return Err(arrayfun_flow(format!(
390                    "arrayfun: unknown name-value argument '{other}'"
391                )))
392            }
393        }
394    }
395
396    if rest.is_empty() {
397        return Err(arrayfun_flow("arrayfun: expected at least one input array"));
398    }
399
400    let inputs_snapshot = rest.clone();
401    let has_gpu_input = inputs_snapshot
402        .iter()
403        .any(|value| matches!(value, Value::GpuTensor(_)));
404    let gpu_device_id = inputs_snapshot.iter().find_map(|v| {
405        if let Value::GpuTensor(h) = v {
406            Some(h.device_id)
407        } else {
408            None
409        }
410    });
411
412    if uniform_output {
413        if let Some(gpu_result) =
414            try_gpu_fast_path(&callable, &inputs_snapshot, error_handler.as_ref()).await?
415        {
416            return Ok(gpu_result);
417        }
418    }
419
420    let mut inputs: Vec<ArrayInput> = Vec::with_capacity(rest.len());
421    let mut base_shape: Vec<usize> = Vec::new();
422    let mut base_len: Option<usize> = None;
423
424    for (idx, raw) in rest.into_iter().enumerate() {
425        if matches!(raw, Value::Cell(_)) {
426            return Err(arrayfun_flow(
427                "arrayfun: cell inputs are not supported (use cellfun instead)",
428            ));
429        }
430        if matches!(raw, Value::Struct(_)) {
431            return Err(arrayfun_flow("arrayfun: struct inputs are not supported"));
432        }
433
434        let host_value = gather_if_needed_async(&raw).await?;
435        let data = ArrayData::from_value(host_value)?;
436        let len = data.len();
437        let is_scalar = len == 1;
438
439        let mut input = ArrayInput { data, is_scalar };
440
441        if let Some(current) = base_len {
442            if current == len {
443                if len > 1 {
444                    let shape = input.shape_vec();
445                    if shape != base_shape {
446                        return Err(arrayfun_flow(format!(
447                            "arrayfun: input {} does not match the size of the first array",
448                            idx + 1
449                        )));
450                    }
451                }
452            } else if len == 1 {
453                input.is_scalar = true;
454            } else if current == 1 {
455                base_len = Some(len);
456                base_shape = input.shape_vec();
457                for prior in &mut inputs {
458                    let prior_len = prior.len();
459                    if prior_len == len {
460                        if prior.shape_vec() != base_shape {
461                            return Err(arrayfun_flow(format!(
462                                "arrayfun: input {} does not match the size of the first array",
463                                idx
464                            )));
465                        }
466                    } else if prior_len == 1 {
467                        prior.is_scalar = true;
468                    } else if prior_len == 0 && len == 0 {
469                        continue;
470                    } else {
471                        return Err(arrayfun_flow(format!(
472                            "arrayfun: input {} does not match the size of the first array",
473                            idx
474                        )));
475                    }
476                }
477            } else if len == 0 && current == 0 {
478                let shape = input.shape_vec();
479                if shape != base_shape {
480                    return Err(arrayfun_flow(format!(
481                        "arrayfun: input {} does not match the size of the first array",
482                        idx + 1
483                    )));
484                }
485            } else {
486                return Err(arrayfun_flow(format!(
487                    "arrayfun: input {} does not match the size of the first array",
488                    idx + 1
489                )));
490            }
491        } else {
492            base_len = Some(len);
493            base_shape = input.shape_vec();
494        }
495
496        inputs.push(input);
497    }
498
499    let total_len = base_len.unwrap_or(0);
500
501    if total_len == 0 {
502        if uniform_output {
503            return Ok(empty_uniform(&base_shape));
504        } else {
505            return make_cell_with_shape(Vec::new(), base_shape)
506                .map_err(|e| arrayfun_flow(format!("arrayfun: {e}")));
507        }
508    }
509
510    let mut collector = if uniform_output {
511        Some(UniformCollector::Pending)
512    } else {
513        None
514    };
515
516    let mut cell_outputs: Vec<Value> = Vec::new();
517    let mut args: Vec<Value> = Vec::with_capacity(inputs.len());
518
519    for idx in 0..total_len {
520        args.clear();
521        for input in &inputs {
522            args.push(input.value_at(idx)?);
523        }
524
525        let result = match callable.call(&args).await {
526            Ok(value) => value,
527            Err(err) => {
528                let handler = match error_handler.as_ref() {
529                    Some(handler) => handler,
530                    None => {
531                        return Err(arrayfun_flow_with_source(
532                            format!("arrayfun: {}", err.message()),
533                            err,
534                        ))
535                    }
536                };
537                let err_message = format_handler_error(&err);
538                let err_value = make_error_struct(&err_message, idx, &base_shape)?;
539                let mut handler_args = Vec::with_capacity(1 + args.len());
540                handler_args.push(err_value);
541                handler_args.extend(args.clone());
542                handler.call(&handler_args).await?
543            }
544        };
545
546        let host_result = gather_if_needed_async(&result).await?;
547
548        if let Some(collector) = collector.as_mut() {
549            collector.push(&host_result)?;
550        } else {
551            cell_outputs.push(host_result);
552        }
553    }
554
555    if let Some(collector) = collector {
556        let uniform = collector.finish(&base_shape)?;
557        maybe_upload_uniform(uniform, has_gpu_input, gpu_device_id)
558    } else {
559        make_cell_with_shape(cell_outputs, base_shape)
560            .map_err(|e| arrayfun_flow(format!("arrayfun: {e}")))
561    }
562}
563
564fn maybe_upload_uniform(
565    value: Value,
566    has_gpu_input: bool,
567    gpu_device_id: Option<u32>,
568) -> BuiltinResult<Value> {
569    if !has_gpu_input {
570        return Ok(value);
571    }
572    let _ = gpu_device_id; // may be used only in cfg(test)
573    let provider = match runmat_accelerate_api::provider() {
574        Some(p) => p,
575        None => return Ok(value),
576    };
577
578    match value {
579        Value::Tensor(tensor) => {
580            let view = HostTensorView {
581                data: &tensor.data,
582                shape: &tensor.shape,
583            };
584            let handle = provider
585                .upload(&view)
586                .map_err(|e| arrayfun_flow(format!("arrayfun: {e}")))?;
587            Ok(Value::GpuTensor(handle))
588        }
589        Value::LogicalArray(logical) => {
590            let data: Vec<f64> = logical
591                .data
592                .iter()
593                .map(|&bit| if bit != 0 { 1.0 } else { 0.0 })
594                .collect();
595            let tensor = Tensor::new(data, logical.shape.clone())
596                .map_err(|e| arrayfun_flow(format!("arrayfun: {e}")))?;
597            let view = HostTensorView {
598                data: &tensor.data,
599                shape: &tensor.shape,
600            };
601            let handle = provider
602                .upload(&view)
603                .map_err(|e| arrayfun_flow(format!("arrayfun: {e}")))?;
604            set_handle_logical(&handle, true);
605            Ok(Value::GpuTensor(handle))
606        }
607        other => Ok(other),
608    }
609}
610
611fn empty_uniform(shape: &[usize]) -> Value {
612    if shape.is_empty() {
613        return Value::Tensor(Tensor::zeros(vec![0, 0]));
614    }
615    let total: usize = shape.iter().product();
616    let tensor = Tensor::new(vec![0.0; total], shape.to_vec())
617        .unwrap_or_else(|_| Tensor::zeros(shape.to_vec()));
618    Value::Tensor(tensor)
619}
620
621fn parse_uniform_output(value: Value) -> BuiltinResult<bool> {
622    match value {
623        Value::Bool(b) => Ok(b),
624        Value::Num(n) => Ok(n != 0.0),
625        Value::Int(iv) => Ok(iv.to_f64() != 0.0),
626        Value::String(s) => parse_bool_string(&s)
627            .ok_or_else(|| arrayfun_error(&ARRAYFUN_ERROR_UNIFORM_OUTPUT_OPTION)),
628        Value::CharArray(ca) if ca.rows == 1 => {
629            let text: String = ca.data.iter().collect();
630            parse_bool_string(&text)
631                .ok_or_else(|| arrayfun_error(&ARRAYFUN_ERROR_UNIFORM_OUTPUT_OPTION))
632        }
633        other => Err(arrayfun_error_with_detail(
634            &ARRAYFUN_ERROR_UNIFORM_OUTPUT_OPTION,
635            format!("got {other:?}"),
636        )),
637    }
638}
639
640fn parse_bool_string(value: &str) -> Option<bool> {
641    match value.trim().to_ascii_lowercase().as_str() {
642        "true" | "on" => Some(true),
643        "false" | "off" => Some(false),
644        _ => None,
645    }
646}
647
648fn extract_string(value: &Value) -> Option<String> {
649    match value {
650        Value::String(s) => Some(s.clone()),
651        Value::CharArray(ca) if ca.rows == 1 => Some(ca.data.iter().collect()),
652        Value::StringArray(sa) if sa.data.len() == 1 => Some(sa.data[0].clone()),
653        _ => None,
654    }
655}
656
657struct ArrayInput {
658    data: ArrayData,
659    is_scalar: bool,
660}
661
662impl ArrayInput {
663    fn len(&self) -> usize {
664        self.data.len()
665    }
666
667    fn shape_vec(&self) -> Vec<usize> {
668        self.data.shape_vec()
669    }
670
671    fn value_at(&self, idx: usize) -> BuiltinResult<Value> {
672        if self.is_scalar {
673            self.data.value_at(0)
674        } else {
675            self.data.value_at(idx)
676        }
677    }
678}
679
680enum ArrayData {
681    Tensor(Tensor),
682    Logical(LogicalArray),
683    Complex(ComplexTensor),
684    Char(CharArray),
685    String(StringArray),
686    Scalar(Value),
687}
688
689impl ArrayData {
690    fn from_value(value: Value) -> BuiltinResult<Self> {
691        match value {
692            Value::Tensor(t) => Ok(ArrayData::Tensor(t)),
693            Value::LogicalArray(l) => Ok(ArrayData::Logical(l)),
694            Value::ComplexTensor(c) => Ok(ArrayData::Complex(c)),
695            Value::CharArray(ca) => Ok(ArrayData::Char(ca)),
696            Value::StringArray(sa) => Ok(ArrayData::String(sa)),
697            Value::Num(_)
698            | Value::Bool(_)
699            | Value::Int(_)
700            | Value::Complex(_, _)
701            | Value::String(_) => {
702                Ok(ArrayData::Scalar(value))
703            }
704            other => Err(arrayfun_flow(format!(
705                "arrayfun: unsupported input type {other:?} (expected numeric, logical, complex, char, or string arrays)"
706            ))),
707        }
708    }
709
710    fn len(&self) -> usize {
711        match self {
712            ArrayData::Tensor(t) => t.data.len(),
713            ArrayData::Logical(l) => l.data.len(),
714            ArrayData::Complex(c) => c.data.len(),
715            ArrayData::Char(ca) => ca.rows * ca.cols,
716            ArrayData::String(sa) => sa.data.len(),
717            ArrayData::Scalar(_) => 1,
718        }
719    }
720
721    fn shape_vec(&self) -> Vec<usize> {
722        match self {
723            ArrayData::Tensor(t) => {
724                if t.shape.is_empty() {
725                    vec![1, 1]
726                } else {
727                    t.shape.clone()
728                }
729            }
730            ArrayData::Logical(l) => {
731                if l.shape.is_empty() {
732                    vec![1, 1]
733                } else {
734                    l.shape.clone()
735                }
736            }
737            ArrayData::Complex(c) => {
738                if c.shape.is_empty() {
739                    vec![1, 1]
740                } else {
741                    c.shape.clone()
742                }
743            }
744            ArrayData::Char(ca) => vec![ca.rows, ca.cols],
745            ArrayData::String(sa) => {
746                if sa.shape.is_empty() {
747                    vec![1, 1]
748                } else {
749                    sa.shape.clone()
750                }
751            }
752            ArrayData::Scalar(_) => vec![1, 1],
753        }
754    }
755
756    fn value_at(&self, idx: usize) -> BuiltinResult<Value> {
757        match self {
758            ArrayData::Tensor(t) => {
759                Ok(Value::Num(*t.data.get(idx).ok_or_else(|| {
760                    arrayfun_flow("arrayfun: index out of bounds")
761                })?))
762            }
763            ArrayData::Logical(l) => Ok(Value::Bool(
764                *l.data
765                    .get(idx)
766                    .ok_or_else(|| arrayfun_flow("arrayfun: index out of bounds"))?
767                    != 0,
768            )),
769            ArrayData::Complex(c) => {
770                let (re, im) = c
771                    .data
772                    .get(idx)
773                    .ok_or_else(|| arrayfun_flow("arrayfun: index out of bounds"))?;
774                Ok(Value::Complex(*re, *im))
775            }
776            ArrayData::Char(ca) => {
777                if ca.rows == 0 || ca.cols == 0 {
778                    return Ok(Value::CharArray(
779                        CharArray::new(Vec::new(), 0, 0)
780                            .map_err(|e| arrayfun_flow(format!("arrayfun: {e}")))?,
781                    ));
782                }
783                let rows = ca.rows;
784                let cols = ca.cols;
785                let row = idx % rows;
786                let col = idx / rows;
787                let data_idx = row * cols + col;
788                let ch = *ca
789                    .data
790                    .get(data_idx)
791                    .ok_or_else(|| arrayfun_flow("arrayfun: index out of bounds"))?;
792                let char_array = CharArray::new(vec![ch], 1, 1)
793                    .map_err(|e| arrayfun_flow(format!("arrayfun: {e}")))?;
794                Ok(Value::CharArray(char_array))
795            }
796            ArrayData::String(sa) => {
797                Ok(Value::String(sa.data.get(idx).cloned().ok_or_else(
798                    || arrayfun_flow("arrayfun: index out of bounds"),
799                )?))
800            }
801            ArrayData::Scalar(v) => Ok(v.clone()),
802        }
803    }
804}
805
806#[derive(Clone)]
807enum Callable {
808    Builtin { name: String },
809    ExternalName { name: String },
810    Closure(Closure),
811}
812
813impl Callable {
814    fn resolved_semantic_handle(name: &str) -> Option<Self> {
815        let function = user_functions::resolve_semantic_function_by_name(name)?;
816        Some(Callable::Closure(Closure {
817            function_name: name.to_string(),
818            bound_function: Some(function),
819            captures: Vec::new(),
820        }))
821    }
822
823    fn from_function(value: Value) -> BuiltinResult<Self> {
824        match value {
825            Value::String(text) => Self::from_text(&text),
826            Value::CharArray(ca) => {
827                if ca.rows != 1 {
828                    Err(arrayfun_flow(
829                        "arrayfun: function name must be a character vector or string scalar",
830                    ))
831                } else {
832                    let text: String = ca.data.iter().collect();
833                    Self::from_text(&text)
834                }
835            }
836            Value::StringArray(sa) if sa.data.len() == 1 => Self::from_text(&sa.data[0]),
837            Value::FunctionHandle(name) => Self::from_text(&name),
838            Value::ExternalFunctionHandle(name) => {
839                if let Some(callable) = Self::resolved_semantic_handle(&name) {
840                    Ok(callable)
841                } else if crate::is_well_formed_qualified_name(&name) {
842                    Ok(Callable::ExternalName { name })
843                } else {
844                    Ok(Callable::Builtin { name })
845                }
846            }
847            Value::BoundFunctionHandle { name, function } => Ok(Callable::Closure(Closure {
848                function_name: name,
849                bound_function: Some(function),
850                captures: Vec::new(),
851            })),
852            Value::Closure(mut closure) => {
853                if closure.bound_function.is_none() {
854                    if let Some(function) =
855                        user_functions::resolve_semantic_function_by_name(&closure.function_name)
856                    {
857                        closure.bound_function = Some(function);
858                    }
859                }
860                Ok(Callable::Closure(closure))
861            }
862            Value::Num(_) | Value::Int(_) | Value::Bool(_) => Err(arrayfun_flow(
863                "arrayfun: expected function handle or builtin name, not a scalar value",
864            )),
865            other => Err(arrayfun_flow(format!(
866                "arrayfun: expected function handle or builtin name, got {other:?}"
867            ))),
868        }
869    }
870
871    fn from_text(text: &str) -> BuiltinResult<Self> {
872        let trimmed = text.trim();
873        if trimmed.is_empty() {
874            return Err(arrayfun_flow(
875                "arrayfun: expected function handle or builtin name, got empty string",
876            ));
877        }
878        if let Some(rest) = trimmed.strip_prefix('@') {
879            let name = rest.trim();
880            if name.is_empty() {
881                Err(arrayfun_flow("arrayfun: empty function handle"))
882            } else {
883                if let Some(callable) = Self::resolved_semantic_handle(name) {
884                    return Ok(callable);
885                }
886                if crate::is_well_formed_qualified_name(name) {
887                    return Ok(Callable::ExternalName {
888                        name: name.to_string(),
889                    });
890                }
891                Ok(Callable::Builtin {
892                    name: name.to_string(),
893                })
894            }
895        } else {
896            let name = trimmed.to_ascii_lowercase();
897            if let Some(callable) = Self::resolved_semantic_handle(&name) {
898                return Ok(callable);
899            }
900            if crate::is_well_formed_qualified_name(&name) {
901                return Ok(Callable::ExternalName { name });
902            }
903            Ok(Callable::Builtin { name })
904        }
905    }
906
907    fn builtin_name(&self) -> Option<&str> {
908        match self {
909            Callable::Builtin { name } => Some(name.as_str()),
910            Callable::ExternalName { .. } | Callable::Closure(_) => None,
911        }
912    }
913
914    async fn call(&self, args: &[Value]) -> crate::BuiltinResult<Value> {
915        match self {
916            Callable::Builtin { name } => {
917                let request = user_functions::CallableRequest::resolved(
918                    runmat_hir::CallableIdentity::DynamicName(runmat_hir::SymbolName(name.clone())),
919                    runmat_hir::CallableFallbackPolicy::RuntimeNameResolution,
920                    args.to_vec(),
921                    1,
922                );
923                if let Some(result) = user_functions::try_call_semantic_descriptor(request).await {
924                    return result;
925                }
926                crate::call_builtin_async(name, args).await
927            }
928            Callable::ExternalName { name } => {
929                let identity = crate::external_callable_identity_for_name(name);
930                let request = user_functions::CallableRequest::resolved(
931                    identity.clone(),
932                    runmat_hir::CallableFallbackPolicy::ExternalBoundary,
933                    args.to_vec(),
934                    1,
935                );
936                if let Some(result) = user_functions::try_call_semantic_descriptor(request).await {
937                    return result;
938                }
939                Err(arrayfun_error_with_message(
940                    format!("Undefined function for callable identity {identity:?}"),
941                    &ARRAYFUN_ERROR_UNDEFINED_FUNCTION,
942                ))
943            }
944            Callable::Closure(c) => {
945                let mut merged = c.captures.clone();
946                merged.extend_from_slice(args);
947                if let Some(function) = c.bound_function {
948                    let request =
949                        user_functions::CallableRequest::semantic(function, merged.clone(), 1);
950                    if let Some(result) =
951                        user_functions::try_call_semantic_descriptor(request).await
952                    {
953                        return result;
954                    }
955                    return Err(arrayfun_error_with_detail(
956                        &ARRAYFUN_ERROR_CALLBACK_FAILED,
957                        format!(
958                            "semantic closure '{}' ({function}) is unavailable",
959                            c.function_name
960                        ),
961                    ));
962                }
963                if let Some(function) =
964                    user_functions::resolve_semantic_function_by_name(&c.function_name)
965                {
966                    let request =
967                        user_functions::CallableRequest::semantic(function, merged.clone(), 1);
968                    if let Some(result) =
969                        user_functions::try_call_semantic_descriptor(request).await
970                    {
971                        return result;
972                    }
973                }
974                crate::call_builtin_async(&c.function_name, &merged).await
975            }
976        }
977    }
978}
979
980async fn try_gpu_fast_path(
981    callable: &Callable,
982    inputs: &[Value],
983    error_handler: Option<&Callable>,
984) -> BuiltinResult<Option<Value>> {
985    if inputs.is_empty() || error_handler.is_some() {
986        return Ok(None);
987    }
988    if !inputs
989        .iter()
990        .all(|value| matches!(value, Value::GpuTensor(_)))
991    {
992        return Ok(None);
993    }
994
995    let provider = match runmat_accelerate_api::provider() {
996        Some(p) => p,
997        None => return Ok(None),
998    };
999
1000    let Some(name_raw) = callable.builtin_name() else {
1001        return Ok(None);
1002    };
1003    let name = name_raw.to_ascii_lowercase();
1004
1005    let mut handles: Vec<GpuTensorHandle> = Vec::with_capacity(inputs.len());
1006    for value in inputs {
1007        if let Value::GpuTensor(handle) = value {
1008            handles.push(handle.clone());
1009        }
1010    }
1011
1012    if handles.len() >= 2 {
1013        let base_shape = handles[0].shape.clone();
1014        if handles
1015            .iter()
1016            .skip(1)
1017            .any(|handle| handle.shape != base_shape)
1018        {
1019            return Ok(None);
1020        }
1021    }
1022
1023    let result = match name.as_str() {
1024        "sin" if handles.len() == 1 => provider.unary_sin(&handles[0]).await,
1025        "cos" if handles.len() == 1 => provider.unary_cos(&handles[0]).await,
1026        "abs" if handles.len() == 1 => provider.unary_abs(&handles[0]).await,
1027        "exp" if handles.len() == 1 => provider.unary_exp(&handles[0]).await,
1028        "log" if handles.len() == 1 => provider.unary_log(&handles[0]).await,
1029        "sqrt" if handles.len() == 1 => provider.unary_sqrt(&handles[0]).await,
1030        "plus" if handles.len() == 2 => provider.elem_add(&handles[0], &handles[1]).await,
1031        "minus" if handles.len() == 2 => provider.elem_sub(&handles[0], &handles[1]).await,
1032        "times" if handles.len() == 2 => provider.elem_mul(&handles[0], &handles[1]).await,
1033        "rdivide" if handles.len() == 2 => provider.elem_div(&handles[0], &handles[1]).await,
1034        "ldivide" if handles.len() == 2 => provider.elem_div(&handles[1], &handles[0]).await,
1035        _ => return Ok(None),
1036    };
1037
1038    match result {
1039        Ok(handle) => Ok(Some(Value::GpuTensor(handle))),
1040        Err(_) => Ok(None),
1041    }
1042}
1043
1044enum UniformCollector {
1045    Pending,
1046    Double(Vec<f64>),
1047    Logical(Vec<u8>),
1048    Complex(Vec<(f64, f64)>),
1049    Char(Vec<char>),
1050}
1051
1052impl UniformCollector {
1053    fn push(&mut self, value: &Value) -> BuiltinResult<()> {
1054        match self {
1055            UniformCollector::Pending => match classify_value(value)? {
1056                ClassifiedValue::Logical(b) => {
1057                    *self = UniformCollector::Logical(vec![b as u8]);
1058                    Ok(())
1059                }
1060                ClassifiedValue::Double(d) => {
1061                    *self = UniformCollector::Double(vec![d]);
1062                    Ok(())
1063                }
1064                ClassifiedValue::Complex(c) => {
1065                    *self = UniformCollector::Complex(vec![c]);
1066                    Ok(())
1067                }
1068                ClassifiedValue::Char(ch) => {
1069                    *self = UniformCollector::Char(vec![ch]);
1070                    Ok(())
1071                }
1072            },
1073            UniformCollector::Logical(bits) => match classify_value(value)? {
1074                ClassifiedValue::Logical(b) => {
1075                    bits.push(b as u8);
1076                    Ok(())
1077                }
1078                ClassifiedValue::Double(d) => {
1079                    let mut data: Vec<f64> = bits
1080                        .iter()
1081                        .map(|&bit| if bit != 0 { 1.0 } else { 0.0 })
1082                        .collect();
1083                    data.push(d);
1084                    *self = UniformCollector::Double(data);
1085                    Ok(())
1086                }
1087                ClassifiedValue::Complex(c) => {
1088                    let mut data: Vec<(f64, f64)> = bits
1089                        .iter()
1090                        .map(|&bit| if bit != 0 { (1.0, 0.0) } else { (0.0, 0.0) })
1091                        .collect();
1092                    data.push(c);
1093                    *self = UniformCollector::Complex(data);
1094                    Ok(())
1095                }
1096                ClassifiedValue::Char(ch) => {
1097                    let mut data: Vec<f64> = bits
1098                        .iter()
1099                        .map(|&bit| if bit != 0 { 1.0 } else { 0.0 })
1100                        .collect();
1101                    data.push(ch as u32 as f64);
1102                    *self = UniformCollector::Double(data);
1103                    Ok(())
1104                }
1105            },
1106            UniformCollector::Double(data) => match classify_value(value)? {
1107                ClassifiedValue::Logical(b) => {
1108                    data.push(if b { 1.0 } else { 0.0 });
1109                    Ok(())
1110                }
1111                ClassifiedValue::Double(d) => {
1112                    data.push(d);
1113                    Ok(())
1114                }
1115                ClassifiedValue::Complex(c) => {
1116                    let promoted: Vec<(f64, f64)> = data.iter().map(|&v| (v, 0.0)).collect();
1117                    let mut complex = promoted;
1118                    complex.push(c);
1119                    *self = UniformCollector::Complex(complex);
1120                    Ok(())
1121                }
1122                ClassifiedValue::Char(ch) => {
1123                    data.push(ch as u32 as f64);
1124                    Ok(())
1125                }
1126            },
1127            UniformCollector::Complex(data) => match classify_value(value)? {
1128                ClassifiedValue::Logical(b) => {
1129                    data.push((if b { 1.0 } else { 0.0 }, 0.0));
1130                    Ok(())
1131                }
1132                ClassifiedValue::Double(d) => {
1133                    data.push((d, 0.0));
1134                    Ok(())
1135                }
1136                ClassifiedValue::Complex(c) => {
1137                    data.push(c);
1138                    Ok(())
1139                }
1140                ClassifiedValue::Char(ch) => {
1141                    data.push((ch as u32 as f64, 0.0));
1142                    Ok(())
1143                }
1144            },
1145            UniformCollector::Char(chars) => match classify_value(value)? {
1146                ClassifiedValue::Char(ch) => {
1147                    chars.push(ch);
1148                    Ok(())
1149                }
1150                ClassifiedValue::Logical(b) => {
1151                    let mut data: Vec<f64> = chars.iter().map(|&ch| ch as u32 as f64).collect();
1152                    data.push(if b { 1.0 } else { 0.0 });
1153                    *self = UniformCollector::Double(data);
1154                    Ok(())
1155                }
1156                ClassifiedValue::Double(d) => {
1157                    let mut data: Vec<f64> = chars.iter().map(|&ch| ch as u32 as f64).collect();
1158                    data.push(d);
1159                    *self = UniformCollector::Double(data);
1160                    Ok(())
1161                }
1162                ClassifiedValue::Complex(c) => {
1163                    let mut promoted: Vec<(f64, f64)> =
1164                        chars.iter().map(|&ch| (ch as u32 as f64, 0.0)).collect();
1165                    promoted.push(c);
1166                    *self = UniformCollector::Complex(promoted);
1167                    Ok(())
1168                }
1169            },
1170        }
1171    }
1172
1173    fn finish(self, shape: &[usize]) -> BuiltinResult<Value> {
1174        match self {
1175            UniformCollector::Pending => {
1176                let total = shape.iter().product();
1177                let tensor = Tensor::new(vec![0.0; total], shape.to_vec())
1178                    .map_err(|e| arrayfun_internal(format!("arrayfun: {e}")))?;
1179                Ok(Value::Tensor(tensor))
1180            }
1181            UniformCollector::Double(data) => {
1182                let tensor = Tensor::new(data, shape.to_vec())
1183                    .map_err(|e| arrayfun_internal(format!("arrayfun: {e}")))?;
1184                Ok(Value::Tensor(tensor))
1185            }
1186            UniformCollector::Logical(bits) => {
1187                let logical = LogicalArray::new(bits, shape.to_vec())
1188                    .map_err(|e| arrayfun_internal(format!("arrayfun: {e}")))?;
1189                Ok(Value::LogicalArray(logical))
1190            }
1191            UniformCollector::Complex(entries) => {
1192                let tensor = ComplexTensor::new(entries, shape.to_vec())
1193                    .map_err(|e| arrayfun_internal(format!("arrayfun: {e}")))?;
1194                Ok(Value::ComplexTensor(tensor))
1195            }
1196            UniformCollector::Char(chars) => {
1197                let normalized_shape = if shape.is_empty() {
1198                    vec![1, 1]
1199                } else {
1200                    shape.to_vec()
1201                };
1202
1203                if normalized_shape.len() > 2 {
1204                    return Err(arrayfun_error_with_detail(
1205                        &ARRAYFUN_ERROR_UNIFORM_OUTPUT_TYPE,
1206                        "character outputs with UniformOutput=true must be 2-D",
1207                    ));
1208                }
1209
1210                let rows = normalized_shape.first().copied().unwrap_or(1);
1211                let cols = normalized_shape.get(1).copied().unwrap_or(1);
1212                let expected = rows.checked_mul(cols).ok_or_else(|| {
1213                    arrayfun_internal("arrayfun: character output size exceeds platform limits")
1214                })?;
1215
1216                if expected != chars.len() {
1217                    return Err(arrayfun_error_with_detail(
1218                        &ARRAYFUN_ERROR_UNIFORM_OUTPUT_TYPE,
1219                        "callback returned the wrong number of characters",
1220                    ));
1221                }
1222
1223                let mut row_major = vec!['\0'; expected];
1224                for col in 0..cols {
1225                    for row in 0..rows {
1226                        let col_major_idx = row + col * rows;
1227                        let row_major_idx = row * cols + col;
1228                        row_major[row_major_idx] = chars[col_major_idx];
1229                    }
1230                }
1231
1232                let array = CharArray::new(row_major, rows, cols)
1233                    .map_err(|e| arrayfun_internal(format!("arrayfun: {e}")))?;
1234                Ok(Value::CharArray(array))
1235            }
1236        }
1237    }
1238}
1239
1240enum ClassifiedValue {
1241    Logical(bool),
1242    Double(f64),
1243    Complex((f64, f64)),
1244    Char(char),
1245}
1246
1247fn classify_value(value: &Value) -> BuiltinResult<ClassifiedValue> {
1248    match value {
1249        Value::Bool(b) => Ok(ClassifiedValue::Logical(*b)),
1250        Value::LogicalArray(la) if la.len() == 1 => Ok(ClassifiedValue::Logical(la.data[0] != 0)),
1251        Value::Int(i) => Ok(ClassifiedValue::Double(i.to_f64())),
1252        Value::Num(n) => Ok(ClassifiedValue::Double(*n)),
1253        Value::Tensor(t) if t.data.len() == 1 => Ok(ClassifiedValue::Double(t.data[0])),
1254        Value::Complex(re, im) => Ok(ClassifiedValue::Complex((*re, *im))),
1255        Value::ComplexTensor(t) if t.data.len() == 1 => Ok(ClassifiedValue::Complex(t.data[0])),
1256        Value::CharArray(ca) if ca.rows * ca.cols == 1 => {
1257            let ch = ca.data.first().copied().unwrap_or('\0');
1258            Ok(ClassifiedValue::Char(ch))
1259        }
1260        other => Err(arrayfun_error_with_detail(
1261            &ARRAYFUN_ERROR_UNIFORM_OUTPUT_TYPE,
1262            format!(
1263                "callback must return scalar numeric, logical, character, or complex values for UniformOutput=true (got {other:?})"
1264            ),
1265        )),
1266    }
1267}
1268
1269fn make_error_struct(
1270    raw_error: &str,
1271    linear_index: usize,
1272    shape: &[usize],
1273) -> BuiltinResult<Value> {
1274    let (identifier, message) = split_error_message(raw_error);
1275    let mut st = runmat_builtins::StructValue::new();
1276    st.fields
1277        .insert("identifier".to_string(), Value::String(identifier));
1278    st.fields
1279        .insert("message".to_string(), Value::String(message));
1280    st.fields
1281        .insert("index".to_string(), Value::Num((linear_index + 1) as f64));
1282    let subs = linear_to_indices(linear_index, shape);
1283    let subs_tensor = dims_to_row_tensor(&subs)?;
1284    st.fields
1285        .insert("indices".to_string(), Value::Tensor(subs_tensor));
1286    Ok(Value::Struct(st))
1287}
1288
1289fn split_error_message(raw: &str) -> (String, String) {
1290    let trimmed = raw.trim();
1291    let mut indices = trimmed.match_indices(':');
1292    if let Some((_, _)) = indices.next() {
1293        if let Some((second_idx, _)) = indices.next() {
1294            let identifier = trimmed[..second_idx].trim().to_string();
1295            let message = trimmed[second_idx + 1..].trim().to_string();
1296            if !identifier.is_empty() && identifier.contains(':') {
1297                return (
1298                    identifier,
1299                    if message.is_empty() {
1300                        trimmed.to_string()
1301                    } else {
1302                        message
1303                    },
1304                );
1305            }
1306        } else if trimmed.len() >= 7
1307            && (trimmed[..7].eq_ignore_ascii_case("matlab:")
1308                || trimmed[..7].eq_ignore_ascii_case("runmat:"))
1309        {
1310            return (trimmed.to_string(), String::new());
1311        }
1312    }
1313    (
1314        "RunMat:arrayfun:FunctionError".to_string(),
1315        trimmed.to_string(),
1316    )
1317}
1318
1319fn linear_to_indices(mut index: usize, shape: &[usize]) -> Vec<usize> {
1320    if shape.is_empty() {
1321        return vec![1];
1322    }
1323    let mut subs = Vec::with_capacity(shape.len());
1324    for &dim in shape {
1325        if dim == 0 {
1326            subs.push(1);
1327            continue;
1328        }
1329        let coord = (index % dim) + 1;
1330        subs.push(coord);
1331        index /= dim;
1332    }
1333    subs
1334}
1335
1336fn dims_to_row_tensor(dims: &[usize]) -> BuiltinResult<Tensor> {
1337    let data: Vec<f64> = dims.iter().map(|&d| d as f64).collect();
1338    Tensor::new(data, vec![1, dims.len()]).map_err(|e| arrayfun_internal(format!("arrayfun: {e}")))
1339}
1340
1341#[cfg(test)]
1342pub(crate) mod tests {
1343    use super::*;
1344    use crate::builtins::common::test_support;
1345    use futures::executor::block_on;
1346    use runmat_accelerate_api::HostTensorView;
1347    use runmat_builtins::{ResolveContext, Tensor, Type};
1348    use std::sync::Arc;
1349
1350    fn call(func: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
1351        block_on(arrayfun_builtin(func, rest))
1352    }
1353
1354    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1355    #[test]
1356    fn arrayfun_basic_sin() {
1357        let tensor = Tensor::new(vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0], vec![2, 3]).unwrap();
1358        let expected: Vec<f64> = tensor.data.iter().map(|&x| x.sin()).collect();
1359        let result = call(
1360            Value::FunctionHandle("sin".to_string()),
1361            vec![Value::Tensor(tensor.clone())],
1362        )
1363        .expect("arrayfun");
1364        match result {
1365            Value::Tensor(out) => {
1366                assert_eq!(out.shape, vec![2, 3]);
1367                assert_eq!(out.data, expected);
1368            }
1369            other => panic!("expected tensor, got {other:?}"),
1370        }
1371    }
1372
1373    #[test]
1374    fn arrayfun_semantic_function_handle_uses_semantic_invoker() {
1375        let _guard = crate::user_functions::install_semantic_function_invoker(Some(Arc::new(
1376            |function, args, requested_outputs| {
1377                assert_eq!(function, 78);
1378                assert_eq!(requested_outputs, 1);
1379                let [Value::Num(value)] = args else {
1380                    panic!("expected scalar numeric argument, got {args:?}");
1381                };
1382                let value = *value;
1383                Box::pin(async move { Ok(Value::Num(value + 10.0)) })
1384            },
1385        )));
1386        let tensor = Tensor::new(vec![1.0, 2.0], vec![1, 2]).expect("tensor");
1387        let handle = Value::BoundFunctionHandle {
1388            name: "arrayfun_target".to_string(),
1389            function: 78,
1390        };
1391
1392        let result = call(handle, vec![Value::Tensor(tensor)]).expect("semantic arrayfun");
1393        match result {
1394            Value::Tensor(out) => {
1395                assert_eq!(out.shape, vec![1, 2]);
1396                assert_eq!(out.data, vec![11.0, 12.0]);
1397            }
1398            other => panic!("expected tensor, got {other:?}"),
1399        }
1400    }
1401
1402    #[test]
1403    fn arrayfun_name_only_callback_uses_semantic_resolver() {
1404        let _resolver_guard =
1405            crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
1406                (name == "resolved_arrayfun_target").then_some(80)
1407            })));
1408        let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
1409            Arc::new(|function, args, requested_outputs| {
1410                assert_eq!(function, 80);
1411                assert_eq!(requested_outputs, 1);
1412                let [Value::Num(value)] = args else {
1413                    panic!("expected scalar numeric argument, got {args:?}");
1414                };
1415                let value = *value;
1416                Box::pin(async move { Ok(Value::Num(value + 20.0)) })
1417            }),
1418        ));
1419        let tensor = Tensor::new(vec![1.0, 2.0], vec![1, 2]).expect("tensor");
1420
1421        let result = call(
1422            Value::String("resolved_arrayfun_target".to_string()),
1423            vec![Value::Tensor(tensor)],
1424        )
1425        .expect("resolved name-only arrayfun");
1426        match result {
1427            Value::Tensor(out) => {
1428                assert_eq!(out.shape, vec![1, 2]);
1429                assert_eq!(out.data, vec![21.0, 22.0]);
1430            }
1431            other => panic!("expected tensor, got {other:?}"),
1432        }
1433    }
1434
1435    #[test]
1436    fn arrayfun_qualified_text_callback_classifies_as_external_name() {
1437        let callable =
1438            Callable::from_text("pkg.callback").expect("qualified arrayfun callback should parse");
1439        assert!(matches!(
1440            callable,
1441            Callable::ExternalName { name } if name == "pkg.callback"
1442        ));
1443    }
1444
1445    #[test]
1446    fn arrayfun_external_handle_uses_semantic_resolver() {
1447        let _resolver_guard =
1448            crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
1449                (name == "pkg.callback").then_some(87)
1450            })));
1451        let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
1452            Arc::new(|function, args, requested_outputs| {
1453                assert_eq!(function, 87);
1454                assert_eq!(requested_outputs, 1);
1455                let [Value::Num(value)] = args else {
1456                    panic!("expected scalar numeric argument, got {args:?}");
1457                };
1458                let value = *value;
1459                Box::pin(async move { Ok(Value::Num(value + 30.0)) })
1460            }),
1461        ));
1462        let tensor = Tensor::new(vec![1.0, 2.0], vec![1, 2]).expect("tensor");
1463
1464        let result = call(
1465            Value::ExternalFunctionHandle("pkg.callback".to_string()),
1466            vec![Value::Tensor(tensor)],
1467        )
1468        .expect("resolved external-handle arrayfun");
1469        match result {
1470            Value::Tensor(out) => {
1471                assert_eq!(out.shape, vec![1, 2]);
1472                assert_eq!(out.data, vec![31.0, 32.0]);
1473            }
1474            other => panic!("expected tensor, got {other:?}"),
1475        }
1476    }
1477
1478    #[test]
1479    fn arrayfun_single_segment_external_handle_uses_runtime_name_resolution() {
1480        let _resolver_guard =
1481            crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
1482                (name == "callback").then_some(887)
1483            })));
1484        let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
1485            Arc::new(|function, args, requested_outputs| {
1486                assert_eq!(function, 887);
1487                assert_eq!(requested_outputs, 1);
1488                let [Value::Num(value)] = args else {
1489                    panic!("expected scalar numeric argument, got {args:?}");
1490                };
1491                let value = *value;
1492                Box::pin(async move { Ok(Value::Num(value + 40.0)) })
1493            }),
1494        ));
1495        let tensor = Tensor::new(vec![1.0, 2.0], vec![1, 2]).expect("tensor");
1496
1497        let result = call(
1498            Value::ExternalFunctionHandle("callback".to_string()),
1499            vec![Value::Tensor(tensor)],
1500        )
1501        .expect("single-segment external-handle arrayfun should resolve via runtime-name policy");
1502        match result {
1503            Value::Tensor(out) => {
1504                assert_eq!(out.shape, vec![1, 2]);
1505                assert_eq!(out.data, vec![41.0, 42.0]);
1506            }
1507            other => panic!("expected tensor, got {other:?}"),
1508        }
1509    }
1510
1511    #[test]
1512    fn arrayfun_external_handle_prefers_semantic_handle_binding_when_resolved() {
1513        let _resolver_guard =
1514            crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
1515                (name == "pkg.callback").then_some(87)
1516            })));
1517        let callable =
1518            Callable::from_function(Value::ExternalFunctionHandle("pkg.callback".to_string()))
1519                .expect("external handle should parse");
1520        assert!(matches!(
1521            callable,
1522            Callable::Closure(Closure {
1523                function_name,
1524                bound_function: Some(87),
1525                ..
1526            }) if function_name == "pkg.callback"
1527        ));
1528    }
1529
1530    #[test]
1531    fn arrayfun_name_only_closure_prefers_semantic_handle_binding_when_resolved() {
1532        let _resolver_guard =
1533            crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
1534                (name == "pkg.callback").then_some(187)
1535            })));
1536        let callable = Callable::from_function(Value::Closure(Closure {
1537            function_name: "pkg.callback".to_string(),
1538            bound_function: None,
1539            captures: vec![Value::Num(5.0)],
1540        }))
1541        .expect("closure callback should parse");
1542        assert!(matches!(
1543            callable,
1544            Callable::Closure(Closure {
1545                function_name,
1546                bound_function: Some(187),
1547                captures
1548            }) if function_name == "pkg.callback" && captures == vec![Value::Num(5.0)]
1549        ));
1550    }
1551
1552    #[test]
1553    fn arrayfun_name_only_closure_call_uses_semantic_resolver_when_unbound() {
1554        let _resolver_guard =
1555            crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
1556                (name == "pkg.callback").then_some(287)
1557            })));
1558        let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
1559            Arc::new(|function, args, requested_outputs| {
1560                assert_eq!(function, 287);
1561                assert_eq!(requested_outputs, 1);
1562                assert_eq!(args, &[Value::Num(5.0), Value::Num(4.0)]);
1563                Box::pin(async { Ok(Value::Num(9.0)) })
1564            }),
1565        ));
1566        let callable = Callable::Closure(Closure {
1567            function_name: "pkg.callback".to_string(),
1568            bound_function: None,
1569            captures: vec![Value::Num(5.0)],
1570        });
1571        let value = block_on(callable.call(&[Value::Num(4.0)])).expect("closure call");
1572        assert_eq!(value, Value::Num(9.0));
1573    }
1574
1575    #[test]
1576    fn arrayfun_external_handle_errors_as_undefined_when_unresolved() {
1577        let _resolver_guard =
1578            crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|_| None)));
1579        let tensor = Tensor::new(vec![1.0], vec![1, 1]).expect("tensor");
1580
1581        let err = call(
1582            Value::ExternalFunctionHandle("pkg.callback".to_string()),
1583            vec![Value::Tensor(tensor)],
1584        )
1585        .expect_err("unresolved external callback should error");
1586        assert_eq!(
1587            err.identifier(),
1588            ARRAYFUN_ERROR_UNDEFINED_FUNCTION.identifier,
1589            "unexpected error: {}",
1590            err.message()
1591        );
1592        assert!(
1593            err.message().contains("ExternalName(QualifiedName"),
1594            "unexpected error: {err:?}"
1595        );
1596        assert!(
1597            !err.message().contains("Undefined function 'pkg.callback'"),
1598            "well-formed external callback should report typed identity: {err:?}"
1599        );
1600    }
1601
1602    #[test]
1603    fn arrayfun_malformed_external_handle_errors_as_undefined_when_unresolved() {
1604        let _resolver_guard =
1605            crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|_| None)));
1606        let tensor = Tensor::new(vec![1.0], vec![1, 1]).expect("tensor");
1607
1608        let err = call(
1609            Value::ExternalFunctionHandle("pkg..callback".to_string()),
1610            vec![Value::Tensor(tensor)],
1611        )
1612        .expect_err("malformed unresolved external callback should error");
1613        assert_eq!(
1614            err.identifier(),
1615            ARRAYFUN_ERROR_UNDEFINED_FUNCTION.identifier,
1616            "unexpected error: {}",
1617            err.message()
1618        );
1619        assert!(
1620            err.message().contains("pkg..callback"),
1621            "unexpected error: {err:?}"
1622        );
1623    }
1624
1625    #[test]
1626    fn arrayfun_type_tracks_function_returns() {
1627        let func = Type::Function {
1628            params: vec![Type::Num],
1629            returns: Box::new(Type::Num),
1630        };
1631        assert_eq!(
1632            arrayfun_type(&[func, Type::tensor()], &ResolveContext::new(Vec::new())),
1633            Type::tensor()
1634        );
1635    }
1636
1637    #[test]
1638    fn arrayfun_type_uses_logical_returns() {
1639        let func = Type::Function {
1640            params: vec![Type::Num],
1641            returns: Box::new(Type::Bool),
1642        };
1643        assert_eq!(
1644            arrayfun_type(&[func, Type::tensor()], &ResolveContext::new(Vec::new())),
1645            Type::logical()
1646        );
1647    }
1648
1649    #[test]
1650    fn arrayfun_type_with_text_args_stays_unknown() {
1651        let func = Type::Function {
1652            params: vec![Type::Num],
1653            returns: Box::new(Type::Num),
1654        };
1655        assert_eq!(
1656            arrayfun_type(
1657                &[func, Type::tensor(), Type::String, Type::Bool],
1658                &ResolveContext::new(Vec::new()),
1659            ),
1660            Type::Unknown
1661        );
1662    }
1663
1664    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1665    #[test]
1666    fn arrayfun_additional_scalar_argument() {
1667        let tensor = Tensor::new(vec![0.5, 1.0, -1.0], vec![3, 1]).unwrap();
1668        let expected: Vec<f64> = tensor.data.iter().map(|&y| y.atan2(1.0)).collect();
1669        let result = call(
1670            Value::FunctionHandle("atan2".to_string()),
1671            vec![Value::Tensor(tensor), Value::Num(1.0)],
1672        )
1673        .expect("arrayfun");
1674        match result {
1675            Value::Tensor(out) => {
1676                assert_eq!(out.data, expected);
1677            }
1678            other => panic!("expected tensor, got {other:?}"),
1679        }
1680    }
1681
1682    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1683    #[test]
1684    fn arrayfun_uniform_false_returns_cell() {
1685        let tensor = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
1686        let expected: Vec<Value> = tensor.data.iter().map(|&x| Value::Num(x.sin())).collect();
1687        let result = call(
1688            Value::FunctionHandle("sin".to_string()),
1689            vec![
1690                Value::Tensor(tensor),
1691                Value::String("UniformOutput".into()),
1692                Value::Bool(false),
1693            ],
1694        )
1695        .expect("arrayfun");
1696        let Value::Cell(cell) = result else {
1697            panic!("expected cell, got something else");
1698        };
1699        assert_eq!(cell.rows, 2);
1700        assert_eq!(cell.cols, 1);
1701        for (row, value) in expected.iter().enumerate() {
1702            assert_eq!(cell.get(row, 0).unwrap(), *value);
1703        }
1704    }
1705
1706    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1707    #[test]
1708    fn arrayfun_uniform_output_option_identifier() {
1709        let tensor = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
1710        let err = call(
1711            Value::FunctionHandle("sin".to_string()),
1712            vec![
1713                Value::Tensor(tensor),
1714                Value::String("UniformOutput".into()),
1715                Value::String("maybe".into()),
1716            ],
1717        )
1718        .expect_err("expected invalid uniform output option");
1719        assert_eq!(
1720            err.identifier(),
1721            ARRAYFUN_ERROR_UNIFORM_OUTPUT_OPTION.identifier
1722        );
1723    }
1724
1725    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1726    #[test]
1727    fn arrayfun_unknown_name_value_identifier() {
1728        let tensor = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
1729        let err = call(
1730            Value::FunctionHandle("sin".to_string()),
1731            vec![
1732                Value::Tensor(tensor),
1733                Value::String("MysteryFlag".into()),
1734                Value::Bool(true),
1735            ],
1736        )
1737        .expect_err("expected unknown name-value error");
1738        assert_eq!(err.identifier(), ARRAYFUN_ERROR_INVALID_INPUT.identifier);
1739    }
1740
1741    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1742    #[test]
1743    fn arrayfun_size_mismatch_errors() {
1744        let taller = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
1745        let shorter = Tensor::new(vec![4.0, 5.0], vec![2, 1]).unwrap();
1746        let err = call(
1747            Value::FunctionHandle("sin".to_string()),
1748            vec![Value::Tensor(taller), Value::Tensor(shorter)],
1749        )
1750        .expect_err("expected size mismatch error");
1751        let err = err.to_string();
1752        assert!(
1753            err.contains("does not match"),
1754            "expected size mismatch error, got {err}"
1755        );
1756    }
1757
1758    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1759    #[test]
1760    fn arrayfun_error_handler_recovers() {
1761        let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
1762        let handler = Value::Closure(Closure {
1763            function_name: "__arrayfun_test_handler".into(),
1764            bound_function: None,
1765            captures: vec![Value::Num(42.0)],
1766        });
1767        let result = call(
1768            Value::String("@nonexistent_builtin".into()),
1769            vec![
1770                Value::Tensor(tensor),
1771                Value::String("ErrorHandler".into()),
1772                handler,
1773            ],
1774        )
1775        .expect("arrayfun error handler");
1776        match result {
1777            Value::Tensor(out) => {
1778                assert_eq!(out.shape, vec![3, 1]);
1779                assert_eq!(out.data, vec![42.0, 42.0, 42.0]);
1780            }
1781            other => panic!("expected tensor, got {other:?}"),
1782        }
1783    }
1784
1785    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1786    #[test]
1787    fn arrayfun_error_without_handler_propagates_identifier() {
1788        let tensor = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
1789        let err = call(
1790            Value::String("@nonexistent_builtin".into()),
1791            vec![Value::Tensor(tensor)],
1792        )
1793        .expect_err("expected unresolved function error");
1794        assert_eq!(
1795            err.identifier(),
1796            ARRAYFUN_ERROR_UNDEFINED_FUNCTION.identifier,
1797            "unexpected error: {}",
1798            err.message()
1799        );
1800    }
1801
1802    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1803    #[test]
1804    fn arrayfun_uniform_logical_result() {
1805        let tensor = Tensor::new(vec![1.0, f64::NAN, 0.0, f64::INFINITY], vec![4, 1]).unwrap();
1806        let result = call(
1807            Value::FunctionHandle("isfinite".to_string()),
1808            vec![Value::Tensor(tensor)],
1809        )
1810        .expect("arrayfun isfinite");
1811        match result {
1812            Value::LogicalArray(la) => {
1813                assert_eq!(la.shape, vec![4, 1]);
1814                assert_eq!(la.data, vec![1, 0, 1, 0]);
1815            }
1816            other => panic!("expected logical array, got {other:?}"),
1817        }
1818    }
1819
1820    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1821    #[test]
1822    fn arrayfun_uniform_character_result() {
1823        let tensor = Tensor::new(vec![65.0, 66.0, 67.0], vec![1, 3]).unwrap();
1824        let result = call(
1825            Value::FunctionHandle("char".to_string()),
1826            vec![Value::Tensor(tensor)],
1827        )
1828        .expect("arrayfun char");
1829        match result {
1830            Value::CharArray(ca) => {
1831                assert_eq!(ca.rows, 1);
1832                assert_eq!(ca.cols, 3);
1833                assert_eq!(ca.data, vec!['A', 'B', 'C']);
1834            }
1835            other => panic!("expected char array, got {other:?}"),
1836        }
1837    }
1838
1839    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1840    #[test]
1841    fn arrayfun_uniform_false_gpu_returns_cell() {
1842        test_support::with_test_provider(|provider| {
1843            let tensor = Tensor::new(vec![0.0, 1.0], vec![2, 1]).unwrap();
1844            let view = HostTensorView {
1845                data: &tensor.data,
1846                shape: &tensor.shape,
1847            };
1848            let handle = provider.upload(&view).expect("upload");
1849            let result = call(
1850                Value::FunctionHandle("sin".to_string()),
1851                vec![
1852                    Value::GpuTensor(handle),
1853                    Value::String("UniformOutput".into()),
1854                    Value::Bool(false),
1855                ],
1856            )
1857            .expect("arrayfun");
1858            match result {
1859                Value::Cell(cell) => {
1860                    assert_eq!(cell.rows, 2);
1861                    assert_eq!(cell.cols, 1);
1862                    let first = cell.get(0, 0).expect("first cell");
1863                    let second = cell.get(1, 0).expect("second cell");
1864                    match (first, second) {
1865                        (Value::Num(a), Value::Num(b)) => {
1866                            assert!((a - 0.0f64.sin()).abs() < 1e-12);
1867                            assert!((b - 1.0f64.sin()).abs() < 1e-12);
1868                        }
1869                        other => panic!("expected numeric cells, got {other:?}"),
1870                    }
1871                }
1872                other => panic!("expected cell, got {other:?}"),
1873            }
1874        });
1875    }
1876
1877    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1878    #[test]
1879    fn arrayfun_gpu_roundtrip() {
1880        test_support::with_test_provider(|provider| {
1881            let tensor = Tensor::new(vec![0.0, 1.0, 2.0, 3.0], vec![4, 1]).unwrap();
1882            let view = HostTensorView {
1883                data: &tensor.data,
1884                shape: &tensor.shape,
1885            };
1886            let handle = provider.upload(&view).expect("upload");
1887            let result = call(
1888                Value::FunctionHandle("sin".to_string()),
1889                vec![Value::GpuTensor(handle)],
1890            )
1891            .expect("arrayfun");
1892            match result {
1893                Value::GpuTensor(gpu) => {
1894                    let gathered = test_support::gather(Value::GpuTensor(gpu.clone())).unwrap();
1895                    let expected: Vec<f64> = tensor.data.iter().map(|&x| x.sin()).collect();
1896                    assert_eq!(gathered.data, expected);
1897                    let _ = provider.free(&gpu);
1898                }
1899                other => panic!("expected gpu tensor, got {other:?}"),
1900            }
1901        });
1902    }
1903
1904    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1905    #[test]
1906    #[cfg(feature = "wgpu")]
1907    fn arrayfun_wgpu_sin_matches_cpu() {
1908        let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
1909            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
1910        );
1911        let provider = runmat_accelerate_api::provider().expect("wgpu provider");
1912
1913        let tensor = Tensor::new(vec![0.0, 1.0, 2.0, 3.0], vec![4, 1]).unwrap();
1914        let view = HostTensorView {
1915            data: &tensor.data,
1916            shape: &tensor.shape,
1917        };
1918        let handle = provider.upload(&view).expect("upload");
1919        let result = call(
1920            Value::FunctionHandle("sin".into()),
1921            vec![Value::GpuTensor(handle.clone())],
1922        )
1923        .expect("arrayfun sin gpu");
1924        let Value::GpuTensor(out_handle) = result else {
1925            panic!("expected GPU tensor result");
1926        };
1927        let gathered = test_support::gather(Value::GpuTensor(out_handle.clone())).unwrap();
1928        let expected: Vec<f64> = tensor.data.iter().map(|v| v.sin()).collect();
1929        assert_eq!(gathered.shape, tensor.shape);
1930        let tol = match provider.precision() {
1931            runmat_accelerate_api::ProviderPrecision::F64 => 1e-12,
1932            runmat_accelerate_api::ProviderPrecision::F32 => 1e-5,
1933        };
1934        for (actual, expect) in gathered.data.iter().zip(expected.iter()) {
1935            assert!(
1936                (actual - expect).abs() < tol,
1937                "expected {expect}, got {actual}"
1938            );
1939        }
1940        let _ = provider.free(&handle);
1941        let _ = provider.free(&out_handle);
1942    }
1943
1944    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1945    #[test]
1946    #[cfg(feature = "wgpu")]
1947    fn arrayfun_wgpu_plus_matches_cpu() {
1948        let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
1949            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
1950        );
1951        let provider = runmat_accelerate_api::provider().expect("wgpu provider");
1952
1953        let a = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]).unwrap();
1954        let b = Tensor::new(vec![4.0, 3.0, 2.0, 1.0], vec![2, 2]).unwrap();
1955        let view_a = HostTensorView {
1956            data: &a.data,
1957            shape: &a.shape,
1958        };
1959        let view_b = HostTensorView {
1960            data: &b.data,
1961            shape: &b.shape,
1962        };
1963        let handle_a = provider.upload(&view_a).expect("upload a");
1964        let handle_b = provider.upload(&view_b).expect("upload b");
1965        let result = call(
1966            Value::FunctionHandle("plus".into()),
1967            vec![
1968                Value::GpuTensor(handle_a.clone()),
1969                Value::GpuTensor(handle_b.clone()),
1970            ],
1971        )
1972        .expect("arrayfun plus gpu");
1973
1974        let Value::GpuTensor(out_handle) = result else {
1975            panic!("expected GPU tensor result");
1976        };
1977        let gathered = test_support::gather(Value::GpuTensor(out_handle.clone())).unwrap();
1978        let expected: Vec<f64> = a
1979            .data
1980            .iter()
1981            .zip(b.data.iter())
1982            .map(|(x, y)| x + y)
1983            .collect();
1984        assert_eq!(gathered.shape, a.shape);
1985        let tol = match provider.precision() {
1986            runmat_accelerate_api::ProviderPrecision::F64 => 1e-12,
1987            runmat_accelerate_api::ProviderPrecision::F32 => 1e-5,
1988        };
1989        for (actual, expect) in gathered.data.iter().zip(expected.iter()) {
1990            assert!(
1991                (actual - expect).abs() < tol,
1992                "expected {expect}, got {actual}"
1993            );
1994        }
1995        let _ = provider.free(&handle_a);
1996        let _ = provider.free(&handle_b);
1997        let _ = provider.free(&out_handle);
1998    }
1999
2000    const ARRAYFUN_TEST_HELPER_ERRORS: [BuiltinErrorDescriptor; 0] = [];
2001    const ARRAYFUN_TEST_HELPER_OUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
2002        name: "out",
2003        ty: BuiltinParamType::Any,
2004        arity: BuiltinParamArity::Required,
2005        default: None,
2006        description: "Helper output value.",
2007    }];
2008    const ARRAYFUN_TEST_HANDLER_INPUTS: [BuiltinParamDescriptor; 3] = [
2009        BuiltinParamDescriptor {
2010            name: "seed",
2011            ty: BuiltinParamType::Any,
2012            arity: BuiltinParamArity::Required,
2013            default: None,
2014            description: "Seed value.",
2015        },
2016        BuiltinParamDescriptor {
2017            name: "err",
2018            ty: BuiltinParamType::Any,
2019            arity: BuiltinParamArity::Required,
2020            default: None,
2021            description: "Error context placeholder.",
2022        },
2023        BuiltinParamDescriptor {
2024            name: "rest",
2025            ty: BuiltinParamType::Any,
2026            arity: BuiltinParamArity::Variadic,
2027            default: None,
2028            description: "Additional values.",
2029        },
2030    ];
2031    const ARRAYFUN_TEST_HANDLER_SIGNATURES: [BuiltinSignatureDescriptor; 1] =
2032        [BuiltinSignatureDescriptor {
2033            label: "out = __arrayfun_test_handler(seed, err, ...)",
2034            inputs: &ARRAYFUN_TEST_HANDLER_INPUTS,
2035            outputs: &ARRAYFUN_TEST_HELPER_OUT,
2036        }];
2037    const ARRAYFUN_TEST_HANDLER_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
2038        signatures: &ARRAYFUN_TEST_HANDLER_SIGNATURES,
2039        output_mode: BuiltinOutputMode::Fixed,
2040        completion_policy: BuiltinCompletionPolicy::HiddenInternal,
2041        errors: &ARRAYFUN_TEST_HELPER_ERRORS,
2042    };
2043
2044    #[runmat_macros::runtime_builtin(
2045        name = "__arrayfun_test_handler",
2046        descriptor(
2047            crate::builtins::acceleration::gpu::arrayfun::tests::ARRAYFUN_TEST_HANDLER_DESCRIPTOR
2048        ),
2049        type_resolver(arrayfun_type),
2050        builtin_path = "crate::builtins::acceleration::gpu::arrayfun::tests"
2051    )]
2052    async fn arrayfun_test_handler(
2053        seed: Value,
2054        _err: Value,
2055        rest: Vec<Value>,
2056    ) -> crate::BuiltinResult<Value> {
2057        let _ = rest;
2058        Ok(seed)
2059    }
2060}