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
39pub fn is_gpu_value(value: &Value) -> bool {
41 matches!(value, Value::GpuTensor(_))
42}
43
44pub 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
57pub 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 #[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
213pub 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 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 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 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 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 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 if matching_count == 1 || last_error.identifier().is_some() {
306 return Err(last_error);
307 }
308
309 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 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 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}