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