1use crate::builtins::common::spec::{
4 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
5 ReductionNaN, ResidencyPolicy, ShapeRequirements,
6};
7use crate::builtins::structs::type_resolvers::structfun_type;
8use crate::{
9 build_runtime_error, call_feval_async_with_outputs, current_requested_outputs,
10 gather_if_needed_async, BuiltinResult, RuntimeError,
11};
12use runmat_builtins::{
13 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
14 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
15 ComplexTensor, LogicalArray, StringArray, StructValue, Tensor, Value,
16};
17use runmat_macros::runtime_builtin;
18
19#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::structs::core::structfun")]
20pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
21 name: "structfun",
22 op_kind: GpuOpKind::Custom("host-struct-map"),
23 supported_precisions: &[],
24 broadcast: BroadcastSemantics::None,
25 provider_hooks: &[],
26 constant_strategy: ConstantStrategy::InlineLiteral,
27 residency: ResidencyPolicy::GatherImmediately,
28 nan_mode: ReductionNaN::Include,
29 two_pass_threshold: None,
30 workgroup_size: None,
31 accepts_nan_mode: false,
32 notes: "Executes callbacks on the host through the cellfun mapping engine; GPU-resident field values are gathered before callback invocation.",
33};
34
35#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::structs::core::structfun")]
36pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
37 name: "structfun",
38 shape: ShapeRequirements::Any,
39 constant_strategy: ConstantStrategy::InlineLiteral,
40 elementwise: None,
41 reduction: None,
42 emits_nan: true,
43 notes: "Callback execution happens on the host; fusion planners should treat structfun as a fusion barrier.",
44};
45
46const BUILTIN_NAME: &str = "structfun";
47
48const STRUCTFUN_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
49 name: "A",
50 ty: BuiltinParamType::Any,
51 arity: BuiltinParamArity::Required,
52 default: None,
53 description: "Mapped callback outputs.",
54}];
55
56const STRUCTFUN_INPUTS: [BuiltinParamDescriptor; 2] = [
57 BuiltinParamDescriptor {
58 name: "func",
59 ty: BuiltinParamType::Any,
60 arity: BuiltinParamArity::Required,
61 default: None,
62 description: "Function handle or builtin/function name.",
63 },
64 BuiltinParamDescriptor {
65 name: "S",
66 ty: BuiltinParamType::Any,
67 arity: BuiltinParamArity::Required,
68 default: None,
69 description: "Scalar struct input.",
70 },
71];
72
73const STRUCTFUN_UNIFORM_INPUTS: [BuiltinParamDescriptor; 4] = [
74 BuiltinParamDescriptor {
75 name: "func",
76 ty: BuiltinParamType::Any,
77 arity: BuiltinParamArity::Required,
78 default: None,
79 description: "Function handle or builtin/function name.",
80 },
81 BuiltinParamDescriptor {
82 name: "S",
83 ty: BuiltinParamType::Any,
84 arity: BuiltinParamArity::Required,
85 default: None,
86 description: "Scalar struct input.",
87 },
88 BuiltinParamDescriptor {
89 name: "UniformOutput",
90 ty: BuiltinParamType::StringScalar,
91 arity: BuiltinParamArity::Required,
92 default: Some("\"UniformOutput\""),
93 description: "Name-value key for uniform output mode.",
94 },
95 BuiltinParamDescriptor {
96 name: "tf",
97 ty: BuiltinParamType::LogicalArray,
98 arity: BuiltinParamArity::Required,
99 default: Some("true"),
100 description: "Whether callback outputs must be scalar-uniform.",
101 },
102];
103
104const STRUCTFUN_ERROR_HANDLER_INPUTS: [BuiltinParamDescriptor; 4] = [
105 BuiltinParamDescriptor {
106 name: "func",
107 ty: BuiltinParamType::Any,
108 arity: BuiltinParamArity::Required,
109 default: None,
110 description: "Function handle or builtin/function name.",
111 },
112 BuiltinParamDescriptor {
113 name: "S",
114 ty: BuiltinParamType::Any,
115 arity: BuiltinParamArity::Required,
116 default: None,
117 description: "Scalar struct input.",
118 },
119 BuiltinParamDescriptor {
120 name: "ErrorHandler",
121 ty: BuiltinParamType::StringScalar,
122 arity: BuiltinParamArity::Required,
123 default: Some("\"ErrorHandler\""),
124 description: "Name-value key for callback error handler.",
125 },
126 BuiltinParamDescriptor {
127 name: "errfunc",
128 ty: BuiltinParamType::Any,
129 arity: BuiltinParamArity::Required,
130 default: None,
131 description: "Error handler function handle or name.",
132 },
133];
134
135const STRUCTFUN_BOTH_OPTIONS_INPUTS: [BuiltinParamDescriptor; 6] = [
136 BuiltinParamDescriptor {
137 name: "func",
138 ty: BuiltinParamType::Any,
139 arity: BuiltinParamArity::Required,
140 default: None,
141 description: "Function handle or builtin/function name.",
142 },
143 BuiltinParamDescriptor {
144 name: "S",
145 ty: BuiltinParamType::Any,
146 arity: BuiltinParamArity::Required,
147 default: None,
148 description: "Scalar struct input.",
149 },
150 BuiltinParamDescriptor {
151 name: "UniformOutput",
152 ty: BuiltinParamType::StringScalar,
153 arity: BuiltinParamArity::Required,
154 default: Some("\"UniformOutput\""),
155 description: "Name-value key for uniform output mode.",
156 },
157 BuiltinParamDescriptor {
158 name: "tf",
159 ty: BuiltinParamType::LogicalArray,
160 arity: BuiltinParamArity::Required,
161 default: Some("true"),
162 description: "Whether callback outputs must be scalar-uniform.",
163 },
164 BuiltinParamDescriptor {
165 name: "ErrorHandler",
166 ty: BuiltinParamType::StringScalar,
167 arity: BuiltinParamArity::Required,
168 default: Some("\"ErrorHandler\""),
169 description: "Name-value key for callback error handler.",
170 },
171 BuiltinParamDescriptor {
172 name: "errfunc",
173 ty: BuiltinParamType::Any,
174 arity: BuiltinParamArity::Required,
175 default: None,
176 description: "Error handler function handle or name.",
177 },
178];
179
180const STRUCTFUN_SIGNATURES: [BuiltinSignatureDescriptor; 4] = [
181 BuiltinSignatureDescriptor {
182 label: "A = structfun(func, S)",
183 inputs: &STRUCTFUN_INPUTS,
184 outputs: &STRUCTFUN_OUTPUT,
185 },
186 BuiltinSignatureDescriptor {
187 label: "A = structfun(func, S, \"UniformOutput\", tf)",
188 inputs: &STRUCTFUN_UNIFORM_INPUTS,
189 outputs: &STRUCTFUN_OUTPUT,
190 },
191 BuiltinSignatureDescriptor {
192 label: "A = structfun(func, S, \"ErrorHandler\", errfunc)",
193 inputs: &STRUCTFUN_ERROR_HANDLER_INPUTS,
194 outputs: &STRUCTFUN_OUTPUT,
195 },
196 BuiltinSignatureDescriptor {
197 label: "A = structfun(func, S, \"UniformOutput\", tf, \"ErrorHandler\", errfunc)",
198 inputs: &STRUCTFUN_BOTH_OPTIONS_INPUTS,
199 outputs: &STRUCTFUN_OUTPUT,
200 },
201];
202
203const STRUCTFUN_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
204 code: "RM.STRUCTFUN.INVALID_INPUT",
205 identifier: Some("RunMat:structfun:InvalidInput"),
206 when: "Input callback, struct argument, or name-value forms are invalid.",
207 message: "structfun: invalid input arguments",
208};
209
210const STRUCTFUN_ERROR_NOT_SCALAR_STRUCT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
211 code: "RM.STRUCTFUN.NOT_SCALAR_STRUCT",
212 identifier: Some("RunMat:structfun:NotScalarStruct"),
213 when: "Second argument is not a scalar struct.",
214 message: "structfun: second input must be a scalar struct",
215};
216
217const STRUCTFUN_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
218 code: "RM.STRUCTFUN.INTERNAL",
219 identifier: Some("RunMat:structfun:Internal"),
220 when: "Internal field vector materialisation fails.",
221 message: "structfun: internal error",
222};
223
224const STRUCTFUN_ERROR_UNIFORM_OUTPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
225 code: "RM.STRUCTFUN.UNIFORM_OUTPUT",
226 identifier: Some("RunMat:structfun:UniformOutput"),
227 when: "UniformOutput mode requirements are violated.",
228 message: "structfun: uniform output contract violated",
229};
230
231const STRUCTFUN_ERROR_FUNCTION_ERROR: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
232 code: "RM.STRUCTFUN.FUNCTION_ERROR",
233 identifier: Some("RunMat:structfun:FunctionError"),
234 when: "Callback execution or callback output arity fails.",
235 message: "structfun: callback execution error",
236};
237
238const STRUCTFUN_ERRORS: [BuiltinErrorDescriptor; 5] = [
239 STRUCTFUN_ERROR_INVALID_INPUT,
240 STRUCTFUN_ERROR_NOT_SCALAR_STRUCT,
241 STRUCTFUN_ERROR_INTERNAL,
242 STRUCTFUN_ERROR_UNIFORM_OUTPUT,
243 STRUCTFUN_ERROR_FUNCTION_ERROR,
244];
245
246pub const STRUCTFUN_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
247 signatures: &STRUCTFUN_SIGNATURES,
248 output_mode: BuiltinOutputMode::ByRequestedOutputCount,
249 completion_policy: BuiltinCompletionPolicy::Public,
250 errors: &STRUCTFUN_ERRORS,
251};
252
253#[runtime_builtin(
254 name = "structfun",
255 category = "structs/core",
256 summary = "Apply a function to each field of a scalar struct.",
257 keywords = "structfun,struct,functional,uniformoutput,errorhandler",
258 type_resolver(structfun_type),
259 descriptor(crate::builtins::structs::core::structfun::STRUCTFUN_DESCRIPTOR),
260 builtin_path = "crate::builtins::structs::core::structfun"
261)]
262async fn structfun_builtin(
263 func: Value,
264 struct_value: Value,
265 rest: Vec<Value>,
266) -> BuiltinResult<Value> {
267 let options = parse_options(&rest)?;
268 let fields = match struct_value {
269 Value::Struct(st) => FieldValues::from_struct(st),
270 Value::Cell(_) => {
271 return Err(structfun_error(
272 STRUCTFUN_ERROR_NOT_SCALAR_STRUCT.message,
273 &STRUCTFUN_ERROR_NOT_SCALAR_STRUCT,
274 ))
275 }
276 other => {
277 return Err(structfun_error(
278 format!(
279 "{} (got {other:?})",
280 STRUCTFUN_ERROR_NOT_SCALAR_STRUCT.message
281 ),
282 &STRUCTFUN_ERROR_NOT_SCALAR_STRUCT,
283 ))
284 }
285 };
286
287 execute_structfun(func, fields, options).await
288}
289
290struct FieldValues {
291 names: Vec<String>,
292 values: Vec<Value>,
293}
294
295impl FieldValues {
296 fn from_struct(struct_value: StructValue) -> Self {
297 let mut names = Vec::with_capacity(struct_value.fields.len());
298 let mut values = Vec::with_capacity(struct_value.fields.len());
299 for (name, value) in struct_value.fields {
300 names.push(name);
301 values.push(value);
302 }
303 Self { names, values }
304 }
305}
306
307#[derive(Clone)]
308struct StructfunOptions {
309 uniform_output: bool,
310 error_handler: Option<Value>,
311}
312
313fn parse_options(rest: &[Value]) -> BuiltinResult<StructfunOptions> {
314 if !rest.len().is_multiple_of(2) {
315 return Err(structfun_error(
316 "structfun: optional arguments must be name-value pairs",
317 &STRUCTFUN_ERROR_INVALID_INPUT,
318 ));
319 }
320
321 let mut options = StructfunOptions {
322 uniform_output: true,
323 error_handler: None,
324 };
325
326 for pair in rest.chunks_exact(2) {
327 let Some(name) = extract_option_name(&pair[0]) else {
328 return Err(structfun_error(
329 "structfun: option names must be character vectors or string scalars",
330 &STRUCTFUN_ERROR_INVALID_INPUT,
331 ));
332 };
333 match name.trim().to_ascii_lowercase().as_str() {
334 "uniformoutput" => {
335 options.uniform_output = parse_bool_option(&pair[1])?;
336 }
337 "errorhandler" => {
338 options.error_handler = Some(pair[1].clone());
339 }
340 other => {
341 return Err(structfun_error(
342 format!("structfun: unknown name-value argument '{other}'"),
343 &STRUCTFUN_ERROR_INVALID_INPUT,
344 ))
345 }
346 }
347 }
348
349 Ok(options)
350}
351
352fn extract_option_name(value: &Value) -> Option<String> {
353 match value {
354 Value::String(s) => Some(s.clone()),
355 Value::CharArray(ca) if ca.rows == 1 => Some(ca.data.iter().collect()),
356 Value::StringArray(StringArray { data, .. }) if data.len() == 1 => Some(data[0].clone()),
357 _ => None,
358 }
359}
360
361fn parse_bool_option(value: &Value) -> BuiltinResult<bool> {
362 match value {
363 Value::Bool(flag) => Ok(*flag),
364 Value::Num(n) => Ok(*n != 0.0),
365 Value::Int(int) => Ok(int.to_f64() != 0.0),
366 Value::String(text) => parse_bool_text(text).ok_or_else(uniform_option_error),
367 Value::CharArray(chars) if chars.rows == 1 => {
368 let text: String = chars.data.iter().collect();
369 parse_bool_text(&text).ok_or_else(uniform_option_error)
370 }
371 Value::StringArray(StringArray { data, .. }) if data.len() == 1 => {
372 parse_bool_text(&data[0]).ok_or_else(uniform_option_error)
373 }
374 _ => Err(uniform_option_error()),
375 }
376}
377
378fn parse_bool_text(value: &str) -> Option<bool> {
379 match value.trim().to_ascii_lowercase().as_str() {
380 "true" | "on" => Some(true),
381 "false" | "off" => Some(false),
382 _ => None,
383 }
384}
385
386fn uniform_option_error() -> RuntimeError {
387 structfun_error(
388 "structfun: UniformOutput must be logical true or false",
389 &STRUCTFUN_ERROR_UNIFORM_OUTPUT,
390 )
391}
392
393async fn execute_structfun(
394 func: Value,
395 fields: FieldValues,
396 options: StructfunOptions,
397) -> BuiltinResult<Value> {
398 let requested_outputs = current_requested_outputs();
399 if requested_outputs == 0 {
400 return Ok(Value::OutputList(Vec::new()));
401 }
402
403 let mut collectors = (0..requested_outputs)
404 .map(|_| OutputCollector::new(options.uniform_output, &fields.names))
405 .collect::<Vec<_>>();
406
407 for (linear_idx, value) in fields.values.iter().enumerate() {
408 let field_value = gather_if_needed_async(value).await?;
409 let result = match call_feval_async_with_outputs(
410 func.clone(),
411 std::slice::from_ref(&field_value),
412 requested_outputs,
413 )
414 .await
415 {
416 Ok(value) => value,
417 Err(err) => {
418 let Some(handler) = options.error_handler.as_ref() else {
419 return Err(wrap_callback_error(err));
420 };
421 let err_struct = make_error_struct(&err, linear_idx, &fields.names[linear_idx]);
422 call_feval_async_with_outputs(
423 handler.clone(),
424 &[err_struct, field_value.clone()],
425 requested_outputs,
426 )
427 .await
428 .map_err(wrap_callback_error)?
429 }
430 };
431
432 let outputs = normalize_callback_outputs(result, requested_outputs)?;
433 for (collector, output) in collectors.iter_mut().zip(outputs.into_iter()) {
434 let host_output = gather_if_needed_async(&output).await?;
435 collector.push(&fields.names[linear_idx], host_output)?;
436 }
437 }
438
439 let outputs = collectors
440 .into_iter()
441 .map(OutputCollector::finish)
442 .collect::<BuiltinResult<Vec<_>>>()?;
443 if requested_outputs == 1 {
444 Ok(outputs.into_iter().next().unwrap())
445 } else {
446 Ok(Value::OutputList(outputs))
447 }
448}
449
450fn normalize_callback_outputs(value: Value, requested_outputs: usize) -> BuiltinResult<Vec<Value>> {
451 match value {
452 Value::OutputList(values) if values.len() == requested_outputs => Ok(values),
453 Value::OutputList(values) => Err(structfun_error(
454 format!(
455 "structfun: callback returned {} outputs but {} were requested",
456 values.len(),
457 requested_outputs
458 ),
459 &STRUCTFUN_ERROR_FUNCTION_ERROR,
460 )),
461 other if requested_outputs == 1 => Ok(vec![other]),
462 _ => Err(structfun_error(
463 "structfun: callback did not return the requested number of outputs",
464 &STRUCTFUN_ERROR_FUNCTION_ERROR,
465 )),
466 }
467}
468
469fn make_error_struct(error: &RuntimeError, linear_idx: usize, field_name: &str) -> Value {
470 let mut st = StructValue::new();
471 st.insert(
472 "identifier",
473 Value::String(error.identifier().unwrap_or("").to_string()),
474 );
475 st.insert("message", Value::String(error.message().to_string()));
476 st.insert("index", Value::Num((linear_idx + 1) as f64));
477 st.insert("field", Value::String(field_name.to_string()));
478 Value::Struct(st)
479}
480
481enum OutputCollector {
482 Uniform(UniformCollector),
483 Struct(StructValue),
484}
485
486impl OutputCollector {
487 fn new(uniform_output: bool, names: &[String]) -> Self {
488 if uniform_output {
489 OutputCollector::Uniform(UniformCollector::new(names.len()))
490 } else {
491 OutputCollector::Struct(StructValue::new())
492 }
493 }
494
495 fn push(&mut self, field_name: &str, value: Value) -> BuiltinResult<()> {
496 match self {
497 OutputCollector::Uniform(collector) => collector.push(&value),
498 OutputCollector::Struct(out) => {
499 out.insert(field_name.to_string(), value);
500 Ok(())
501 }
502 }
503 }
504
505 fn finish(self) -> BuiltinResult<Value> {
506 match self {
507 OutputCollector::Uniform(collector) => collector.finish(),
508 OutputCollector::Struct(out) => Ok(Value::Struct(out)),
509 }
510 }
511}
512
513enum UniformCollector {
514 Pending { len: usize },
515 Double { data: Vec<f64>, len: usize },
516 Logical { data: Vec<u8>, len: usize },
517 Complex { data: Vec<(f64, f64)>, len: usize },
518}
519
520impl UniformCollector {
521 fn new(len: usize) -> Self {
522 UniformCollector::Pending { len }
523 }
524
525 fn push(&mut self, value: &Value) -> BuiltinResult<()> {
526 match self {
527 UniformCollector::Pending { len } => match classify_uniform_value(value)? {
528 ClassifiedValue::Logical(flag) => {
529 *self = UniformCollector::Logical {
530 data: vec![flag as u8],
531 len: *len,
532 };
533 Ok(())
534 }
535 ClassifiedValue::Double(value) => {
536 *self = UniformCollector::Double {
537 data: vec![value],
538 len: *len,
539 };
540 Ok(())
541 }
542 ClassifiedValue::Complex(value) => {
543 *self = UniformCollector::Complex {
544 data: vec![value],
545 len: *len,
546 };
547 Ok(())
548 }
549 },
550 UniformCollector::Logical { data, len } => match classify_uniform_value(value)? {
551 ClassifiedValue::Logical(flag) => {
552 data.push(flag as u8);
553 Ok(())
554 }
555 ClassifiedValue::Double(value) => {
556 let mut values = data
557 .iter()
558 .map(|&bit| if bit != 0 { 1.0 } else { 0.0 })
559 .collect::<Vec<_>>();
560 values.push(value);
561 *self = UniformCollector::Double {
562 data: values,
563 len: *len,
564 };
565 Ok(())
566 }
567 ClassifiedValue::Complex(value) => {
568 let mut values = data
569 .iter()
570 .map(|&bit| if bit != 0 { (1.0, 0.0) } else { (0.0, 0.0) })
571 .collect::<Vec<_>>();
572 values.push(value);
573 *self = UniformCollector::Complex {
574 data: values,
575 len: *len,
576 };
577 Ok(())
578 }
579 },
580 UniformCollector::Double { data, len } => match classify_uniform_value(value)? {
581 ClassifiedValue::Logical(flag) => {
582 data.push(if flag { 1.0 } else { 0.0 });
583 Ok(())
584 }
585 ClassifiedValue::Double(value) => {
586 data.push(value);
587 Ok(())
588 }
589 ClassifiedValue::Complex(value) => {
590 let mut values = data.iter().map(|&value| (value, 0.0)).collect::<Vec<_>>();
591 values.push(value);
592 *self = UniformCollector::Complex {
593 data: values,
594 len: *len,
595 };
596 Ok(())
597 }
598 },
599 UniformCollector::Complex { data, .. } => match classify_uniform_value(value)? {
600 ClassifiedValue::Logical(flag) => {
601 data.push((if flag { 1.0 } else { 0.0 }, 0.0));
602 Ok(())
603 }
604 ClassifiedValue::Double(value) => {
605 data.push((value, 0.0));
606 Ok(())
607 }
608 ClassifiedValue::Complex(value) => {
609 data.push(value);
610 Ok(())
611 }
612 },
613 }
614 }
615
616 fn finish(self) -> BuiltinResult<Value> {
617 match self {
618 UniformCollector::Pending { len } => Tensor::new(Vec::new(), vec![len, 1])
619 .map(Value::Tensor)
620 .map_err(|err| {
621 structfun_error(format!("structfun: {err}"), &STRUCTFUN_ERROR_INTERNAL)
622 }),
623 UniformCollector::Double { data, len } => Tensor::new(data, vec![len, 1])
624 .map(Value::Tensor)
625 .map_err(|err| {
626 structfun_error(format!("structfun: {err}"), &STRUCTFUN_ERROR_INTERNAL)
627 }),
628 UniformCollector::Logical { data, len } => LogicalArray::new(data, vec![len, 1])
629 .map(Value::LogicalArray)
630 .map_err(|err| {
631 structfun_error(format!("structfun: {err}"), &STRUCTFUN_ERROR_INTERNAL)
632 }),
633 UniformCollector::Complex { data, len } => ComplexTensor::new(data, vec![len, 1])
634 .map(Value::ComplexTensor)
635 .map_err(|err| {
636 structfun_error(format!("structfun: {err}"), &STRUCTFUN_ERROR_INTERNAL)
637 }),
638 }
639 }
640}
641
642enum ClassifiedValue {
643 Logical(bool),
644 Double(f64),
645 Complex((f64, f64)),
646}
647
648fn classify_uniform_value(value: &Value) -> BuiltinResult<ClassifiedValue> {
649 match value {
650 Value::Bool(flag) => Ok(ClassifiedValue::Logical(*flag)),
651 Value::Num(value) => Ok(ClassifiedValue::Double(*value)),
652 Value::Int(value) => Ok(ClassifiedValue::Double(value.to_f64())),
653 Value::Complex(re, im) => Ok(ClassifiedValue::Complex((*re, *im))),
654 Value::Tensor(tensor) if tensor.data.len() == 1 => {
655 Ok(ClassifiedValue::Double(tensor.data[0]))
656 }
657 Value::LogicalArray(array) if array.data.len() == 1 => {
658 Ok(ClassifiedValue::Logical(array.data[0] != 0))
659 }
660 Value::ComplexTensor(tensor) if tensor.data.len() == 1 => {
661 Ok(ClassifiedValue::Complex(tensor.data[0]))
662 }
663 _ => Err(structfun_error(
664 "structfun: callback must return scalar values when 'UniformOutput' is true",
665 &STRUCTFUN_ERROR_UNIFORM_OUTPUT,
666 )),
667 }
668}
669
670fn wrap_callback_error(error: RuntimeError) -> RuntimeError {
671 structfun_error(
672 error.message().replace("cellfun:", "structfun:"),
673 &STRUCTFUN_ERROR_FUNCTION_ERROR,
674 )
675}
676
677fn structfun_error(
678 message: impl Into<String>,
679 error: &'static BuiltinErrorDescriptor,
680) -> RuntimeError {
681 let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
682 if let Some(identifier) = error.identifier {
683 builder = builder.with_identifier(identifier);
684 }
685 builder.build()
686}
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691 use futures::executor::block_on;
692 use runmat_builtins::{CellArray, Tensor};
693 use std::sync::Arc;
694
695 fn call(func: Value, st: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
696 block_on(structfun_builtin(func, st, rest))
697 }
698
699 fn sample_struct() -> StructValue {
700 let mut st = StructValue::new();
701 st.insert(
702 "alpha",
703 Value::Tensor(Tensor::new(vec![1.0, 2.0, 3.0], vec![1, 3]).unwrap()),
704 );
705 st.insert(
706 "beta",
707 Value::Tensor(Tensor::new(vec![4.0, 5.0], vec![1, 2]).unwrap()),
708 );
709 st
710 }
711
712 #[test]
713 fn structfun_length_uniform_default_returns_column_vector() {
714 let result = call(
715 Value::String("@length".into()),
716 Value::Struct(sample_struct()),
717 Vec::new(),
718 )
719 .expect("structfun");
720 match result {
721 Value::Tensor(tensor) => {
722 assert_eq!(tensor.shape, vec![2, 1]);
723 assert_eq!(tensor.data, vec![3.0, 2.0]);
724 }
725 other => panic!("expected tensor, got {other:?}"),
726 }
727 }
728
729 #[test]
730 fn structfun_uniform_output_false_preserves_field_names() {
731 let mut st = StructValue::new();
732 st.insert("name", Value::String("runmat".into()));
733 st.insert("flag", Value::Bool(true));
734
735 let result = call(
736 Value::String("@class".into()),
737 Value::Struct(st),
738 vec![Value::String("UniformOutput".into()), Value::Bool(false)],
739 )
740 .expect("structfun");
741 match result {
742 Value::Struct(out) => {
743 assert_eq!(
744 out.fields.get("name"),
745 Some(&Value::String("string".into()))
746 );
747 assert_eq!(
748 out.fields.get("flag"),
749 Some(&Value::String("logical".into()))
750 );
751 }
752 other => panic!("expected struct, got {other:?}"),
753 }
754 }
755
756 #[test]
757 fn structfun_multiple_outputs_collect_each_output() {
758 let _guard = crate::user_functions::install_semantic_function_invoker(Some(Arc::new(
759 |_function, args, requested_outputs| {
760 assert_eq!(requested_outputs, 2);
761 let Value::Num(input) = args[0] else {
762 panic!("expected numeric input");
763 };
764 Box::pin(async move {
765 Ok(Value::OutputList(vec![
766 Value::Num(input),
767 Value::Num(input + 10.0),
768 ]))
769 })
770 },
771 )));
772 let _output_guard = crate::output_count::push_output_count(Some(2));
773 let mut st = StructValue::new();
774 st.insert("a", Value::Num(1.0));
775 st.insert("b", Value::Num(2.0));
776 let handle = Value::BoundFunctionHandle {
777 name: "two_outputs".to_string(),
778 function: 91,
779 };
780
781 let result = call(handle, Value::Struct(st), Vec::new()).expect("structfun");
782 match result {
783 Value::OutputList(outputs) => {
784 assert_eq!(outputs.len(), 2);
785 match &outputs[0] {
786 Value::Tensor(tensor) => assert_eq!(tensor.data, vec![1.0, 2.0]),
787 other => panic!("expected first tensor, got {other:?}"),
788 }
789 match &outputs[1] {
790 Value::Tensor(tensor) => assert_eq!(tensor.data, vec![11.0, 12.0]),
791 other => panic!("expected second tensor, got {other:?}"),
792 }
793 }
794 other => panic!("expected output list, got {other:?}"),
795 }
796 }
797
798 #[test]
799 fn structfun_multiple_outputs_with_uniform_false_return_structs() {
800 let _guard = crate::user_functions::install_semantic_function_invoker(Some(Arc::new(
801 |_function, args, requested_outputs| {
802 assert_eq!(requested_outputs, 2);
803 let Value::Num(input) = args[0] else {
804 panic!("expected numeric input");
805 };
806 Box::pin(async move {
807 Ok(Value::OutputList(vec![
808 Value::String(format!("v{input}")),
809 Value::Bool(input > 1.0),
810 ]))
811 })
812 },
813 )));
814 let _output_guard = crate::output_count::push_output_count(Some(2));
815 let mut st = StructValue::new();
816 st.insert("a", Value::Num(1.0));
817 st.insert("b", Value::Num(2.0));
818 let handle = Value::BoundFunctionHandle {
819 name: "two_outputs".to_string(),
820 function: 92,
821 };
822
823 let result = call(
824 handle,
825 Value::Struct(st),
826 vec![Value::String("UniformOutput".into()), Value::Bool(false)],
827 )
828 .expect("structfun");
829 match result {
830 Value::OutputList(outputs) => {
831 assert_eq!(outputs.len(), 2);
832 match &outputs[0] {
833 Value::Struct(out) => {
834 assert_eq!(out.fields.get("a"), Some(&Value::String("v1".into())));
835 assert_eq!(out.fields.get("b"), Some(&Value::String("v2".into())));
836 }
837 other => panic!("expected first struct, got {other:?}"),
838 }
839 match &outputs[1] {
840 Value::Struct(out) => {
841 assert_eq!(out.fields.get("a"), Some(&Value::Bool(false)));
842 assert_eq!(out.fields.get("b"), Some(&Value::Bool(true)));
843 }
844 other => panic!("expected second struct, got {other:?}"),
845 }
846 }
847 other => panic!("expected output list, got {other:?}"),
848 }
849 }
850
851 #[test]
852 fn structfun_callback_errors_use_structfun_identifier() {
853 let err = call(
854 Value::String("@missing_structfun_callback".into()),
855 Value::Struct(sample_struct()),
856 Vec::new(),
857 )
858 .expect_err("missing callback should fail");
859 assert_eq!(err.identifier(), STRUCTFUN_ERROR_FUNCTION_ERROR.identifier);
860 assert!(err.message().contains("Undefined function"));
861 }
862
863 #[test]
864 fn structfun_rejects_non_scalar_struct_array() {
865 let array = CellArray::new(vec![Value::Struct(StructValue::new())], 1, 1).unwrap();
866 let err = call(
867 Value::String("@length".into()),
868 Value::Cell(array),
869 Vec::new(),
870 )
871 .expect_err("struct arrays should fail");
872 assert_eq!(
873 err.identifier(),
874 STRUCTFUN_ERROR_NOT_SCALAR_STRUCT.identifier
875 );
876 }
877
878 #[test]
879 fn structfun_rejects_extra_non_option_arguments() {
880 let err = call(
881 Value::String("@length".into()),
882 Value::Struct(sample_struct()),
883 vec![Value::Num(1.0)],
884 )
885 .expect_err("extra positional args should fail");
886 assert_eq!(err.identifier(), STRUCTFUN_ERROR_INVALID_INPUT.identifier);
887 }
888
889 #[test]
890 fn structfun_descriptor_covers_option_forms() {
891 let labels = STRUCTFUN_DESCRIPTOR
892 .signatures
893 .iter()
894 .map(|signature| signature.label)
895 .collect::<Vec<_>>();
896 assert!(labels.contains(&"A = structfun(func, S)"));
897 assert!(labels.contains(&"A = structfun(func, S, \"UniformOutput\", tf)"));
898 assert!(labels.contains(&"A = structfun(func, S, \"ErrorHandler\", errfunc)"));
899 assert!(labels
900 .contains(&"A = structfun(func, S, \"UniformOutput\", tf, \"ErrorHandler\", errfunc)"));
901 }
902}