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::{AccelProvider, GpuTensorHandle, GpuTensorStorage, HostTensorOwned};
3use runmat_builtins::{
4    builtin_functions, ComplexTensor, LogicalArray, NumericDType, Tensor, Value,
5};
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}
23
24impl Drop for ClassAccessContextGuard {
25    fn drop(&mut self) {
26        let previous = self.previous.take();
27        CLASS_ACCESS_CONTEXT.with(|slot| {
28            *slot.borrow_mut() = previous;
29        });
30    }
31}
32
33pub fn push_class_access_context(class_name: Option<String>) -> ClassAccessContextGuard {
34    let previous =
35        CLASS_ACCESS_CONTEXT.with(|slot| std::mem::replace(&mut *slot.borrow_mut(), class_name));
36    ClassAccessContextGuard { previous }
37}
38
39fn current_class_access_context() -> Option<String> {
40    CLASS_ACCESS_CONTEXT.with(|slot| slot.borrow().clone())
41}
42
43pub fn class_access_context() -> Option<String> {
44    current_class_access_context()
45}
46
47/// Return `true` when the passed value is a GPU-resident tensor handle.
48pub fn is_gpu_value(value: &Value) -> bool {
49    matches!(value, Value::GpuTensor(_))
50}
51
52/// Returns true when the value (or nested elements) contains any GPU-resident tensors.
53pub fn value_contains_gpu(value: &Value) -> bool {
54    match value {
55        Value::GpuTensor(_) => true,
56        Value::Cell(ca) => ca.data.iter().any(|ptr| value_contains_gpu(ptr)),
57        Value::Struct(sv) => sv.fields.values().any(value_contains_gpu),
58        Value::Object(obj) => obj.properties.values().any(value_contains_gpu),
59        Value::Closure(closure) => closure.captures.iter().any(value_contains_gpu),
60        Value::OutputList(values) => values.iter().any(value_contains_gpu),
61        _ => false,
62    }
63}
64
65/// Convert GPU-resident values to host tensors when an acceleration provider exists.
66/// Non-GPU inputs are passed through unchanged.
67pub async fn gather_if_needed_async(value: &Value) -> Result<Value, RuntimeError> {
68    gather_if_needed_async_impl(value).await
69}
70
71pub async fn download_handle_async(
72    provider: &dyn AccelProvider,
73    handle: &GpuTensorHandle,
74) -> anyhow::Result<HostTensorOwned> {
75    provider.download(handle).await
76}
77
78fn gather_if_needed_async_impl<'a>(
79    value: &'a Value,
80) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Value, RuntimeError>> + 'a>> {
81    Box::pin(async move {
82        match value {
83            Value::GpuTensor(handle) => {
84                let provider =
85                    runmat_accelerate_api::provider_for_handle(handle).ok_or_else(|| {
86                        build_runtime_error("gather: no acceleration provider registered")
87                            .with_identifier("RunMat:gather:ProviderUnavailable")
88                            .build()
89                    })?;
90                let is_logical = runmat_accelerate_api::handle_is_logical(handle);
91                let host = download_handle_async(provider, handle)
92                    .await
93                    .map_err(|err| {
94                        build_runtime_error(format!("gather: {err}"))
95                            .with_identifier("RunMat:gather:DownloadFailed")
96                            .build()
97                    })?;
98                runmat_accelerate_api::clear_residency(handle);
99                let runmat_accelerate_api::HostTensorOwned {
100                    data,
101                    shape,
102                    storage,
103                } = host;
104                if is_logical {
105                    let bits: Vec<u8> =
106                        data.iter().map(|&v| if v != 0.0 { 1 } else { 0 }).collect();
107                    let logical = LogicalArray::new(bits, shape).map_err(|e| {
108                        build_runtime_error(format!("gather: {e}"))
109                            .with_identifier("RunMat:gather:LogicalShapeError")
110                            .build()
111                    })?;
112                    Ok(Value::LogicalArray(logical))
113                } else if storage == GpuTensorStorage::ComplexInterleaved {
114                    let mut data = data;
115                    let precision = runmat_accelerate_api::handle_precision(handle)
116                        .unwrap_or_else(|| provider.precision());
117                    if matches!(precision, runmat_accelerate_api::ProviderPrecision::F32) {
118                        for value in &mut data {
119                            *value = (*value as f32) as f64;
120                        }
121                    }
122                    let mut complex = Vec::with_capacity(data.len() / 2);
123                    for chunk in data.chunks_exact(2) {
124                        complex.push((chunk[0], chunk[1]));
125                    }
126                    let tensor = ComplexTensor::new(complex, shape).map_err(|e| {
127                        build_runtime_error(format!("gather: {e}"))
128                            .with_identifier("RunMat:gather:TensorShapeError")
129                            .build()
130                    })?;
131                    Ok(Value::ComplexTensor(tensor))
132                } else {
133                    let mut data = data;
134                    let precision = runmat_accelerate_api::handle_precision(handle)
135                        .unwrap_or_else(|| provider.precision());
136                    if matches!(precision, runmat_accelerate_api::ProviderPrecision::F32) {
137                        for value in &mut data {
138                            *value = (*value as f32) as f64;
139                        }
140                    }
141                    let dtype = match precision {
142                        runmat_accelerate_api::ProviderPrecision::F32 => NumericDType::F32,
143                        runmat_accelerate_api::ProviderPrecision::F64 => NumericDType::F64,
144                    };
145                    let tensor = Tensor::new_with_dtype(data, shape, dtype).map_err(|e| {
146                        build_runtime_error(format!("gather: {e}"))
147                            .with_identifier("RunMat:gather:TensorShapeError")
148                            .build()
149                    })?;
150                    Ok(Value::Tensor(tensor))
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
198#[cfg(not(target_arch = "wasm32"))]
199pub fn gather_if_needed(value: &Value) -> Result<Value, RuntimeError> {
200    futures::executor::block_on(gather_if_needed_async(value))
201}
202
203#[cfg(target_arch = "wasm32")]
204pub fn gather_if_needed(_value: &Value) -> Result<Value, RuntimeError> {
205    Err(
206        build_runtime_error("gather: synchronous gather is unavailable on wasm")
207            .with_identifier("RunMat:gather:UnavailableOnWasm")
208            .build(),
209    )
210}
211
212/// Call a registered language builtin by name.
213/// Supports function overloading by trying different argument patterns.
214/// Returns an error if no builtin with that name and compatible arguments is found.
215pub fn call_builtin(name: &str, args: &[Value]) -> Result<Value, RuntimeError> {
216    futures::executor::block_on(call_builtin_async(name, args))
217}
218
219#[async_recursion::async_recursion(?Send)]
220async fn call_builtin_async_impl(
221    name: &str,
222    args: &[Value],
223    output_count: Option<usize>,
224) -> Result<Value, RuntimeError> {
225    ensure_wasm_builtins_registered();
226
227    let _output_guard = crate::output_count::push_output_count(output_count);
228    let mut matching_builtins = Vec::new();
229
230    // Collect all builtins with the matching name
231    for b in builtin_functions() {
232        if b.name == name {
233            matching_builtins.push(b);
234        }
235    }
236
237    if matching_builtins.is_empty() {
238        if let Some(result) = try_call_registered_instance_method(name, args, output_count).await? {
239            return Ok(result);
240        }
241        if let Some(result) = try_call_registered_static_method(name, args, output_count).await? {
242            return Ok(result);
243        }
244        // Fallback: treat as class constructor if class is registered.
245        if runmat_builtins::get_class(name).is_some() {
246            return call_registered_class_constructor(name, args, output_count).await;
247        }
248        return Err(build_runtime_error(format!("Undefined function: {name}"))
249            .with_identifier("RunMat:UndefinedFunction")
250            .build());
251    }
252
253    if let Some(result) = try_call_registered_instance_method(name, args, output_count).await? {
254        return Ok(result);
255    }
256
257    // Partition into no-category (tests/legacy shims) and categorized (library) builtins.
258    let mut no_category: Vec<&runmat_builtins::BuiltinFunction> = Vec::new();
259    let mut categorized: Vec<&runmat_builtins::BuiltinFunction> = Vec::new();
260    for b in matching_builtins {
261        if b.category.is_empty() {
262            no_category.push(b);
263        } else {
264            categorized.push(b);
265        }
266    }
267    let matching_count = no_category.len() + categorized.len();
268
269    // Try each builtin until one succeeds. Within each group, prefer later-registered
270    // implementations to allow overrides when names collide.
271    let mut last_error = RuntimeError::new("unknown error");
272    for builtin in no_category
273        .into_iter()
274        .rev()
275        .chain(categorized.into_iter().rev())
276    {
277        let f = builtin.implementation;
278        match (f)(args).await {
279            Ok(result) => return Ok(result),
280            Err(err) => {
281                if should_retry_with_gpu_gather(&err, args) {
282                    match gather_args_for_retry_async(args).await {
283                        Ok(Some(gathered_args)) => match (f)(&gathered_args).await {
284                            Ok(result) => return Ok(result),
285                            Err(retry_err) => last_error = retry_err,
286                        },
287                        Ok(None) => last_error = err,
288                        Err(gather_err) => last_error = gather_err,
289                    }
290                } else {
291                    last_error = err;
292                }
293            }
294        }
295    }
296
297    // A single implementation already knows whether its inputs are invalid or
298    // whether execution failed. Preserve that error verbatim instead of
299    // presenting it as overload resolution noise.
300    if matching_count == 1 || last_error.identifier().is_some() {
301        return Err(last_error);
302    }
303
304    // If none succeeded, return the last error
305    let identifier = last_error
306        .identifier()
307        .unwrap_or("RunMat:NoMatchingOverload")
308        .to_string();
309    let mut builder = build_runtime_error(format!(
310        "No matching overload for `{}` with {} args: {}",
311        name,
312        args.len(),
313        last_error.message()
314    ))
315    .with_source(last_error);
316    builder = builder.with_identifier(identifier);
317    Err(builder.build())
318}
319
320pub(crate) async fn try_call_registered_instance_method(
321    method_name: &str,
322    args: &[Value],
323    output_count: Option<usize>,
324) -> Result<Option<Value>, RuntimeError> {
325    let Some(receiver) = args.first() else {
326        return Ok(None);
327    };
328    let class_name = match receiver {
329        Value::Object(obj) => obj.class_name.as_str(),
330        Value::HandleObject(handle) => handle.class_name.as_str(),
331        _ => return Ok(None),
332    };
333    let Some((method, owner)) = runmat_builtins::lookup_method(class_name, method_name) else {
334        return Ok(None);
335    };
336    if method.is_static {
337        return Ok(None);
338    }
339    let caller_class = current_class_access_context();
340    let access_allowed = match method.access {
341        runmat_builtins::Access::Public => true,
342        runmat_builtins::Access::Private => caller_class.as_deref() == Some(owner.as_str()),
343        runmat_builtins::Access::Protected => caller_class
344            .as_deref()
345            .is_some_and(|caller| runmat_builtins::is_class_or_subclass(caller, &owner)),
346    };
347    if !access_allowed {
348        return Err(build_runtime_error(format!(
349            "Method '{}' is not accessible from current context.",
350            method_name
351        ))
352        .with_identifier("RunMat:MethodPrivate")
353        .build());
354    }
355    if let Some(result) = crate::user_functions::try_call_semantic_function_by_name(
356        &method.function_name,
357        args,
358        output_count.unwrap_or(1),
359    )
360    .await
361    {
362        return finalize_instance_method_result(method_name, receiver, result).map(Some);
363    }
364    if runmat_builtins::builtin_function_by_name(&method.function_name).is_some()
365        && method.function_name != method_name
366    {
367        let result = call_builtin_async_impl(&method.function_name, args, output_count).await;
368        return finalize_instance_method_result(method_name, receiver, result).map(Some);
369    }
370    let owner_qualified = format!("{owner}.{method_name}");
371    if owner_qualified != method.function_name {
372        if let Some(result) = crate::user_functions::try_call_semantic_function_by_name(
373            &owner_qualified,
374            args,
375            output_count.unwrap_or(1),
376        )
377        .await
378        {
379            return finalize_instance_method_result(method_name, receiver, result).map(Some);
380        }
381        if runmat_builtins::builtin_function_by_name(&owner_qualified).is_some()
382            && owner_qualified != method_name
383        {
384            let result = call_builtin_async_impl(&owner_qualified, args, output_count).await;
385            return finalize_instance_method_result(method_name, receiver, result).map(Some);
386        }
387    }
388    Ok(None)
389}
390
391fn finalize_instance_method_result(
392    method_name: &str,
393    receiver: &Value,
394    result: Result<Value, RuntimeError>,
395) -> Result<Value, RuntimeError> {
396    let result = result?;
397    if method_name == "delete" {
398        if let Value::HandleObject(handle) = receiver {
399            if !crate::set_handle_valid(handle, false) {
400                return Err(build_runtime_error(format!(
401                    "delete: failed to invalidate handle object '{}' after its destructor completed",
402                    handle.class_name
403                ))
404                .with_identifier("RunMat:delete:InvalidHandle")
405                .build());
406            }
407        }
408    }
409    Ok(result)
410}
411
412async fn try_call_registered_static_method(
413    qualified_name: &str,
414    args: &[Value],
415    output_count: Option<usize>,
416) -> Result<Option<Value>, RuntimeError> {
417    let Some((class_name, method_name)) = qualified_name.rsplit_once('.') else {
418        return Ok(None);
419    };
420    if class_name.trim().is_empty() || method_name.trim().is_empty() {
421        return Ok(None);
422    }
423    if runmat_builtins::get_class(class_name).is_none() {
424        return Ok(None);
425    }
426    let Some((method, owner)) = runmat_builtins::lookup_method(class_name, method_name) else {
427        return Ok(None);
428    };
429    if !method.is_static || method.access != runmat_builtins::Access::Public {
430        return Ok(None);
431    }
432    if let Some(result) = crate::user_functions::try_call_semantic_function_by_name(
433        &method.function_name,
434        args,
435        output_count.unwrap_or(1),
436    )
437    .await
438    {
439        return result.map(Some);
440    }
441    if runmat_builtins::builtin_function_by_name(&method.function_name).is_some()
442        && method.function_name != qualified_name
443    {
444        return call_builtin_async_impl(&method.function_name, args, output_count)
445            .await
446            .map(Some);
447    }
448    let owner_qualified = format!("{owner}.{method_name}");
449    if owner_qualified != method.function_name {
450        if let Some(result) = crate::user_functions::try_call_semantic_function_by_name(
451            &owner_qualified,
452            args,
453            output_count.unwrap_or(1),
454        )
455        .await
456        {
457            return result.map(Some);
458        }
459        if runmat_builtins::builtin_function_by_name(&owner_qualified).is_some()
460            && owner_qualified != qualified_name
461        {
462            return call_builtin_async_impl(&owner_qualified, args, output_count)
463                .await
464                .map(Some);
465        }
466    }
467    Ok(None)
468}
469
470async fn call_registered_class_constructor(
471    class_name: &str,
472    args: &[Value],
473    output_count: Option<usize>,
474) -> Result<Value, RuntimeError> {
475    let requested_outputs = output_count.unwrap_or(1);
476    let default_object = create_class_object(class_name.to_string()).await?;
477    let constructor_method_name = class_name.rsplit('.').next().unwrap_or(class_name);
478    let Some((ctor, owner)) = runmat_builtins::lookup_method(class_name, constructor_method_name)
479        .or_else(|| runmat_builtins::lookup_method(class_name, class_name))
480    else {
481        return Ok(default_object);
482    };
483    let owner_qualified = format!("{owner}.{constructor_method_name}");
484    let caller_class = current_class_access_context();
485    let ctor_access_allowed = match ctor.access {
486        runmat_builtins::Access::Public => true,
487        runmat_builtins::Access::Private => caller_class.as_deref() == Some(owner.as_str()),
488        runmat_builtins::Access::Protected => caller_class
489            .as_deref()
490            .is_some_and(|caller| runmat_builtins::is_class_or_subclass(caller, &owner)),
491    };
492    if !ctor_access_allowed {
493        return Err(build_runtime_error(format!(
494            "Constructor '{}' is not accessible from current context.",
495            class_name
496        ))
497        .with_identifier("RunMat:MethodPrivate")
498        .build());
499    }
500    let constructor_result = crate::with_constructor_receiver(default_object.clone(), async {
501        if let Some(result) = crate::user_functions::try_call_semantic_function_by_name(
502            &ctor.function_name,
503            args,
504            requested_outputs,
505        )
506        .await
507        {
508            return Ok::<Option<Value>, RuntimeError>(Some(result?));
509        }
510        if runmat_builtins::builtin_function_by_name(&ctor.function_name).is_some()
511            && ctor.function_name != class_name
512        {
513            let result = call_builtin_async_impl(&ctor.function_name, args, output_count).await?;
514            return Ok::<Option<Value>, RuntimeError>(Some(result));
515        }
516        if let Some(result) = crate::user_functions::try_call_semantic_function_by_name(
517            &owner_qualified,
518            args,
519            requested_outputs,
520        )
521        .await
522        {
523            return Ok::<Option<Value>, RuntimeError>(Some(result?));
524        }
525        if runmat_builtins::builtin_function_by_name(&owner_qualified).is_some()
526            && owner_qualified != class_name
527        {
528            let result = call_builtin_async_impl(&owner_qualified, args, output_count).await?;
529            return Ok::<Option<Value>, RuntimeError>(Some(result));
530        }
531        Ok::<Option<Value>, RuntimeError>(None)
532    })
533    .await?;
534    let Some(result) = constructor_result else {
535        return Ok(default_object);
536    };
537    normalize_constructor_result(default_object, result, requested_outputs)
538}
539
540fn normalize_constructor_result(
541    default_object: Value,
542    result: Value,
543    requested_outputs: usize,
544) -> Result<Value, RuntimeError> {
545    if requested_outputs != 1 {
546        return Ok(result);
547    }
548    match result {
549        Value::Struct(struct_value) => match default_object {
550            Value::Object(mut object) => {
551                for (field, value) in struct_value.fields {
552                    object.properties.insert(field, value);
553                }
554                Ok(Value::Object(object))
555            }
556            Value::HandleObject(handle) => {
557                enum ConstructorMergeStatus {
558                    Merged,
559                    InvalidHandle,
560                    NonObject,
561                }
562
563                let merged = runmat_gc::gc_with_value_mut(&handle.target, |target| {
564                    if let Value::Object(object) = target {
565                        if !crate::object_handle_flag_valid(object) {
566                            return ConstructorMergeStatus::InvalidHandle;
567                        }
568                        for (field, value) in struct_value.fields {
569                            runmat_gc::gc_record_handle_write(&handle.target, &value);
570                            object.properties.insert(field, value);
571                        }
572                        ConstructorMergeStatus::Merged
573                    } else {
574                        ConstructorMergeStatus::NonObject
575                    }
576                })
577                .map_err(|e| {
578                    build_runtime_error(format!("constructor result handle target invalid: {e}"))
579                        .build()
580                })?;
581                match merged {
582                    ConstructorMergeStatus::Merged => {}
583                    ConstructorMergeStatus::InvalidHandle => {
584                        return Err(build_runtime_error(
585                            "constructor result handle target is invalid",
586                        )
587                        .build());
588                    }
589                    ConstructorMergeStatus::NonObject => {
590                        return Err(build_runtime_error(
591                            "constructor result handle target is not an object",
592                        )
593                        .build());
594                    }
595                }
596                Ok(Value::HandleObject(handle))
597            }
598            _ => Ok(Value::Struct(struct_value)),
599        },
600        Value::Object(_) | Value::HandleObject(_) => Ok(result),
601        _ => Ok(default_object),
602    }
603}
604
605pub async fn call_builtin_async(name: &str, args: &[Value]) -> Result<Value, RuntimeError> {
606    call_builtin_async_impl(name, args, None).await
607}
608
609pub async fn call_builtin_async_with_outputs(
610    name: &str,
611    args: &[Value],
612    output_count: usize,
613) -> Result<Value, RuntimeError> {
614    call_builtin_async_impl(name, args, Some(output_count)).await
615}
616
617fn should_retry_with_gpu_gather(err: &RuntimeError, args: &[Value]) -> bool {
618    if !args.iter().any(value_contains_gpu) {
619        return false;
620    }
621    let lowered = err.message().to_ascii_lowercase();
622    lowered.contains("gpu")
623}
624
625async fn gather_args_for_retry_async(args: &[Value]) -> Result<Option<Vec<Value>>, RuntimeError> {
626    let mut gathered_any = false;
627    let mut gathered_args = Vec::with_capacity(args.len());
628    for arg in args {
629        if value_contains_gpu(arg) {
630            gathered_args.push(gather_if_needed_async(arg).await?);
631            gathered_any = true;
632        } else {
633            gathered_args.push(arg.clone());
634        }
635    }
636    if gathered_any {
637        Ok(Some(gathered_args))
638    } else {
639        Ok(None)
640    }
641}
642
643#[cfg(test)]
644mod tests {
645    use super::{call_builtin, gather_if_needed_async, value_contains_gpu};
646    use runmat_accelerate_api::{GpuTensorHandle, ThreadProviderGuard};
647    use runmat_builtins::{
648        register_class, Access, ClassDef, Closure, MethodDef, StructValue, Value,
649    };
650    use std::collections::HashMap;
651    use std::sync::atomic::{AtomicU64, Ordering};
652
653    static TEST_CLASS_COUNTER: AtomicU64 = AtomicU64::new(0);
654
655    fn unique_class_name(prefix: &str) -> String {
656        let id = TEST_CLASS_COUNTER.fetch_add(1, Ordering::Relaxed);
657        format!("{}_{}", prefix, id)
658    }
659
660    #[test]
661    fn value_contains_gpu_detects_nested_closure_captures() {
662        let value = Value::Closure(Closure {
663            function_name: "worker".to_string(),
664            bound_function: None,
665            captures: vec![Value::GpuTensor(GpuTensorHandle {
666                shape: vec![1],
667                device_id: 999,
668                buffer_id: 42,
669            })],
670        });
671        assert!(value_contains_gpu(&value));
672    }
673
674    #[test]
675    fn value_contains_gpu_detects_output_list_entries() {
676        let value = Value::OutputList(vec![
677            Value::Num(1.0),
678            Value::GpuTensor(GpuTensorHandle {
679                shape: vec![1],
680                device_id: 998,
681                buffer_id: 43,
682            }),
683        ]);
684        assert!(value_contains_gpu(&value));
685    }
686
687    #[test]
688    fn gather_if_needed_reports_provider_unavailable_for_nested_output_list_gpu() {
689        runmat_accelerate_api::clear_provider();
690        let _provider_guard = ThreadProviderGuard::set(None);
691        let value = Value::OutputList(vec![Value::GpuTensor(GpuTensorHandle {
692            shape: vec![1],
693            // Keep device id at zero so test-only WGPU re-registration hooks are not triggered.
694            device_id: 0,
695            buffer_id: 44,
696        })]);
697        let err = futures::executor::block_on(gather_if_needed_async(&value))
698            .expect_err("missing provider should fail nested output-list gather");
699        assert_eq!(err.identifier(), Some("RunMat:gather:ProviderUnavailable"));
700    }
701
702    #[test]
703    fn gather_if_needed_reports_provider_unavailable_for_closure_capture_gpu() {
704        runmat_accelerate_api::clear_provider();
705        let _provider_guard = ThreadProviderGuard::set(None);
706        let value = Value::Closure(Closure {
707            function_name: "worker".to_string(),
708            bound_function: None,
709            captures: vec![Value::GpuTensor(GpuTensorHandle {
710                shape: vec![1],
711                // Keep device id at zero so test-only WGPU re-registration hooks are not triggered.
712                device_id: 0,
713                buffer_id: 45,
714            })],
715        });
716        let err = futures::executor::block_on(gather_if_needed_async(&value))
717            .expect_err("missing provider should fail closure-captured gather");
718        assert_eq!(err.identifier(), Some("RunMat:gather:ProviderUnavailable"));
719    }
720
721    #[test]
722    fn constructor_fallback_uses_inherited_constructor_metadata_with_semantic_invoker() {
723        let parent_name = unique_class_name("runtime_ctor_parent");
724        let child_name = unique_class_name("runtime_ctor_child");
725        let ctor_fn_name = unique_class_name("runtime_ctor_fn");
726        let ctor_fn_name_for_resolver = ctor_fn_name.clone();
727        let ctor_fn_name_for_invoker = ctor_fn_name.clone();
728        let _resolver_guard = crate::user_functions::install_semantic_function_resolver(Some(
729            std::sync::Arc::new(move |name| (name == ctor_fn_name_for_resolver).then_some(10101)),
730        ));
731        let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
732            std::sync::Arc::new(move |function, _args, requested_outputs| {
733                assert_eq!(function, 10101);
734                assert_eq!(requested_outputs, 1);
735                let mut sv = StructValue::new();
736                sv.fields.insert("x".to_string(), Value::Num(12.0));
737                Box::pin(async move { Ok(Value::Struct(sv)) })
738            }),
739        ));
740
741        let mut parent_methods = HashMap::new();
742        parent_methods.insert(
743            child_name.clone(),
744            MethodDef {
745                name: child_name.clone(),
746                is_static: true,
747                is_abstract: false,
748                is_sealed: false,
749                access: Access::Public,
750                function_name: ctor_fn_name_for_invoker,
751                implicit_class_argument: None,
752            },
753        );
754        register_class(ClassDef {
755            name: parent_name.clone(),
756            parent: None,
757            properties: HashMap::new(),
758            methods: parent_methods,
759        });
760        register_class(ClassDef {
761            name: child_name.clone(),
762            parent: Some(parent_name),
763            properties: HashMap::new(),
764            methods: HashMap::new(),
765        });
766
767        let out =
768            call_builtin(&child_name, &[]).expect("inherited static constructor should dispatch");
769        let Value::Object(obj) = out else {
770            panic!("expected object from constructor dispatch");
771        };
772        assert_eq!(obj.class_name, child_name);
773        assert_eq!(obj.properties.get("x"), Some(&Value::Num(12.0)));
774    }
775
776    #[test]
777    fn constructor_fallback_defaults_when_constructor_is_private_or_unavailable() {
778        let private_class_name = unique_class_name("runtime_ctor_private");
779        let mut private_methods = HashMap::new();
780        private_methods.insert(
781            private_class_name.clone(),
782            MethodDef {
783                name: private_class_name.clone(),
784                is_static: true,
785                is_abstract: false,
786                is_sealed: false,
787                access: Access::Private,
788                function_name: "Point.origin".to_string(),
789                implicit_class_argument: None,
790            },
791        );
792        register_class(ClassDef {
793            name: private_class_name.clone(),
794            parent: None,
795            properties: HashMap::new(),
796            methods: private_methods,
797        });
798        let err = call_builtin(&private_class_name, &[])
799            .expect_err("private constructor should enforce access before default fallback");
800        assert_eq!(err.identifier(), Some("RunMat:MethodPrivate"));
801
802        let public_class_name = unique_class_name("runtime_ctor_public_no_semantic");
803        let mut public_methods = HashMap::new();
804        public_methods.insert(
805            public_class_name.clone(),
806            MethodDef {
807                name: public_class_name.clone(),
808                is_static: true,
809                is_abstract: false,
810                is_sealed: false,
811                access: Access::Public,
812                function_name: unique_class_name("runtime_ctor_missing_body"),
813                implicit_class_argument: None,
814            },
815        );
816        register_class(ClassDef {
817            name: public_class_name.clone(),
818            parent: None,
819            properties: HashMap::new(),
820            methods: public_methods,
821        });
822
823        let out = call_builtin(&public_class_name, &[])
824            .expect("public ctor metadata without semantic body should default-construct");
825        let Value::Object(obj) = out else {
826            panic!("expected object result");
827        };
828        assert_eq!(obj.class_name, public_class_name);
829    }
830
831    #[test]
832    fn dotted_static_method_name_dispatches_to_registered_class_method() {
833        let class_name = unique_class_name("runtime_static_dispatch");
834        let fn_name = unique_class_name("runtime_static_fn");
835        register_class(ClassDef {
836            name: class_name.clone(),
837            parent: None,
838            properties: HashMap::new(),
839            methods: {
840                let mut methods = HashMap::new();
841                methods.insert(
842                    "zero".to_string(),
843                    MethodDef {
844                        name: "zero".to_string(),
845                        is_static: true,
846                        is_abstract: false,
847                        is_sealed: false,
848                        access: Access::Public,
849                        function_name: fn_name.clone(),
850                        implicit_class_argument: None,
851                    },
852                );
853                methods
854            },
855        });
856
857        let fn_name_for_resolver = fn_name.clone();
858        let _resolver_guard = crate::user_functions::install_semantic_function_resolver(Some(
859            std::sync::Arc::new(move |name| (name == fn_name_for_resolver).then_some(20202)),
860        ));
861        let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
862            std::sync::Arc::new(move |function, _args, requested_outputs| {
863                assert_eq!(function, 20202);
864                assert_eq!(requested_outputs, 1);
865                Box::pin(async { Ok(Value::Num(77.0)) })
866            }),
867        ));
868
869        let out = call_builtin(&format!("{class_name}.zero"), &[])
870            .expect("dotted static class method call should dispatch");
871        assert_eq!(out, Value::Num(77.0));
872    }
873}