1use 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; 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}