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