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
47pub fn is_gpu_value(value: &Value) -> bool {
49 matches!(value, Value::GpuTensor(_))
50}
51
52pub 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
65pub 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
212pub 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 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 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 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 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 if matching_count == 1 || last_error.identifier().is_some() {
301 return Err(last_error);
302 }
303
304 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 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 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}