Skip to main content

runmat_runtime/
dispatcher.rs

1use crate::{build_runtime_error, create_class_object, make_cell_with_shape, RuntimeError};
2use runmat_accelerate_api::GpuTensorHandle;
3use runmat_builtins::builtin_functions;
4
5use runmat_value::Value;
6use std::cell::RefCell;
7
8thread_local! {
9    static CLASS_ACCESS_CONTEXT: RefCell<Option<String>> = const { RefCell::new(None) };
10}
11
12#[cfg(target_arch = "wasm32")]
13fn ensure_wasm_builtins_registered() {
14    crate::builtins::wasm_registry::register_all();
15}
16
17#[cfg(not(target_arch = "wasm32"))]
18fn ensure_wasm_builtins_registered() {}
19
20pub struct ClassAccessContextGuard {
21    previous: Option<String>,
22    context: Option<std::rc::Rc<crate::context::RuntimeContextState>>,
23}
24
25impl Drop for ClassAccessContextGuard {
26    fn drop(&mut self) {
27        let previous = self.previous.take();
28        if let Some(context) = &self.context {
29            context.call.borrow_mut().class_access = previous;
30        } else {
31            CLASS_ACCESS_CONTEXT.with(|slot| {
32                *slot.borrow_mut() = previous;
33            });
34        }
35    }
36}
37
38pub fn push_class_access_context(class_name: Option<String>) -> ClassAccessContextGuard {
39    let context =
40        crate::context::legacy::active().map(|context| std::rc::Rc::clone(context.state()));
41    let previous = if let Some(context) = &context {
42        std::mem::replace(&mut context.call.borrow_mut().class_access, class_name)
43    } else {
44        CLASS_ACCESS_CONTEXT.with(|slot| std::mem::replace(&mut *slot.borrow_mut(), class_name))
45    };
46    ClassAccessContextGuard { previous, context }
47}
48
49fn current_class_access_context() -> Option<String> {
50    if let Some(context) = crate::context::legacy::active() {
51        return context.state().call.borrow().class_access.clone();
52    }
53    CLASS_ACCESS_CONTEXT.with(|slot| slot.borrow().clone())
54}
55
56pub fn class_access_context() -> Option<String> {
57    current_class_access_context()
58}
59
60/// Return `true` when the passed value is a GPU-resident tensor handle.
61pub fn is_gpu_value(value: &Value) -> bool {
62    matches!(value, Value::GpuTensor(_))
63}
64
65/// Returns true when the value (or nested elements) contains any GPU-resident tensors.
66pub fn value_contains_gpu(value: &Value) -> bool {
67    match value {
68        Value::GpuTensor(_) => true,
69        Value::Cell(ca) => ca.data.iter().any(|ptr| value_contains_gpu(ptr)),
70        Value::Struct(sv) => sv.fields.values().any(value_contains_gpu),
71        Value::Object(obj) => obj.properties.values().any(value_contains_gpu),
72        Value::Closure(closure) => closure.captures.iter().any(value_contains_gpu),
73        Value::OutputList(values) => values.iter().any(value_contains_gpu),
74        _ => false,
75    }
76}
77
78/// Convert GPU-resident values to host tensors when an acceleration provider exists.
79/// Non-GPU inputs are passed through unchanged.
80pub async fn gather_if_needed_async(value: &Value) -> Result<Value, RuntimeError> {
81    gather_if_needed_async_impl(value).await
82}
83
84fn gather_if_needed_async_impl<'a>(
85    value: &'a Value,
86) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Value, RuntimeError>> + 'a>> {
87    Box::pin(async move {
88        match value {
89            Value::GpuTensor(handle) => {
90                // In parallel test runs, ensure the WGPU provider is reasserted for WGPU handles.
91                #[cfg(all(test, feature = "wgpu"))]
92                {
93                    let active_owner = runmat_accelerate_api::provider()
94                        .is_some_and(|provider| provider.device_id() == handle.device_id);
95                    if handle.device_id != 0 && !active_owner {
96                        let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
97                        runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
98                    );
99                    }
100                }
101                let provider = runmat_accelerate_api::provider_for_handle(handle)
102                    .filter(|provider| provider.device_id() == handle.device_id)
103                    .ok_or_else(|| {
104                        build_runtime_error("gather: no acceleration provider registered")
105                            .with_identifier("RunMat:gather:ProviderUnavailable")
106                            .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
107                            .build()
108                    })?;
109                let expected_element =
110                    crate::builtins::common::gpu_helpers::expected_handle_numeric_element_type(
111                        handle,
112                    )
113                    .map_err(|error| {
114                        build_runtime_error(format!("gather: {error}"))
115                            .with_identifier("RunMat:gpu:ProviderPayloadMismatch")
116                            .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
117                            .build()
118                    })?;
119                let host = provider.download_numeric(handle).await.map_err(|err| {
120                    build_runtime_error(format!("gather: {err}"))
121                        .with_identifier("RunMat:gather:DownloadFailed")
122                        .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
123                        .build()
124                })?;
125                let expected_storage = runmat_accelerate_api::handle_storage(handle);
126                if host.shape != handle.shape
127                    || host.storage != expected_storage
128                    || host.data.element_type() != expected_element
129                {
130                    return Err(provider_payload_mismatch(
131                        handle,
132                        &host.shape,
133                        format!(
134                            "{:?} {:?}, expected {:?} {:?}",
135                            host.data.element_type(),
136                            host.storage,
137                            expected_element,
138                            expected_storage
139                        ),
140                    ));
141                }
142                crate::builtins::common::gpu_helpers::value_from_numeric_download(
143                    host,
144                    runmat_accelerate_api::handle_is_logical(handle),
145                )
146                .map_err(|error| {
147                    build_runtime_error(format!("gather: {error}"))
148                        .with_identifier("RunMat:gpu:ProviderPayloadMismatch")
149                        .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
150                        .build()
151                })
152            }
153            Value::Cell(ca) => {
154                let mut gathered = Vec::with_capacity(ca.data.len());
155                for ptr in &ca.data {
156                    gathered.push(gather_if_needed_async_impl(ptr).await?);
157                }
158                make_cell_with_shape(gathered, ca.shape.clone()).map_err(|err| {
159                    build_runtime_error(format!("gather: {err}"))
160                        .with_identifier("RunMat:gather:CellShapeError")
161                        .build()
162                })
163            }
164            Value::Struct(sv) => {
165                let mut gathered = sv.clone();
166                for value in gathered.fields.values_mut() {
167                    let updated = gather_if_needed_async_impl(value).await?;
168                    *value = updated;
169                }
170                Ok(Value::Struct(gathered))
171            }
172            Value::Object(obj) => {
173                let mut cloned = obj.clone();
174                for value in cloned.properties.values_mut() {
175                    *value = gather_if_needed_async_impl(value).await?;
176                }
177                Ok(Value::Object(cloned))
178            }
179            Value::Closure(closure) => {
180                let mut cloned = closure.clone();
181                for value in &mut cloned.captures {
182                    *value = gather_if_needed_async_impl(value).await?;
183                }
184                Ok(Value::Closure(cloned))
185            }
186            Value::OutputList(values) => {
187                let mut gathered = Vec::with_capacity(values.len());
188                for value in values {
189                    gathered.push(gather_if_needed_async_impl(value).await?);
190                }
191                Ok(Value::OutputList(gathered))
192            }
193            other => Ok(other.clone()),
194        }
195    })
196}
197
198fn provider_payload_mismatch(
199    handle: &GpuTensorHandle,
200    actual_shape: &[usize],
201    detail: String,
202) -> RuntimeError {
203    build_runtime_error(format!(
204        "gather: provider payload mismatch ({detail}; shape {actual_shape:?}, expected {:?})",
205        handle.shape
206    ))
207    .with_identifier("RunMat:gpu:ProviderPayloadMismatch")
208    .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
209    .build()
210}
211
212#[cfg(not(target_arch = "wasm32"))]
213pub fn gather_if_needed(value: &Value) -> Result<Value, RuntimeError> {
214    futures::executor::block_on(gather_if_needed_async(value))
215}
216
217#[cfg(target_arch = "wasm32")]
218pub fn gather_if_needed(_value: &Value) -> Result<Value, RuntimeError> {
219    Err(
220        build_runtime_error("gather: synchronous gather is unavailable on wasm")
221            .with_identifier("RunMat:gather:UnavailableOnWasm")
222            .build(),
223    )
224}
225
226/// Call a registered language builtin by name.
227/// Supports function overloading by trying different argument patterns.
228/// Returns an error if no builtin with that name and compatible arguments is found.
229pub fn call_builtin(name: &str, args: &[Value]) -> Result<Value, RuntimeError> {
230    futures::executor::block_on(call_builtin_async(name, args))
231}
232
233#[async_recursion::async_recursion(?Send)]
234async fn call_builtin_async_impl(
235    name: &str,
236    args: &[Value],
237    output_count: Option<usize>,
238) -> Result<Value, RuntimeError> {
239    ensure_wasm_builtins_registered();
240
241    let _output_guard = crate::output_count::push_output_count(output_count);
242    let scoped_builtin_service = crate::context::legacy::active()
243        .and_then(|context| context.service_ports().builtin().cloned());
244    let matching_bindings = scoped_builtin_service.as_ref().map_or_else(
245        || crate::builtin::runtime_builtin_bindings_by_name(name),
246        |service| service.bindings_by_name(name),
247    );
248    let mut matching_builtins = Vec::new();
249
250    // Collect all builtins with the matching name
251    if scoped_builtin_service.is_none() {
252        for b in builtin_functions() {
253            if b.name == name {
254                matching_builtins.push(b);
255            }
256        }
257    }
258
259    if !matching_bindings.is_empty() && !matching_builtins.is_empty() {
260        return Err(build_runtime_error(format!(
261            "builtin `{name}` has both canonical and legacy runtime bindings"
262        ))
263        .with_identifier("RunMat:Catalog:DuplicateBindingAuthority")
264        .build());
265    }
266
267    if matching_bindings.is_empty() && matching_builtins.is_empty() {
268        if let Some(result) = try_call_registered_instance_method(name, args, output_count).await? {
269            return compatibility_checked_builtin_result(name, args, result);
270        }
271        if let Some(result) = try_call_registered_static_method(name, args, output_count).await? {
272            return compatibility_checked_builtin_result(name, args, result);
273        }
274        // Fallback: treat as class constructor if class is registered.
275        if crate::class_registry::get_class(name).is_some() {
276            let result = call_registered_class_constructor(name, args, output_count).await?;
277            return compatibility_checked_builtin_result(name, args, result);
278        }
279        return Err(build_runtime_error(format!("Undefined function: {name}"))
280            .with_identifier("RunMat:UndefinedFunction")
281            .build());
282    }
283
284    if let Some(result) = try_call_registered_instance_method(name, args, output_count).await? {
285        return compatibility_checked_builtin_result(name, args, result);
286    }
287
288    // Partition into no-category (tests/legacy shims) and categorized (library) builtins.
289    let mut no_category: Vec<&runmat_builtins::BuiltinFunction> = Vec::new();
290    let mut categorized: Vec<&runmat_builtins::BuiltinFunction> = Vec::new();
291    for b in matching_builtins {
292        if b.category.is_empty() {
293            no_category.push(b);
294        } else {
295            categorized.push(b);
296        }
297    }
298    let matching_count = matching_bindings.len() + no_category.len() + categorized.len();
299    let implementations = matching_bindings
300        .into_iter()
301        .rev()
302        .map(|binding| binding.implementation)
303        .chain(
304            no_category
305                .into_iter()
306                .rev()
307                .chain(categorized.into_iter().rev())
308                .map(|builtin| builtin.implementation),
309        );
310
311    // Try each builtin until one succeeds. Within each group, prefer later-registered
312    // implementations to allow overrides when names collide.
313    let mut last_error = RuntimeError::new("unknown error");
314    for implementation in implementations {
315        let f = implementation;
316        match (f)(args).await {
317            Ok(result) => return compatibility_checked_builtin_result(name, args, result),
318            Err(err) => {
319                if should_retry_with_gpu_gather(&err, args) {
320                    match gather_args_for_retry_async(args).await {
321                        Ok(Some(gathered_args)) => match (f)(&gathered_args).await {
322                            Ok(result) => {
323                                return compatibility_checked_builtin_result(name, args, result);
324                            }
325                            Err(retry_err) => last_error = retry_err,
326                        },
327                        Ok(None) => last_error = err,
328                        Err(gather_err) => last_error = gather_err,
329                    }
330                } else {
331                    last_error = err;
332                }
333            }
334        }
335    }
336
337    // A single implementation already knows whether its inputs are invalid or
338    // whether execution failed. Preserve that error verbatim instead of
339    // presenting it as overload resolution noise.
340    if matching_count == 1 || last_error.identifier().is_some() {
341        return Err(last_error);
342    }
343
344    // If none succeeded, return the last error
345    let identifier = last_error
346        .identifier()
347        .unwrap_or("RunMat:NoMatchingOverload")
348        .to_string();
349    let mut builder = build_runtime_error(format!(
350        "No matching overload for `{}` with {} args: {}",
351        name,
352        args.len(),
353        last_error.message()
354    ))
355    .with_source(last_error);
356    builder = builder.with_identifier(identifier);
357    Err(builder.build())
358}
359
360fn compatibility_checked_builtin_result(
361    name: &str,
362    args: &[Value],
363    mut result: Value,
364) -> Result<Value, RuntimeError> {
365    crate::compatibility::ensure_value_compatible(&result, name)?;
366    propagate_gpu_provenance(name, args, &mut result);
367    Ok(result)
368}
369
370fn propagate_gpu_provenance(name: &str, args: &[Value], result: &mut Value) {
371    let mut saw_gpu = false;
372    let mut explicit = false;
373    for arg in args {
374        visit_gpu_handles(arg, &mut |handle| {
375            saw_gpu = true;
376            explicit |= runmat_accelerate_api::handle_is_explicit(handle);
377        });
378    }
379    if !saw_gpu {
380        let explicit_constructor = matches!(
381            name,
382            "zeros"
383                | "ones"
384                | "inf"
385                | "nan"
386                | "rand"
387                | "randn"
388                | "randi"
389                | "eye"
390                | "true"
391                | "false"
392        ) && args.iter().any(|arg| {
393            crate::builtins::common::tensor::value_to_string(arg)
394                .is_some_and(|text| text.eq_ignore_ascii_case("gpuarray"))
395        });
396        visit_gpu_handles_mut(result, &mut |handle| {
397            if explicit_constructor {
398                handle.descriptor.provenance =
399                    Some(runmat_accelerate_api::GpuHandleProvenance::Explicit);
400            } else if runmat_accelerate_api::handle_provenance(handle).is_none() {
401                handle.descriptor.provenance =
402                    Some(runmat_accelerate_api::GpuHandleProvenance::Automatic);
403            }
404        });
405        return;
406    }
407    let provenance = if explicit {
408        runmat_accelerate_api::GpuHandleProvenance::Explicit
409    } else {
410        runmat_accelerate_api::GpuHandleProvenance::Automatic
411    };
412    visit_gpu_handles_mut(result, &mut |handle| {
413        handle.descriptor.provenance = Some(provenance);
414    });
415}
416
417fn visit_gpu_handles(value: &Value, visitor: &mut impl FnMut(&GpuTensorHandle)) {
418    match value {
419        Value::GpuTensor(handle) => visitor(handle),
420        Value::Cell(cell) => cell
421            .data
422            .iter()
423            .for_each(|value| visit_gpu_handles(value, visitor)),
424        Value::Struct(value) => value
425            .fields
426            .values()
427            .for_each(|value| visit_gpu_handles(value, visitor)),
428        Value::Object(value) => value
429            .properties
430            .values()
431            .for_each(|value| visit_gpu_handles(value, visitor)),
432        Value::Closure(value) => value
433            .captures
434            .iter()
435            .for_each(|value| visit_gpu_handles(value, visitor)),
436        Value::OutputList(values) => values
437            .iter()
438            .for_each(|value| visit_gpu_handles(value, visitor)),
439        _ => {}
440    }
441}
442
443fn visit_gpu_handles_mut(value: &mut Value, visitor: &mut impl FnMut(&mut GpuTensorHandle)) {
444    match value {
445        Value::GpuTensor(handle) => visitor(handle),
446        Value::Cell(cell) => cell
447            .data
448            .iter_mut()
449            .for_each(|value| visit_gpu_handles_mut(value, visitor)),
450        Value::Struct(value) => value
451            .fields
452            .values_mut()
453            .for_each(|value| visit_gpu_handles_mut(value, visitor)),
454        Value::Object(value) => value
455            .properties
456            .values_mut()
457            .for_each(|value| visit_gpu_handles_mut(value, visitor)),
458        Value::Closure(value) => value
459            .captures
460            .iter_mut()
461            .for_each(|value| visit_gpu_handles_mut(value, visitor)),
462        Value::OutputList(values) => values
463            .iter_mut()
464            .for_each(|value| visit_gpu_handles_mut(value, visitor)),
465        _ => {}
466    }
467}
468
469pub(crate) async fn try_call_registered_instance_method(
470    method_name: &str,
471    args: &[Value],
472    output_count: Option<usize>,
473) -> Result<Option<Value>, RuntimeError> {
474    let Some(receiver) = args.first() else {
475        return Ok(None);
476    };
477    let class_name = match receiver {
478        Value::Object(obj) => obj.class_name.as_str(),
479        Value::HandleObject(handle) => handle.class_name.as_str(),
480        _ => return Ok(None),
481    };
482    let Some((method, owner)) = crate::class_registry::lookup_method(class_name, method_name)
483    else {
484        return Ok(None);
485    };
486    if method.is_static {
487        return Ok(None);
488    }
489    let caller_class = current_class_access_context();
490    let access_allowed = match method.access {
491        runmat_types::MemberAccess::Public => true,
492        runmat_types::MemberAccess::Private => caller_class.as_deref() == Some(owner.as_str()),
493        runmat_types::MemberAccess::Protected => caller_class
494            .as_deref()
495            .is_some_and(|caller| crate::class_registry::is_class_or_subclass(caller, &owner)),
496    };
497    if !access_allowed {
498        return Err(build_runtime_error(format!(
499            "Method '{}' is not accessible from current context.",
500            method_name
501        ))
502        .with_identifier("RunMat:MethodPrivate")
503        .build());
504    }
505    if let Some(result) = crate::user_functions::try_call_semantic_function_by_name(
506        &method.function_name,
507        args,
508        output_count.unwrap_or(1),
509    )
510    .await
511    {
512        return finalize_instance_method_result(method_name, receiver, result).map(Some);
513    }
514    if runmat_builtins::builtin_name_is_known(&method.function_name)
515        && method.function_name != method_name
516    {
517        let result = call_builtin_async_impl(&method.function_name, args, output_count).await;
518        return finalize_instance_method_result(method_name, receiver, result).map(Some);
519    }
520    let owner_qualified = format!("{owner}.{method_name}");
521    if owner_qualified != method.function_name {
522        if let Some(result) = crate::user_functions::try_call_semantic_function_by_name(
523            &owner_qualified,
524            args,
525            output_count.unwrap_or(1),
526        )
527        .await
528        {
529            return finalize_instance_method_result(method_name, receiver, result).map(Some);
530        }
531        if runmat_builtins::builtin_name_is_known(&owner_qualified)
532            && owner_qualified != method_name
533        {
534            let result = call_builtin_async_impl(&owner_qualified, args, output_count).await;
535            return finalize_instance_method_result(method_name, receiver, result).map(Some);
536        }
537    }
538    Ok(None)
539}
540
541fn finalize_instance_method_result(
542    method_name: &str,
543    receiver: &Value,
544    result: Result<Value, RuntimeError>,
545) -> Result<Value, RuntimeError> {
546    let result = result?;
547    if method_name == "delete" {
548        if let Value::HandleObject(handle) = receiver {
549            if !crate::set_handle_valid(handle, false) {
550                return Err(build_runtime_error(format!(
551                    "delete: failed to invalidate handle object '{}' after its destructor completed",
552                    handle.class_name
553                ))
554                .with_identifier("RunMat:delete:InvalidHandle")
555                .build());
556            }
557        }
558    }
559    Ok(result)
560}
561
562async fn try_call_registered_static_method(
563    qualified_name: &str,
564    args: &[Value],
565    output_count: Option<usize>,
566) -> Result<Option<Value>, RuntimeError> {
567    let Some((class_name, method_name)) = qualified_name.rsplit_once('.') else {
568        return Ok(None);
569    };
570    if class_name.trim().is_empty() || method_name.trim().is_empty() {
571        return Ok(None);
572    }
573    if crate::class_registry::get_class(class_name).is_none() {
574        return Ok(None);
575    }
576    let Some((method, owner)) = crate::class_registry::lookup_method(class_name, method_name)
577    else {
578        return Ok(None);
579    };
580    if !method.is_static || method.access != runmat_types::MemberAccess::Public {
581        return Ok(None);
582    }
583    if let Some(result) = crate::user_functions::try_call_semantic_function_by_name(
584        &method.function_name,
585        args,
586        output_count.unwrap_or(1),
587    )
588    .await
589    {
590        return result.map(Some);
591    }
592    if runmat_builtins::builtin_name_is_known(&method.function_name)
593        && method.function_name != qualified_name
594    {
595        return call_builtin_async_impl(&method.function_name, args, output_count)
596            .await
597            .map(Some);
598    }
599    let owner_qualified = format!("{owner}.{method_name}");
600    if owner_qualified != method.function_name {
601        if let Some(result) = crate::user_functions::try_call_semantic_function_by_name(
602            &owner_qualified,
603            args,
604            output_count.unwrap_or(1),
605        )
606        .await
607        {
608            return result.map(Some);
609        }
610        if runmat_builtins::builtin_name_is_known(&owner_qualified)
611            && owner_qualified != qualified_name
612        {
613            return call_builtin_async_impl(&owner_qualified, args, output_count)
614                .await
615                .map(Some);
616        }
617    }
618    Ok(None)
619}
620
621async fn call_registered_class_constructor(
622    class_name: &str,
623    args: &[Value],
624    output_count: Option<usize>,
625) -> Result<Value, RuntimeError> {
626    let requested_outputs = output_count.unwrap_or(1);
627    let default_object = create_class_object(class_name.to_string()).await?;
628    let constructor_method_name = class_name.rsplit('.').next().unwrap_or(class_name);
629    let Some((ctor, owner)) =
630        crate::class_registry::lookup_method(class_name, constructor_method_name)
631            .or_else(|| crate::class_registry::lookup_method(class_name, class_name))
632    else {
633        return Ok(default_object);
634    };
635    let owner_qualified = format!("{owner}.{constructor_method_name}");
636    let caller_class = current_class_access_context();
637    let ctor_access_allowed = match ctor.access {
638        runmat_types::MemberAccess::Public => true,
639        runmat_types::MemberAccess::Private => caller_class.as_deref() == Some(owner.as_str()),
640        runmat_types::MemberAccess::Protected => caller_class
641            .as_deref()
642            .is_some_and(|caller| crate::class_registry::is_class_or_subclass(caller, &owner)),
643    };
644    if !ctor_access_allowed {
645        return Err(build_runtime_error(format!(
646            "Constructor '{}' is not accessible from current context.",
647            class_name
648        ))
649        .with_identifier("RunMat:MethodPrivate")
650        .build());
651    }
652    let constructor_result = crate::with_constructor_receiver(default_object.clone(), async {
653        if let Some(result) = crate::user_functions::try_call_semantic_function_by_name(
654            &ctor.function_name,
655            args,
656            requested_outputs,
657        )
658        .await
659        {
660            return Ok::<Option<Value>, RuntimeError>(Some(result?));
661        }
662        if runmat_builtins::builtin_name_is_known(&ctor.function_name)
663            && ctor.function_name != class_name
664        {
665            let result = call_builtin_async_impl(&ctor.function_name, args, output_count).await?;
666            return Ok::<Option<Value>, RuntimeError>(Some(result));
667        }
668        if let Some(result) = crate::user_functions::try_call_semantic_function_by_name(
669            &owner_qualified,
670            args,
671            requested_outputs,
672        )
673        .await
674        {
675            return Ok::<Option<Value>, RuntimeError>(Some(result?));
676        }
677        if runmat_builtins::builtin_name_is_known(&owner_qualified) && owner_qualified != class_name
678        {
679            let result = call_builtin_async_impl(&owner_qualified, args, output_count).await?;
680            return Ok::<Option<Value>, RuntimeError>(Some(result));
681        }
682        Ok::<Option<Value>, RuntimeError>(None)
683    })
684    .await?;
685    let Some(result) = constructor_result else {
686        return Ok(default_object);
687    };
688    normalize_constructor_result(default_object, result, requested_outputs)
689}
690
691fn normalize_constructor_result(
692    default_object: Value,
693    result: Value,
694    requested_outputs: usize,
695) -> Result<Value, RuntimeError> {
696    if requested_outputs != 1 {
697        return Ok(result);
698    }
699    match result {
700        Value::Struct(struct_value) => match default_object {
701            Value::Object(mut object) => {
702                for (field, value) in struct_value.fields {
703                    object.properties.insert(field, value);
704                }
705                Ok(Value::Object(object))
706            }
707            Value::HandleObject(handle) => {
708                enum ConstructorMergeStatus {
709                    Merged,
710                    InvalidHandle,
711                    NonObject,
712                }
713
714                let merged = runmat_gc::gc_with_value_mut(&handle.target, |target| {
715                    if let Value::Object(object) = target {
716                        if !crate::object_handle_flag_valid(object) {
717                            return ConstructorMergeStatus::InvalidHandle;
718                        }
719                        for (field, value) in struct_value.fields {
720                            runmat_gc::gc_record_handle_write(&handle.target, &value);
721                            object.properties.insert(field, value);
722                        }
723                        ConstructorMergeStatus::Merged
724                    } else {
725                        ConstructorMergeStatus::NonObject
726                    }
727                })
728                .map_err(|e| {
729                    build_runtime_error(format!("constructor result handle target invalid: {e}"))
730                        .build()
731                })?;
732                match merged {
733                    ConstructorMergeStatus::Merged => {}
734                    ConstructorMergeStatus::InvalidHandle => {
735                        return Err(build_runtime_error(
736                            "constructor result handle target is invalid",
737                        )
738                        .build());
739                    }
740                    ConstructorMergeStatus::NonObject => {
741                        return Err(build_runtime_error(
742                            "constructor result handle target is not an object",
743                        )
744                        .build());
745                    }
746                }
747                Ok(Value::HandleObject(handle))
748            }
749            _ => Ok(Value::Struct(struct_value)),
750        },
751        Value::Object(_) | Value::HandleObject(_) => Ok(result),
752        _ => Ok(default_object),
753    }
754}
755
756pub async fn call_builtin_async(name: &str, args: &[Value]) -> Result<Value, RuntimeError> {
757    call_builtin_async_impl(name, args, None).await
758}
759
760pub async fn call_builtin_async_with_outputs(
761    name: &str,
762    args: &[Value],
763    output_count: usize,
764) -> Result<Value, RuntimeError> {
765    call_builtin_async_impl(name, args, Some(output_count)).await
766}
767
768fn should_retry_with_gpu_gather(err: &RuntimeError, args: &[Value]) -> bool {
769    if !args.iter().any(value_contains_gpu) {
770        return false;
771    }
772    if error_chain_has_gpu_gather_retry(err, crate::GpuGatherRetry::Never) {
773        return false;
774    }
775    if args.iter().any(value_contains_explicit_gpu) {
776        return false;
777    }
778    // Compatibility errors are policy decisions. Retain this source-chain
779    // defense for wrappers that have not yet propagated an explicit policy.
780    if error_chain_has_identifier_prefix(err, "RunMat:compatibility:") {
781        return false;
782    }
783    error_chain_has_gpu_gather_retry(err, crate::GpuGatherRetry::Requested)
784}
785
786fn value_contains_explicit_gpu(value: &Value) -> bool {
787    match value {
788        Value::GpuTensor(handle) => runmat_accelerate_api::handle_is_explicit(handle),
789        Value::Cell(cell) => cell.data.iter().any(value_contains_explicit_gpu),
790        Value::Struct(value) => value.fields.values().any(value_contains_explicit_gpu),
791        Value::Object(value) => value.properties.values().any(value_contains_explicit_gpu),
792        Value::Closure(value) => value.captures.iter().any(value_contains_explicit_gpu),
793        Value::OutputList(values) => values.iter().any(value_contains_explicit_gpu),
794        _ => false,
795    }
796}
797
798fn error_chain_has_gpu_gather_retry(err: &RuntimeError, policy: crate::GpuGatherRetry) -> bool {
799    let mut current: Option<&(dyn std::error::Error + 'static)> = Some(err);
800    while let Some(error) = current {
801        if error
802            .downcast_ref::<RuntimeError>()
803            .is_some_and(|error| error.gpu_gather_retry() == policy)
804        {
805            return true;
806        }
807        current = error.source();
808    }
809    false
810}
811
812fn error_chain_has_identifier_prefix(err: &RuntimeError, prefix: &str) -> bool {
813    let mut current: Option<&(dyn std::error::Error + 'static)> = Some(err);
814    while let Some(error) = current {
815        if error
816            .downcast_ref::<RuntimeError>()
817            .and_then(RuntimeError::identifier)
818            .is_some_and(|identifier| identifier.starts_with(prefix))
819        {
820            return true;
821        }
822        current = error.source();
823    }
824    false
825}
826
827async fn gather_args_for_retry_async(args: &[Value]) -> Result<Option<Vec<Value>>, RuntimeError> {
828    let mut gathered_any = false;
829    let mut gathered_args = Vec::with_capacity(args.len());
830    for arg in args {
831        if value_contains_gpu(arg) {
832            gathered_args.push(gather_if_needed_async(arg).await?);
833            gathered_any = true;
834        } else {
835            gathered_args.push(arg.clone());
836        }
837    }
838    if gathered_any {
839        Ok(Some(gathered_args))
840    } else {
841        Ok(None)
842    }
843}
844
845#[cfg(test)]
846mod tests {
847    use super::{
848        call_builtin, gather_if_needed_async, should_retry_with_gpu_gather, value_contains_gpu,
849    };
850    use futures::executor::block_on;
851    use runmat_accelerate_api::{GpuTensorHandle, ThreadProviderGuard};
852    use runmat_types::MemberAccess;
853    use runmat_value::{Closure, StructValue, Value};
854    use runmat_value::{IntegerStorage, Tensor};
855    use std::collections::HashMap;
856    use std::sync::atomic::{AtomicU64, Ordering};
857
858    static TEST_CLASS_COUNTER: AtomicU64 = AtomicU64::new(0);
859
860    struct EmptyBuiltinService;
861
862    impl crate::context::RuntimeBuiltinService for EmptyBuiltinService {
863        fn bindings_by_name(&self, _name: &str) -> Vec<crate::builtin::RuntimeBuiltinBinding> {
864            Vec::new()
865        }
866    }
867
868    #[test]
869    fn catalog_backed_builtin_dispatches_without_legacy_authority() {
870        assert!(runmat_builtins::builtin_function_by_name("full").is_none());
871        let input = Value::Num(7.0);
872        assert_eq!(
873            call_builtin("full", std::slice::from_ref(&input)).unwrap(),
874            input
875        );
876    }
877
878    #[test]
879    fn scoped_builtin_authority_does_not_fall_back_to_global_discovery() {
880        let ports = crate::context::RuntimeServicePorts::default()
881            .with_builtin(std::rc::Rc::new(EmptyBuiltinService));
882        let runtime = crate::context::RuntimeContext::new(std::rc::Rc::new(
883            crate::execution::RuntimeExecutionService::new(),
884        ))
885        .with_service_ports(ports);
886        let _scope = runtime.enter();
887        let error = call_builtin("full", &[Value::Num(7.0)]).expect_err("exact registry miss");
888        assert_eq!(error.identifier(), Some("RunMat:UndefinedFunction"));
889    }
890
891    #[test]
892    fn operation_floating_projection_uses_native_download_and_rejects_integers() {
893        crate::builtins::common::test_support::with_test_provider(|provider| {
894            let single = Tensor::from_f32(vec![1.25, -2.5], vec![1, 2]).unwrap();
895            let single_handle =
896                crate::builtins::common::gpu_helpers::upload_tensor(provider, &single).unwrap();
897            let projected = block_on(
898                crate::builtins::common::gpu_helpers::download_floating_projection_async(
899                    provider,
900                    &single_handle,
901                ),
902            )
903            .unwrap();
904            assert_eq!(projected.data, vec![1.25, -2.5]);
905            assert_eq!(projected.shape, vec![1, 2]);
906
907            let integer =
908                Tensor::new_integer(IntegerStorage::U64(vec![1_u64 << 63, u64::MAX]), vec![1, 2])
909                    .unwrap();
910            let integer_handle =
911                crate::builtins::common::gpu_helpers::upload_tensor(provider, &integer).unwrap();
912            let error = block_on(
913                crate::builtins::common::gpu_helpers::download_floating_projection_async(
914                    provider,
915                    &integer_handle,
916                ),
917            )
918            .expect_err("floating projection must reject native integer storage");
919            assert_eq!(
920                error.identifier(),
921                Some("RunMat:gpu:IntegerFloatingProjection")
922            );
923
924            for handle in [&single_handle, &integer_handle] {
925                provider.free(handle).unwrap();
926                runmat_accelerate_api::clear_handle_metadata(handle);
927            }
928        });
929    }
930
931    #[test]
932    fn compatibility_errors_never_trigger_automatic_gpu_gather_retry() {
933        let gpu = Value::GpuTensor(GpuTensorHandle {
934            shape: vec![1, 1],
935            device_id: 0,
936            buffer_id: 1,
937            descriptor: Default::default(),
938        });
939        let compatibility_error =
940            crate::build_runtime_error("example gpuArray call form is a RunMat extension")
941                .with_identifier("RunMat:compatibility:ExampleExtension")
942                .build();
943        assert!(!should_retry_with_gpu_gather(
944            &compatibility_error,
945            std::slice::from_ref(&gpu)
946        ));
947
948        let wrapped_compatibility_error =
949            crate::build_runtime_error("GPU implementation failed while checking the call")
950                .with_identifier("RunMat:example:GpuFailure")
951                .with_source(compatibility_error)
952                .build();
953        assert!(!should_retry_with_gpu_gather(
954            &wrapped_compatibility_error,
955            std::slice::from_ref(&gpu)
956        ));
957        let requested_compatibility_error =
958            crate::build_runtime_error("host implementation requested")
959                .with_gpu_gather_retry(crate::GpuGatherRetry::Requested)
960                .with_source(
961                    crate::build_runtime_error("RunMat-only GPU form")
962                        .with_identifier("RunMat:compatibility:ExampleExtension")
963                        .build(),
964                )
965                .build();
966        assert!(!should_retry_with_gpu_gather(
967            &requested_compatibility_error,
968            std::slice::from_ref(&gpu)
969        ));
970
971        let automatic_gpu = GpuTensorHandle {
972            shape: vec![1, 1],
973            device_id: 0,
974            buffer_id: 5,
975            descriptor: Default::default(),
976        };
977        let automatic_gpu =
978            automatic_gpu.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Automatic);
979        let ordinary_gpu_error = crate::build_runtime_error("GPU input requires host fallback")
980            .with_identifier("RunMat:example:UnsupportedGpuPath")
981            .build();
982        assert!(!should_retry_with_gpu_gather(
983            &ordinary_gpu_error,
984            &[Value::GpuTensor(automatic_gpu.clone())]
985        ));
986        let automatic_gpu =
987            automatic_gpu.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Explicit);
988        assert!(!should_retry_with_gpu_gather(
989            &ordinary_gpu_error,
990            &[Value::GpuTensor(automatic_gpu.clone())]
991        ));
992        runmat_accelerate_api::clear_handle_metadata(&automatic_gpu);
993
994        let terminal_gpu_error = crate::build_runtime_error("GPU input is semantically invalid")
995            .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
996            .build();
997        assert!(!should_retry_with_gpu_gather(
998            &terminal_gpu_error,
999            &[Value::GpuTensor(GpuTensorHandle {
1000                shape: vec![1, 1],
1001                device_id: 0,
1002                buffer_id: 2,
1003                descriptor: Default::default(),
1004            })]
1005        ));
1006
1007        let nested_terminal = crate::build_runtime_error("terminal provider decision")
1008            .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
1009            .build();
1010        let wrapped_terminal = crate::build_runtime_error("GPU implementation failed")
1011            .with_source(nested_terminal)
1012            .build();
1013        assert!(!should_retry_with_gpu_gather(
1014            &wrapped_terminal,
1015            &[Value::GpuTensor(GpuTensorHandle {
1016                shape: vec![1, 1],
1017                device_id: 0,
1018                buffer_id: 3,
1019                descriptor: Default::default(),
1020            })]
1021        ));
1022
1023        let nested_request = crate::build_runtime_error("host implementation is required")
1024            .with_gpu_gather_retry(crate::GpuGatherRetry::Requested)
1025            .build();
1026        let wrapped_request = crate::build_runtime_error("provider path unavailable")
1027            .with_source(nested_request)
1028            .build();
1029        let requested_handle = GpuTensorHandle {
1030            shape: vec![1, 1],
1031            device_id: 0,
1032            buffer_id: 4,
1033            descriptor: Default::default(),
1034        };
1035        assert!(should_retry_with_gpu_gather(
1036            &wrapped_request,
1037            &[Value::GpuTensor(requested_handle.clone())]
1038        ));
1039        let requested_handle =
1040            requested_handle.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Explicit);
1041        assert!(!should_retry_with_gpu_gather(
1042            &wrapped_request,
1043            &[Value::GpuTensor(requested_handle.clone())]
1044        ));
1045        runmat_accelerate_api::clear_handle_metadata(&requested_handle);
1046    }
1047
1048    #[test]
1049    fn builtin_result_provenance_follows_gpu_input_intent() {
1050        let explicit = GpuTensorHandle {
1051            shape: vec![1, 1],
1052            device_id: 0,
1053            buffer_id: 91,
1054            descriptor: Default::default(),
1055        };
1056        let automatic = GpuTensorHandle {
1057            shape: vec![1, 1],
1058            device_id: 0,
1059            buffer_id: 92,
1060            descriptor: Default::default(),
1061        };
1062        let explicit_result = GpuTensorHandle {
1063            shape: vec![1, 1],
1064            device_id: 0,
1065            buffer_id: 93,
1066            descriptor: Default::default(),
1067        };
1068        let automatic_result = GpuTensorHandle {
1069            shape: vec![1, 1],
1070            device_id: 0,
1071            buffer_id: 94,
1072            descriptor: Default::default(),
1073        };
1074        let explicit =
1075            explicit.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Explicit);
1076        let automatic =
1077            automatic.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Automatic);
1078
1079        let mut explicit_value = Value::GpuTensor(explicit_result.clone());
1080        super::propagate_gpu_provenance(
1081            "plus",
1082            &[Value::GpuTensor(explicit.clone())],
1083            &mut explicit_value,
1084        );
1085        let mut automatic_value = Value::GpuTensor(automatic_result.clone());
1086        super::propagate_gpu_provenance(
1087            "plus",
1088            &[Value::GpuTensor(automatic.clone())],
1089            &mut automatic_value,
1090        );
1091
1092        assert_eq!(
1093            runmat_accelerate_api::handle_provenance(&explicit_result),
1094            None
1095        );
1096        let Value::GpuTensor(explicit_value) = explicit_value else {
1097            unreachable!()
1098        };
1099        assert_eq!(
1100            explicit_value.descriptor.provenance,
1101            Some(runmat_accelerate_api::GpuHandleProvenance::Explicit)
1102        );
1103        assert_eq!(
1104            runmat_accelerate_api::handle_provenance(&automatic_result),
1105            None
1106        );
1107        let Value::GpuTensor(automatic_value) = automatic_value else {
1108            unreachable!()
1109        };
1110        assert_eq!(
1111            automatic_value.descriptor.provenance,
1112            Some(runmat_accelerate_api::GpuHandleProvenance::Automatic)
1113        );
1114        for handle in [&explicit, &automatic, &explicit_result, &automatic_result] {
1115            runmat_accelerate_api::clear_handle_metadata(handle);
1116        }
1117    }
1118
1119    fn unique_class_name(prefix: &str) -> String {
1120        let id = TEST_CLASS_COUNTER.fetch_add(1, Ordering::Relaxed);
1121        format!("{}_{}", prefix, id)
1122    }
1123
1124    #[test]
1125    fn value_contains_gpu_detects_nested_closure_captures() {
1126        let value = Value::Closure(Closure {
1127            function_name: "worker".to_string(),
1128            bound_function: None,
1129            captures: vec![Value::GpuTensor(GpuTensorHandle {
1130                shape: vec![1],
1131                device_id: 999,
1132                buffer_id: 42,
1133                descriptor: Default::default(),
1134            })],
1135        });
1136        assert!(value_contains_gpu(&value));
1137    }
1138
1139    #[test]
1140    fn value_contains_gpu_detects_output_list_entries() {
1141        let value = Value::OutputList(vec![
1142            Value::Num(1.0),
1143            Value::GpuTensor(GpuTensorHandle {
1144                shape: vec![1],
1145                device_id: 998,
1146                buffer_id: 43,
1147                descriptor: Default::default(),
1148            }),
1149        ]);
1150        assert!(value_contains_gpu(&value));
1151    }
1152
1153    #[test]
1154    fn gather_if_needed_reports_provider_unavailable_for_nested_output_list_gpu() {
1155        runmat_accelerate_api::clear_provider();
1156        let _provider_guard = ThreadProviderGuard::set(None);
1157        let value = Value::OutputList(vec![Value::GpuTensor(GpuTensorHandle {
1158            shape: vec![1],
1159            // Keep device id at zero so test-only WGPU re-registration hooks are not triggered.
1160            device_id: 0,
1161            buffer_id: 44,
1162            descriptor: Default::default(),
1163        })]);
1164        let err = futures::executor::block_on(gather_if_needed_async(&value))
1165            .expect_err("missing provider should fail nested output-list gather");
1166        assert_eq!(err.identifier(), Some("RunMat:gather:ProviderUnavailable"));
1167    }
1168
1169    #[test]
1170    fn gather_if_needed_reports_provider_unavailable_for_closure_capture_gpu() {
1171        runmat_accelerate_api::clear_provider();
1172        let _provider_guard = ThreadProviderGuard::set(None);
1173        let value = Value::Closure(Closure {
1174            function_name: "worker".to_string(),
1175            bound_function: None,
1176            captures: vec![Value::GpuTensor(GpuTensorHandle {
1177                shape: vec![1],
1178                // Keep device id at zero so test-only WGPU re-registration hooks are not triggered.
1179                device_id: 0,
1180                buffer_id: 45,
1181                descriptor: Default::default(),
1182            })],
1183        });
1184        let err = futures::executor::block_on(gather_if_needed_async(&value))
1185            .expect_err("missing provider should fail closure-captured gather");
1186        assert_eq!(err.identifier(), Some("RunMat:gather:ProviderUnavailable"));
1187    }
1188
1189    #[test]
1190    fn constructor_fallback_uses_inherited_constructor_metadata_with_semantic_invoker() {
1191        let parent_name = unique_class_name("runtime_ctor_parent");
1192        let child_name = unique_class_name("runtime_ctor_child");
1193        let ctor_fn_name = unique_class_name("runtime_ctor_fn");
1194        let ctor_fn_name_for_resolver = ctor_fn_name.clone();
1195        let ctor_fn_name_for_invoker = ctor_fn_name.clone();
1196        let _resolver_guard = crate::user_functions::install_semantic_function_resolver(Some(
1197            std::sync::Arc::new(move |name| (name == ctor_fn_name_for_resolver).then_some(10101)),
1198        ));
1199        let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
1200            std::sync::Arc::new(move |function, _args, requested_outputs| {
1201                assert_eq!(function, 10101);
1202                assert_eq!(requested_outputs, 1);
1203                let mut sv = StructValue::new();
1204                sv.fields.insert("x".to_string(), Value::Num(12.0));
1205                Box::pin(async move { Ok(Value::Struct(sv)) })
1206            }),
1207        ));
1208
1209        let mut parent_methods = HashMap::new();
1210        parent_methods.insert(
1211            child_name.clone(),
1212            crate::class_registry::RuntimeMethod {
1213                name: child_name.clone(),
1214                is_static: true,
1215                is_abstract: false,
1216                is_sealed: false,
1217                access: MemberAccess::Public,
1218                function_name: ctor_fn_name_for_invoker,
1219                implicit_class_argument: None,
1220            },
1221        );
1222        crate::class_registry::register_class(crate::class_registry::RuntimeClass {
1223            name: parent_name.clone(),
1224            parent: None,
1225            properties: HashMap::new(),
1226            methods: parent_methods,
1227        });
1228        crate::class_registry::register_class(crate::class_registry::RuntimeClass {
1229            name: child_name.clone(),
1230            parent: Some(parent_name),
1231            properties: HashMap::new(),
1232            methods: HashMap::new(),
1233        });
1234
1235        let out =
1236            call_builtin(&child_name, &[]).expect("inherited static constructor should dispatch");
1237        let Value::Object(obj) = out else {
1238            panic!("expected object from constructor dispatch");
1239        };
1240        assert_eq!(obj.class_name, child_name);
1241        assert_eq!(obj.properties.get("x"), Some(&Value::Num(12.0)));
1242    }
1243
1244    #[test]
1245    fn constructor_fallback_defaults_when_constructor_is_private_or_unavailable() {
1246        let private_class_name = unique_class_name("runtime_ctor_private");
1247        let mut private_methods = HashMap::new();
1248        private_methods.insert(
1249            private_class_name.clone(),
1250            crate::class_registry::RuntimeMethod {
1251                name: private_class_name.clone(),
1252                is_static: true,
1253                is_abstract: false,
1254                is_sealed: false,
1255                access: MemberAccess::Private,
1256                function_name: "Point.origin".to_string(),
1257                implicit_class_argument: None,
1258            },
1259        );
1260        crate::class_registry::register_class(crate::class_registry::RuntimeClass {
1261            name: private_class_name.clone(),
1262            parent: None,
1263            properties: HashMap::new(),
1264            methods: private_methods,
1265        });
1266        let err = call_builtin(&private_class_name, &[])
1267            .expect_err("private constructor should enforce access before default fallback");
1268        assert_eq!(err.identifier(), Some("RunMat:MethodPrivate"));
1269
1270        let public_class_name = unique_class_name("runtime_ctor_public_no_semantic");
1271        let mut public_methods = HashMap::new();
1272        public_methods.insert(
1273            public_class_name.clone(),
1274            crate::class_registry::RuntimeMethod {
1275                name: public_class_name.clone(),
1276                is_static: true,
1277                is_abstract: false,
1278                is_sealed: false,
1279                access: MemberAccess::Public,
1280                function_name: unique_class_name("runtime_ctor_missing_body"),
1281                implicit_class_argument: None,
1282            },
1283        );
1284        crate::class_registry::register_class(crate::class_registry::RuntimeClass {
1285            name: public_class_name.clone(),
1286            parent: None,
1287            properties: HashMap::new(),
1288            methods: public_methods,
1289        });
1290
1291        let out = call_builtin(&public_class_name, &[])
1292            .expect("public ctor metadata without semantic body should default-construct");
1293        let Value::Object(obj) = out else {
1294            panic!("expected object result");
1295        };
1296        assert_eq!(obj.class_name, public_class_name);
1297    }
1298
1299    #[test]
1300    fn dotted_static_method_name_dispatches_to_registered_class_method() {
1301        let class_name = unique_class_name("runtime_static_dispatch");
1302        let fn_name = unique_class_name("runtime_static_fn");
1303        crate::class_registry::register_class(crate::class_registry::RuntimeClass {
1304            name: class_name.clone(),
1305            parent: None,
1306            properties: HashMap::new(),
1307            methods: {
1308                let mut methods = HashMap::new();
1309                methods.insert(
1310                    "zero".to_string(),
1311                    crate::class_registry::RuntimeMethod {
1312                        name: "zero".to_string(),
1313                        is_static: true,
1314                        is_abstract: false,
1315                        is_sealed: false,
1316                        access: MemberAccess::Public,
1317                        function_name: fn_name.clone(),
1318                        implicit_class_argument: None,
1319                    },
1320                );
1321                methods
1322            },
1323        });
1324
1325        let fn_name_for_resolver = fn_name.clone();
1326        let _resolver_guard = crate::user_functions::install_semantic_function_resolver(Some(
1327            std::sync::Arc::new(move |name| (name == fn_name_for_resolver).then_some(20202)),
1328        ));
1329        let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
1330            std::sync::Arc::new(move |function, _args, requested_outputs| {
1331                assert_eq!(function, 20202);
1332                assert_eq!(requested_outputs, 1);
1333                Box::pin(async { Ok(Value::Num(77.0)) })
1334            }),
1335        ));
1336
1337        let out = call_builtin(&format!("{class_name}.zero"), &[])
1338            .expect("dotted static class method call should dispatch");
1339        assert_eq!(out, Value::Num(77.0));
1340    }
1341}