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