1#![allow(
2 clippy::await_holding_lock,
3 clippy::enum_variant_names,
4 clippy::get_first,
5 clippy::io_other_error,
6 clippy::needless_range_loop,
7 clippy::redundant_closure,
8 clippy::result_large_err,
9 clippy::too_many_arguments,
10 clippy::useless_conversion
11)]
12#![cfg_attr(target_arch = "wasm32", allow(dead_code))]
13
14use runmat_builtins::{BuiltinErrorDescriptor, Value};
15use std::future::Future;
16use std::pin::Pin;
17use std::task::{Context, Poll};
18
19pub mod analysis;
20pub mod dispatcher;
21pub mod geometry;
22pub mod operations;
23
24pub mod callsite;
25pub mod console;
26pub mod data;
27pub mod interaction;
28pub mod interrupt;
29pub mod output_context;
30pub mod output_count;
31pub mod source_context;
32
33pub mod builtins;
34pub mod comparison;
35pub mod plotting_hooks;
36pub mod replay;
37pub mod runtime_error;
38pub mod user_functions;
39pub mod warning_store;
40pub mod workspace;
41
42pub type BuiltinResult<T> = Result<T, RuntimeError>;
44
45pub const OBJECT_INDEX_PAREN: &str = "()";
46pub const OBJECT_INDEX_BRACE: &str = "{}";
47pub const OBJECT_INDEX_MEMBER: &str = ".";
48pub const CALL_METHOD_BUILTIN_NAME: &str = "call_method";
49pub const CALL_BOUND_METHOD_BUILTIN_NAME: &str = "__runmat_call_bound_method__";
50pub const OBJECT_SUBSREF_METHOD: &str = "subsref";
51pub const OBJECT_SUBSASGN_METHOD: &str = "subsasgn";
52pub(crate) const IDENT_UNDEFINED_FUNCTION: &str = "RunMat:UndefinedFunction";
53pub(crate) const HANDLE_VALID_FLAG_PROPERTY: &str = "__runmat_handle_valid__";
54
55fn object_handle_flag_valid(obj: &runmat_builtins::ObjectInstance) -> bool {
56 !matches!(
57 obj.properties.get(HANDLE_VALID_FLAG_PROPERTY),
58 Some(Value::Bool(false))
59 )
60}
61
62pub(crate) fn is_handle_valid(handle: &runmat_builtins::HandleRef) -> bool {
63 if !handle.valid {
64 return false;
65 }
66 is_handle_target_valid(handle)
67}
68
69pub(crate) fn is_handle_target_valid(handle: &runmat_builtins::HandleRef) -> bool {
70 runmat_gc::gc_with_value(&handle.target, |target| match target {
71 Value::Object(obj) => object_handle_flag_valid(obj),
72 _ => false,
73 })
74 .unwrap_or(false)
75}
76
77pub(crate) fn set_handle_valid(handle: &runmat_builtins::HandleRef, valid: bool) -> bool {
78 runmat_gc::gc_with_value_mut(&handle.target, |target| match target {
79 Value::Object(obj) => {
80 obj.properties
81 .insert(HANDLE_VALID_FLAG_PROPERTY.to_string(), Value::Bool(valid));
82 true
83 }
84 _ => false,
85 })
86 .unwrap_or(false)
87}
88
89pub fn object_property_getter_name(field: &str) -> String {
90 format!("get.{field}")
91}
92
93pub fn object_property_setter_name(field: &str) -> String {
94 format!("set.{field}")
95}
96
97pub(crate) fn current_requested_outputs() -> usize {
98 crate::output_count::current_output_count().unwrap_or(1)
99}
100
101thread_local! {
102 static CONSTRUCTOR_RECEIVER_STACK: std::cell::RefCell<Vec<Value>> =
103 const { std::cell::RefCell::new(Vec::new()) };
104}
105
106struct ConstructorReceiverPollGuard;
107
108impl Drop for ConstructorReceiverPollGuard {
109 fn drop(&mut self) {
110 CONSTRUCTOR_RECEIVER_STACK.with(|stack| {
111 stack.borrow_mut().pop();
112 });
113 }
114}
115
116fn push_constructor_receiver_for_poll(receiver: Value) -> ConstructorReceiverPollGuard {
117 CONSTRUCTOR_RECEIVER_STACK.with(|stack| {
118 stack.borrow_mut().push(receiver);
119 });
120 ConstructorReceiverPollGuard
121}
122
123pub(crate) struct ConstructorReceiverFuture<Fut> {
124 receiver: Value,
125 future: Pin<Box<Fut>>,
126}
127
128impl<Fut: Future> Future for ConstructorReceiverFuture<Fut> {
129 type Output = Fut::Output;
130
131 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
132 let _guard = push_constructor_receiver_for_poll(self.receiver.clone());
133 self.future.as_mut().poll(cx)
134 }
135}
136
137pub(crate) fn with_constructor_receiver<Fut>(
138 receiver: Value,
139 future: Fut,
140) -> ConstructorReceiverFuture<Fut>
141where
142 Fut: Future,
143{
144 ConstructorReceiverFuture {
145 receiver,
146 future: Box::pin(future),
147 }
148}
149
150fn constructor_receiver_class_name(receiver: &Value) -> Option<&str> {
151 match receiver {
152 Value::Object(obj) => Some(obj.class_name.as_str()),
153 Value::HandleObject(handle) => Some(handle.class_name.as_str()),
154 _ => None,
155 }
156}
157
158fn active_constructor_receiver_for(class_name: &str) -> Option<Value> {
159 CONSTRUCTOR_RECEIVER_STACK.with(|stack| {
160 stack
161 .borrow()
162 .iter()
163 .rev()
164 .find(|receiver| {
165 constructor_receiver_class_name(receiver).is_some_and(|receiver_class| {
166 receiver_class == class_name
167 || runmat_builtins::is_class_or_subclass(receiver_class, class_name)
168 })
169 })
170 .cloned()
171 })
172}
173
174fn undefined_callable_error(identity: &runmat_hir::CallableIdentity) -> RuntimeError {
175 let detail = format!("Undefined function for callable identity {identity:?}");
176 build_runtime_error(detail)
177 .with_identifier(IDENT_UNDEFINED_FUNCTION)
178 .build()
179}
180
181pub(crate) fn is_undefined_function_error(err: &RuntimeError) -> bool {
182 err.identifier() == Some(IDENT_UNDEFINED_FUNCTION)
183}
184
185fn build_shape_checked_cell(
186 values: Vec<Value>,
187 rows: usize,
188 cols: usize,
189 context: &str,
190) -> Result<runmat_builtins::CellArray, RuntimeError> {
191 runmat_builtins::CellArray::new(values, rows, cols).map_err(|err| {
192 build_runtime_error(format!("{context}: {err}"))
193 .with_identifier("RunMat:ShapeMismatch")
194 .build()
195 })
196}
197
198pub(crate) fn runtime_descriptor_error(
199 builtin: &'static str,
200 error: &'static BuiltinErrorDescriptor,
201) -> RuntimeError {
202 runtime_descriptor_error_with_message(builtin, error.message, error)
203}
204
205pub(crate) fn runtime_descriptor_error_with_detail(
206 builtin: &'static str,
207 error: &'static BuiltinErrorDescriptor,
208 detail: impl AsRef<str>,
209) -> RuntimeError {
210 runtime_descriptor_error_with_message(
211 builtin,
212 format!("{}: {}", error.message, detail.as_ref()),
213 error,
214 )
215}
216
217fn runtime_descriptor_error_with_message(
218 builtin: &'static str,
219 message: impl Into<String>,
220 error: &'static BuiltinErrorDescriptor,
221) -> RuntimeError {
222 let mut builder = build_runtime_error(message).with_builtin(builtin);
223 if let Some(identifier) = error.identifier {
224 builder = builder.with_identifier(identifier);
225 }
226 builder.build()
227}
228
229pub(crate) fn object_receiver_class_name(receiver: &Value) -> Option<String> {
230 match receiver {
231 Value::Object(obj) => Some(obj.class_name.clone()),
232 Value::HandleObject(handle) => {
233 let class_name = runmat_gc::gc_with_value(&handle.target, |target| match target {
234 Value::Object(obj) => obj.class_name.clone(),
235 _ => handle.class_name.clone(),
236 })
237 .unwrap_or_else(|_| handle.class_name.clone());
238 Some(class_name)
239 }
240 _ => None,
241 }
242}
243
244fn class_member_identity(class_name: &str, member: &str) -> runmat_hir::CallableIdentity {
245 runmat_hir::CallableIdentity::ExternalName(runmat_hir::QualifiedName(vec![
246 runmat_hir::SymbolName(class_name.to_string()),
247 runmat_hir::SymbolName(member.to_string()),
248 ]))
249}
250
251pub(crate) fn qualified_name_segments(name: &str) -> Vec<runmat_hir::SymbolName> {
252 name.split('.')
253 .map(|segment| runmat_hir::SymbolName(segment.to_string()))
254 .collect()
255}
256
257pub(crate) fn is_well_formed_qualified_name(name: &str) -> bool {
258 let segments = qualified_name_segments(name);
259 segments.len() > 1 && segments.iter().all(|segment| !segment.0.is_empty())
260}
261
262pub(crate) fn callable_identity_for_handle_name(
263 name: &str,
264) -> (
265 runmat_hir::CallableIdentity,
266 runmat_hir::CallableFallbackPolicy,
267) {
268 if is_well_formed_qualified_name(name) {
269 let segments = qualified_name_segments(name);
270 (
271 runmat_hir::CallableIdentity::ExternalName(runmat_hir::QualifiedName(segments)),
272 runmat_hir::CallableFallbackPolicy::ExternalBoundary,
273 )
274 } else {
275 (
276 runmat_hir::CallableIdentity::DynamicName(runmat_hir::SymbolName(name.to_string())),
277 runmat_hir::CallableFallbackPolicy::RuntimeNameResolution,
278 )
279 }
280}
281
282pub(crate) fn external_callable_identity_for_name(name: &str) -> runmat_hir::CallableIdentity {
283 if !is_well_formed_qualified_name(name) {
284 runmat_hir::CallableIdentity::ExternalName(runmat_hir::QualifiedName(vec![
285 runmat_hir::SymbolName(name.to_string()),
286 ]))
287 } else {
288 let segments = qualified_name_segments(name);
289 runmat_hir::CallableIdentity::ExternalName(runmat_hir::QualifiedName(segments))
290 }
291}
292
293pub(crate) async fn dispatch_object_external_member(
294 class_name: String,
295 member: &str,
296 args: Vec<Value>,
297 requested_outputs: usize,
298) -> BuiltinResult<Value> {
299 dispatch_callable_with_policy(
300 class_member_identity(&class_name, member),
301 runmat_hir::CallableFallbackPolicy::ExternalBoundary,
302 args,
303 requested_outputs,
304 )
305 .await
306}
307
308async fn dispatch_named_with_requested_outputs(
309 name: &str,
310 args: &[Value],
311 requested_outputs: usize,
312) -> BuiltinResult<Value> {
313 call_builtin_async_with_outputs(name, args, requested_outputs).await
314}
315
316pub(crate) async fn dispatch_callable_with_policy(
317 identity: runmat_hir::CallableIdentity,
318 fallback_policy: runmat_hir::CallableFallbackPolicy,
319 args: Vec<Value>,
320 requested_outputs: usize,
321) -> BuiltinResult<Value> {
322 let request = crate::user_functions::CallableRequest::resolved(
323 identity.clone(),
324 fallback_policy,
325 args.clone(),
326 requested_outputs,
327 );
328 if let Some(result) = crate::user_functions::try_call_semantic_descriptor(request).await {
329 return result;
330 }
331
332 if let Some(name) = fallback_policy.vm_fallback_name_for(&identity) {
333 return dispatch_named_with_requested_outputs(&name, &args, requested_outputs).await;
334 }
335
336 Err(undefined_callable_error(&identity))
337}
338
339pub async fn call_feval_async_with_outputs(
340 func_value: Value,
341 args: &[Value],
342 requested_outputs: usize,
343) -> Result<Value, RuntimeError> {
344 let _guard = crate::output_count::push_output_count(Some(requested_outputs));
345 feval_builtin(func_value, args.to_vec()).await
346}
347
348pub use runtime_error::{
349 build_runtime_error, replay_error, replay_error_with_source, CallFrame, ErrorContext,
350 ReplayErrorKind, RuntimeError, RuntimeErrorBuilder,
351};
352
353pub mod debug_context;
354
355#[cfg(feature = "blas-lapack")]
356pub mod blas;
357#[cfg(feature = "blas-lapack")]
358pub mod lapack;
359
360#[cfg(all(feature = "blas-lapack", target_os = "macos"))]
362#[link(name = "Accelerate", kind = "framework")]
363extern "C" {}
364
365#[cfg(all(feature = "blas-lapack", not(target_os = "macos")))]
367extern crate openblas_src;
368
369pub use dispatcher::{
370 call_builtin, call_builtin_async, call_builtin_async_with_outputs, class_access_context,
371 gather_if_needed, gather_if_needed_async, is_gpu_value, push_class_access_context,
372 value_contains_gpu,
373};
374
375#[cfg(feature = "plot-core")]
376pub use builtins::plotting::{
377 export_figure_scene as runtime_plot_export_figure_scene,
378 import_figure_scene_async as runtime_plot_import_figure_scene_async,
379 import_figure_scene_from_path_async as runtime_plot_import_figure_scene_from_path_async,
380};
381pub use replay::{
382 runtime_export_workspace_state, runtime_import_workspace_state, WorkspaceReplayMode,
383};
384
385pub use runmat_macros::{register_fusion_spec, register_gpu_spec};
386
387pub use builtins::common::concatenation::create_matrix_from_values;
390pub use builtins::common::elementwise::{
391 elementwise_div, elementwise_mul, elementwise_neg, elementwise_pow, power,
392};
393pub use builtins::common::indexing::perform_indexing;
394pub use builtins::common::matrix::value_matmul;
395#[cfg(feature = "blas-lapack")]
401pub use blas::*;
402#[cfg(feature = "blas-lapack")]
403pub use lapack::*;
404
405pub fn make_cell_with_shape(values: Vec<Value>, shape: Vec<usize>) -> Result<Value, String> {
406 let ca = runmat_builtins::CellArray::new_with_shape(values, shape)
407 .map_err(|e| format!("Cell creation error: {e}"))?;
408 Ok(Value::Cell(ca))
409}
410
411pub(crate) fn make_cell(values: Vec<Value>, rows: usize, cols: usize) -> Result<Value, String> {
412 make_cell_with_shape(values, vec![rows, cols])
413}
414
415fn to_string_scalar(v: &Value) -> Result<String, String> {
416 let s: String = v.try_into()?;
417 Ok(s)
418}
419
420fn to_string_array(v: &Value) -> Result<runmat_builtins::StringArray, String> {
421 match v {
422 Value::String(s) => runmat_builtins::StringArray::new(vec![s.clone()], vec![1, 1])
423 .map_err(|e| e.to_string()),
424 Value::StringArray(sa) => Ok(sa.clone()),
425 Value::CharArray(ca) => {
426 let mut out: Vec<String> = Vec::with_capacity(ca.rows);
428 for r in 0..ca.rows {
429 let mut s = String::with_capacity(ca.cols);
430 for c in 0..ca.cols {
431 s.push(ca.data[r * ca.cols + c]);
432 }
433 out.push(s);
434 }
435 runmat_builtins::StringArray::new(out, vec![ca.rows, 1]).map_err(|e| e.to_string())
436 }
437 other => Err(format!("cannot convert to string array: {other:?}")),
438 }
439}
440
441pub(crate) async fn strjoin_rowwise(a: Value, delim: Value) -> crate::BuiltinResult<Value> {
442 let d = to_string_scalar(&delim)?;
443 let sa = to_string_array(&a)?;
444 let rows = *sa.shape.first().unwrap_or(&sa.data.len());
445 let cols = *sa.shape.get(1).unwrap_or(&1);
446 if rows == 0 || cols == 0 {
447 return Ok(Value::StringArray(
448 runmat_builtins::StringArray::new(Vec::new(), vec![0, 0]).unwrap(),
449 ));
450 }
451 let mut out: Vec<String> = Vec::with_capacity(rows);
452 for r in 0..rows {
453 let mut s = String::new();
454 for c in 0..cols {
455 if c > 0 {
456 s.push_str(&d);
457 }
458 s.push_str(&sa.data[r + c * rows]);
459 }
460 out.push(s);
461 }
462 Ok(Value::StringArray(
463 runmat_builtins::StringArray::new(out, vec![rows, 1])
464 .map_err(|e| format!("strjoin: {e}"))?,
465 ))
466}
467
468pub(crate) async fn deal_builtin(rest: Vec<Value>) -> crate::BuiltinResult<Value> {
469 if let Some(out_count) = crate::output_count::current_output_count() {
470 if out_count == 0 {
471 return Ok(Value::OutputList(Vec::new()));
472 }
473 if out_count > 1 {
474 return Ok(crate::output_count::output_list_with_padding(
475 out_count, rest,
476 ));
477 }
478 }
479 let cols = rest.len();
481 make_cell(rest, 1, cols).map_err(Into::into)
482}
483
484pub(crate) async fn rethrow_builtin(e: Value) -> crate::BuiltinResult<Value> {
487 match e {
488 Value::MException(me) => Err(build_runtime_error(me.message)
489 .with_identifier(me.identifier)
490 .build()),
491 Value::String(s) => Err(build_runtime_error(s).build()),
492 other => Err(build_runtime_error(format!("RunMat:error: {other:?}")).build()),
493 }
494}
495
496pub(crate) async fn new_handle_object_builtin(class_name: String) -> crate::BuiltinResult<Value> {
499 let obj = create_class_object(class_name.clone()).await?;
501 let gc = runmat_gc::gc_allocate(obj).map_err(|e| format!("gc: {e}"))?;
502 Ok(Value::HandleObject(runmat_builtins::HandleRef {
503 class_name,
504 target: gc,
505 valid: true,
506 }))
507}
508
509pub(crate) async fn isvalid_builtin(v: Value) -> crate::BuiltinResult<Value> {
510 match v {
511 Value::HandleObject(h) => Ok(Value::Bool(crate::is_handle_valid(&h))),
512 Value::Listener(l) => Ok(Value::Bool(l.valid && l.enabled)),
513 _ => Ok(Value::Bool(false)),
514 }
515}
516
517use std::cell::RefCell;
518
519#[derive(Default)]
520struct EventRegistry {
521 next_id: u64,
522 listeners: std::collections::HashMap<(usize, String), Vec<runmat_builtins::Listener>>,
523 listener_roots: std::collections::HashMap<u64, ListenerRoots>,
524}
525
526struct ListenerRoots {
527 _target: runmat_gc::ExplicitRoot,
528 _callback: runmat_gc::ExplicitRoot,
529}
530
531thread_local! {
532 static EVENT_REGISTRY: RefCell<EventRegistry> = RefCell::new(EventRegistry::default());
533}
534
535#[cfg(test)]
536fn reset_event_registry_for_test() {
537 EVENT_REGISTRY.with(|registry| {
538 *registry.borrow_mut() = EventRegistry::default();
539 });
540}
541
542pub(crate) fn invalidate_listener_registration(listener_id: u64) {
543 EVENT_REGISTRY.with(|registry| {
544 let mut registry = registry.borrow_mut();
545 for listeners in registry.listeners.values_mut() {
546 for listener in listeners.iter_mut() {
547 if listener.id == listener_id {
548 listener.valid = false;
549 listener.enabled = false;
550 }
551 }
552 }
553 registry.listener_roots.remove(&listener_id);
554 });
555}
556
557pub(crate) fn canonicalize_callback_handle_for_semantic_resolution(callback: Value) -> Value {
558 fn normalize_handle_name(text: &str) -> Option<String> {
559 let trimmed = text.trim();
560 let name = trimmed.strip_prefix('@').unwrap_or(trimmed).trim();
561 (!name.is_empty()).then(|| name.to_string())
562 }
563
564 fn resolve_text_handle(text: &str) -> Option<Value> {
565 let name = normalize_handle_name(text)?;
566 let function = crate::user_functions::resolve_semantic_function_by_name(&name)?;
567 Some(Value::BoundFunctionHandle { name, function })
568 }
569
570 match callback {
571 Value::String(text) => resolve_text_handle(&text).unwrap_or_else(|| {
572 crate::builtins::introspection::function_handle_text::dispatch_str2func(Value::String(
573 text.clone(),
574 ))
575 .unwrap_or(Value::String(text))
576 }),
577 Value::StringArray(array) if array.data.len() == 1 => {
578 let text = &array.data[0];
579 resolve_text_handle(text).unwrap_or_else(|| {
580 crate::builtins::introspection::function_handle_text::dispatch_str2func(
581 Value::StringArray(array.clone()),
582 )
583 .unwrap_or(Value::StringArray(array))
584 })
585 }
586 Value::CharArray(chars) if chars.rows == 1 => {
587 let text: String = chars.data.iter().collect();
588 resolve_text_handle(&text).unwrap_or_else(|| {
589 crate::builtins::introspection::function_handle_text::dispatch_str2func(
590 Value::CharArray(chars.clone()),
591 )
592 .unwrap_or(Value::CharArray(chars))
593 })
594 }
595 Value::FunctionHandle(name) => {
596 if let Some(function) = crate::user_functions::resolve_semantic_function_by_name(&name)
597 {
598 Value::BoundFunctionHandle { name, function }
599 } else {
600 Value::FunctionHandle(name)
601 }
602 }
603 Value::ExternalFunctionHandle(name) => {
604 if is_well_formed_qualified_name(&name) {
605 if let Some(function) =
606 crate::user_functions::resolve_semantic_function_by_name(&name)
607 {
608 return Value::BoundFunctionHandle { name, function };
609 }
610 }
611 Value::ExternalFunctionHandle(name)
612 }
613 Value::MethodFunctionHandle(name) => {
614 if let Some(function) = crate::user_functions::resolve_semantic_function_by_name(&name)
615 {
616 Value::BoundFunctionHandle { name, function }
617 } else {
618 Value::MethodFunctionHandle(name)
619 }
620 }
621 Value::Closure(mut closure) => {
622 if closure.bound_function.is_none() {
623 if let Some(function) =
624 crate::user_functions::resolve_semantic_function_by_name(&closure.function_name)
625 {
626 closure.bound_function = Some(function);
627 }
628 }
629 Value::Closure(closure)
630 }
631 other => other,
632 }
633}
634
635fn canonicalize_listener_callback(callback: Value) -> Value {
636 canonicalize_callback_handle_for_semantic_resolution(callback)
637}
638
639pub(crate) async fn addlistener_builtin(
640 target: Value,
641 event_name: String,
642 callback: Value,
643) -> crate::BuiltinResult<Value> {
644 let key_ptr: usize = match &target {
645 Value::HandleObject(h) => {
646 if !crate::is_handle_valid(h) {
647 return Err(build_runtime_error("addlistener: target handle is invalid")
648 .with_builtin("addlistener")
649 .with_identifier("RunMat:AddListenerTargetInvalid")
650 .build());
651 }
652 runmat_gc::gc_handle_addr(&h.target)
653 }
654 Value::Object(_) => {
655 return Err(
656 build_runtime_error("addlistener: target object must be a handle object")
657 .with_builtin("addlistener")
658 .with_identifier("RunMat:AddListenerTargetInvalid")
659 .build(),
660 )
661 }
662 _ => {
663 return Err(
664 build_runtime_error("addlistener: target must be handle or object")
665 .with_builtin("addlistener")
666 .with_identifier("RunMat:AddListenerTargetInvalid")
667 .build(),
668 )
669 }
670 };
671 let id = EVENT_REGISTRY.with(|registry| {
672 let mut registry = registry.borrow_mut();
673 registry.next_id += 1;
674 registry.next_id
675 });
676 let (target_root, target_class_name) = match target {
677 Value::HandleObject(h) => {
678 let class_name = h.class_name.clone();
679 (
680 runmat_gc::gc_root(h.target).map_err(|e| format!("gc: {e}"))?,
681 class_name,
682 )
683 }
684 _ => unreachable!(),
685 };
686 let callback = canonicalize_listener_callback(callback);
687 let callback_root = runmat_gc::gc_allocate_rooted(callback).map_err(|e| format!("gc: {e}"))?;
688 let listener = runmat_builtins::Listener {
689 id,
690 target: target_root.handle(),
691 target_class_name,
692 event_name: event_name.clone(),
693 callback: callback_root.handle(),
694 enabled: true,
695 valid: true,
696 };
697 EVENT_REGISTRY.with(|registry| {
698 let mut registry = registry.borrow_mut();
699 registry
700 .listeners
701 .entry((key_ptr, event_name))
702 .or_default()
703 .push(listener.clone());
704 registry.listener_roots.insert(
705 id,
706 ListenerRoots {
707 _target: target_root,
708 _callback: callback_root,
709 },
710 );
711 });
712 Ok(Value::Listener(listener))
713}
714
715pub(crate) async fn notify_builtin(
716 target: Value,
717 event_name: String,
718 rest: Vec<Value>,
719) -> crate::BuiltinResult<Value> {
720 let key_ptr: usize = match &target {
721 Value::HandleObject(h) => {
722 if !crate::is_handle_valid(h) {
723 return Err(build_runtime_error("notify: target handle is invalid")
724 .with_builtin("notify")
725 .with_identifier("RunMat:NotifyTargetInvalid")
726 .build());
727 }
728 runmat_gc::gc_handle_addr(&h.target)
729 }
730 Value::Object(_) => {
731 return Err(
732 build_runtime_error("notify: target object must be a handle object")
733 .with_builtin("notify")
734 .with_identifier("RunMat:NotifyTargetInvalid")
735 .build(),
736 )
737 }
738 _ => {
739 return Err(
740 build_runtime_error("notify: target must be handle or object")
741 .with_builtin("notify")
742 .with_identifier("RunMat:NotifyTargetInvalid")
743 .build(),
744 )
745 }
746 };
747 let mut to_call: Vec<runmat_builtins::Listener> = Vec::new();
748 EVENT_REGISTRY.with(|registry| {
749 let registry = registry.borrow();
750 if let Some(list) = registry.listeners.get(&(key_ptr, event_name.clone())) {
751 for l in list {
752 if l.valid && l.enabled {
753 to_call.push(l.clone());
754 }
755 }
756 }
757 });
758 for l in to_call {
759 let mut args = Vec::new();
761 args.push(target.clone());
762 args.extend(rest.iter().cloned());
763 let cbv: Value = runmat_gc::gc_clone_value(&l.callback).map_err(|e| {
764 build_runtime_error(format!("notify: invalid listener callback handle: {e}"))
765 .with_builtin("notify")
766 .with_identifier("RunMat:NotifyInvalidCallback")
767 .build()
768 })?;
769 let should_dispatch = match &cbv {
770 Value::String(s) => !s.trim().is_empty(),
771 Value::StringArray(sa) => sa.data.len() == 1 && !sa.data[0].trim().is_empty(),
772 Value::CharArray(ca) if ca.rows == 1 => {
773 let text: String = ca.data.iter().collect();
774 !text.trim().is_empty()
775 }
776 Value::FunctionHandle(_)
777 | Value::ExternalFunctionHandle(_)
778 | Value::MethodFunctionHandle(_)
779 | Value::BoundFunctionHandle { .. }
780 | Value::Closure(_) => true,
781 _ => false,
782 };
783 if should_dispatch {
784 let _ = call_feval_async_with_outputs(cbv.clone(), &args, 0).await?;
785 }
786 }
787 Ok(Value::Num(0.0))
788}
789
790pub(crate) async fn get_p_builtin(obj: Value) -> crate::BuiltinResult<Value> {
794 match obj {
795 Value::Object(o) => {
796 if let Some(v) = o.properties.get("p_backing") {
797 Ok(v.clone())
798 } else {
799 Ok(Value::Num(0.0))
800 }
801 }
802 other => Err(build_runtime_error(format!(
803 "get.p: requires object receiver (got {other:?})"
804 ))
805 .with_builtin("get.p")
806 .with_identifier("RunMat:GetPReceiverInvalid")
807 .build()),
808 }
809}
810
811pub(crate) async fn set_p_builtin(obj: Value, val: Value) -> crate::BuiltinResult<Value> {
812 match obj {
813 Value::Object(mut o) => {
814 o.properties.insert("p_backing".to_string(), val);
815 Ok(Value::Object(o))
816 }
817 other => Err(build_runtime_error(format!(
818 "set.p: requires object receiver (got {other:?})"
819 ))
820 .with_builtin("set.p")
821 .with_identifier("RunMat:SetPReceiverInvalid")
822 .build()),
823 }
824}
825
826pub(crate) async fn make_anon_builtin(params: String, body: String) -> crate::BuiltinResult<Value> {
827 Ok(Value::String(format!("@anon({params}) {body}")))
828}
829
830pub async fn create_class_object(class_name: String) -> crate::BuiltinResult<Value> {
831 if runmat_builtins::is_class_abstract(&class_name) {
832 return Err(build_runtime_error(format!(
833 "Cannot instantiate abstract class '{}'.",
834 class_name
835 ))
836 .with_identifier("RunMat:AbstractMethodMissing")
837 .build());
838 }
839 if let Some(def) = runmat_builtins::get_class(&class_name) {
840 let mut chain: Vec<runmat_builtins::ClassDef> = Vec::new();
842 let mut is_handle_class = false;
843 let mut visited = std::collections::HashSet::new();
844 let mut cursor: Option<String> = Some(def.name.clone());
846 while let Some(name) = cursor {
847 if name.eq_ignore_ascii_case("handle") {
848 is_handle_class = true;
849 break;
850 }
851 if !visited.insert(name.clone()) {
852 break;
853 }
854 if let Some(cd) = runmat_builtins::get_class(&name) {
855 if cd
856 .parent
857 .as_ref()
858 .is_some_and(|parent| parent.eq_ignore_ascii_case("handle"))
859 {
860 is_handle_class = true;
861 }
862 chain.push(cd.clone());
863 cursor = cd.parent.clone();
864 } else {
865 break;
866 }
867 }
868 chain.reverse();
870 let mut obj = runmat_builtins::ObjectInstance::new(def.name.clone());
871 let empty_default = || {
873 Value::Tensor(runmat_builtins::Tensor::new(vec![], vec![0, 0]).expect("empty tensor"))
874 };
875 for cd in chain {
876 for (k, p) in cd.properties.iter() {
877 if !p.is_static {
878 obj.properties.insert(
879 k.clone(),
880 p.default_value.clone().unwrap_or_else(empty_default),
881 );
882 }
883 }
884 }
885 if is_handle_class {
886 let gc = runmat_gc::gc_allocate(Value::Object(obj)).map_err(|e| format!("gc: {e}"))?;
887 Ok(Value::HandleObject(runmat_builtins::HandleRef {
888 class_name: def.name.clone(),
889 target: gc,
890 valid: true,
891 }))
892 } else {
893 Ok(Value::Object(obj))
894 }
895 } else {
896 Ok(Value::Object(runmat_builtins::ObjectInstance::new(
897 class_name,
898 )))
899 }
900}
901
902pub async fn call_super_constructor(
903 class_name: String,
904 super_class_name: String,
905 args: Vec<Value>,
906) -> crate::BuiltinResult<Value> {
907 let receiver = if let Some(active) = active_constructor_receiver_for(&class_name) {
908 active
909 } else {
910 create_class_object(class_name).await?
911 };
912 let ctor_result = with_constructor_receiver(receiver.clone(), async {
913 let ctor_name = super_class_name
914 .rsplit('.')
915 .next()
916 .filter(|name| !name.trim().is_empty())
917 .unwrap_or(super_class_name.as_str());
918 let ctor_lookup = runmat_builtins::lookup_method(&super_class_name, ctor_name)
919 .or_else(|| runmat_builtins::lookup_method(&super_class_name, &super_class_name));
920 let Some((ctor, _owner)) = ctor_lookup else {
921 return Ok::<Option<Value>, RuntimeError>(None);
922 };
923 let Some(result) = crate::user_functions::try_call_semantic_function_by_name(
924 &ctor.function_name,
925 &args,
926 1,
927 )
928 .await
929 else {
930 return Ok::<Option<Value>, RuntimeError>(None);
931 };
932 Ok::<Option<Value>, RuntimeError>(Some(result?))
933 })
934 .await?;
935 let Some(ctor_result) = ctor_result else {
936 return Ok(receiver);
937 };
938 fn merge_parent_props_into_object(
939 receiver_obj: &mut runmat_builtins::ObjectInstance,
940 ctor_result: Value,
941 owner: Option<&runmat_gc::GcHandle>,
942 ) -> Result<(), RuntimeError> {
943 match ctor_result {
944 Value::Object(parent_obj) => {
945 for (name, value) in parent_obj.properties {
946 if let Some(owner) = owner {
947 runmat_gc::gc_record_handle_write(owner, &value);
948 }
949 receiver_obj.properties.insert(name, value);
950 }
951 }
952 Value::HandleObject(parent_handle) => {
953 if let Some(owner) = owner {
954 if parent_handle.target == *owner {
955 if parent_handle.valid {
956 return Ok(());
957 }
958 return Err(build_runtime_error(
959 "super constructor returned invalid parent handle",
960 )
961 .build());
962 }
963 }
964 if !is_handle_valid(&parent_handle) {
965 return Err(build_runtime_error(
966 "super constructor returned invalid parent handle",
967 )
968 .build());
969 }
970 match runmat_gc::gc_clone_value(&parent_handle.target).map_err(|e| {
971 build_runtime_error(format!(
972 "super constructor returned stale parent handle: {e}"
973 ))
974 .build()
975 })? {
976 Value::Object(parent_obj) => {
977 for (name, value) in parent_obj.properties {
978 if let Some(owner) = owner {
979 runmat_gc::gc_record_handle_write(owner, &value);
980 }
981 receiver_obj.properties.insert(name, value);
982 }
983 }
984 _ => {
985 return Err(build_runtime_error(
986 "super constructor returned non-object parent handle",
987 )
988 .build());
989 }
990 }
991 }
992 Value::Struct(parent_fields) => {
993 for (name, value) in parent_fields.fields {
994 if let Some(owner) = owner {
995 runmat_gc::gc_record_handle_write(owner, &value);
996 }
997 receiver_obj.properties.insert(name, value);
998 }
999 }
1000 _ => {}
1001 }
1002 Ok(())
1003 }
1004 match receiver {
1005 Value::Object(mut receiver_obj) => {
1006 merge_parent_props_into_object(&mut receiver_obj, ctor_result, None)?;
1007 Ok(Value::Object(receiver_obj))
1008 }
1009 Value::HandleObject(handle) => {
1010 let merged = runmat_gc::gc_with_value_mut(&handle.target, |target| {
1011 if let Value::Object(receiver_obj) = target {
1012 if !object_handle_flag_valid(receiver_obj) {
1013 return Err(build_runtime_error(
1014 "super constructor receiver handle is invalid",
1015 )
1016 .build());
1017 }
1018 merge_parent_props_into_object(receiver_obj, ctor_result, Some(&handle.target))
1019 } else {
1020 Err(
1021 build_runtime_error("super constructor receiver target is not an object")
1022 .build(),
1023 )
1024 }
1025 })
1026 .map_err(|e| {
1027 build_runtime_error(format!("super constructor receiver invalid: {e}")).build()
1028 })?;
1029 merged?;
1030 Ok(Value::HandleObject(handle))
1031 }
1032 _ => Ok(receiver),
1033 }
1034}
1035
1036pub async fn call_super_method(
1037 class_name: String,
1038 super_class_name: String,
1039 method_name: String,
1040 args: Vec<Value>,
1041) -> crate::BuiltinResult<Value> {
1042 let Some((method, owner)) = runmat_builtins::lookup_method(&super_class_name, &method_name)
1043 else {
1044 return Err(build_runtime_error(format!(
1045 "Undefined superclass method '{}@{}'",
1046 method_name, super_class_name
1047 ))
1048 .with_identifier("RunMat:UndefinedFunction")
1049 .build());
1050 };
1051 if method.is_static {
1052 return Err(build_runtime_error(format!(
1053 "Superclass method '{}@{}' is static and cannot be called with super method syntax.",
1054 method_name, super_class_name
1055 ))
1056 .with_identifier("RunMat:MethodStaticAccess")
1057 .build());
1058 }
1059 let access_allowed = match method.access {
1060 runmat_builtins::Access::Public => true,
1061 runmat_builtins::Access::Protected => {
1062 runmat_builtins::is_class_or_subclass(&class_name, &owner)
1063 }
1064 runmat_builtins::Access::Private => class_name == owner,
1065 };
1066 if !access_allowed {
1067 return Err(build_runtime_error(format!(
1068 "Method '{}@{}' is not accessible from class '{}'.",
1069 method_name, super_class_name, class_name
1070 ))
1071 .with_identifier("RunMat:MethodPrivate")
1072 .build());
1073 }
1074 let Some(result) =
1075 crate::user_functions::try_call_semantic_function_by_name(&method.function_name, &args, 1)
1076 .await
1077 else {
1078 return Err(
1079 build_runtime_error(format!("Undefined function: {}", method.function_name))
1080 .with_identifier("RunMat:UndefinedFunction")
1081 .build(),
1082 );
1083 };
1084 result
1085}
1086
1087pub(crate) async fn classref_builtin(class_name: String) -> crate::BuiltinResult<Value> {
1090 Ok(Value::ClassRef(class_name))
1091}
1092
1093pub(crate) async fn register_test_classes_builtin() -> crate::BuiltinResult<Value> {
1094 use runmat_builtins::*;
1095 let mut props = std::collections::HashMap::new();
1096 props.insert(
1097 "x".to_string(),
1098 PropertyDef {
1099 name: "x".to_string(),
1100 is_static: false,
1101 is_constant: false,
1102 is_dependent: false,
1103 get_access: Access::Public,
1104 set_access: Access::Public,
1105 default_value: Some(Value::Num(0.0)),
1106 },
1107 );
1108 props.insert(
1109 "y".to_string(),
1110 PropertyDef {
1111 name: "y".to_string(),
1112 is_static: false,
1113 is_constant: false,
1114 is_dependent: false,
1115 get_access: Access::Public,
1116 set_access: Access::Public,
1117 default_value: Some(Value::Num(0.0)),
1118 },
1119 );
1120 props.insert(
1121 "staticValue".to_string(),
1122 PropertyDef {
1123 name: "staticValue".to_string(),
1124 is_static: true,
1125 is_constant: false,
1126 is_dependent: false,
1127 get_access: Access::Public,
1128 set_access: Access::Public,
1129 default_value: Some(Value::Num(42.0)),
1130 },
1131 );
1132 props.insert(
1133 "secret".to_string(),
1134 PropertyDef {
1135 name: "secret".to_string(),
1136 is_static: false,
1137 is_constant: false,
1138 is_dependent: false,
1139 get_access: Access::Private,
1140 set_access: Access::Private,
1141 default_value: Some(Value::Num(99.0)),
1142 },
1143 );
1144 let mut methods = std::collections::HashMap::new();
1145 methods.insert(
1146 "move".to_string(),
1147 MethodDef {
1148 name: "move".to_string(),
1149 is_static: false,
1150 is_abstract: false,
1151 is_sealed: false,
1152 access: Access::Public,
1153 function_name: "Point.move".to_string(),
1154 implicit_class_argument: None,
1155 },
1156 );
1157 methods.insert(
1158 "origin".to_string(),
1159 MethodDef {
1160 name: "origin".to_string(),
1161 is_static: true,
1162 is_abstract: false,
1163 is_sealed: false,
1164 access: Access::Public,
1165 function_name: "Point.origin".to_string(),
1166 implicit_class_argument: None,
1167 },
1168 );
1169 runmat_builtins::register_class(ClassDef {
1170 name: "Point".to_string(),
1171 parent: None,
1172 properties: props,
1173 methods,
1174 });
1175
1176 let mut ns_props = std::collections::HashMap::new();
1178 ns_props.insert(
1179 "x".to_string(),
1180 PropertyDef {
1181 name: "x".to_string(),
1182 is_static: false,
1183 is_constant: false,
1184 is_dependent: false,
1185 get_access: Access::Public,
1186 set_access: Access::Public,
1187 default_value: Some(Value::Num(1.0)),
1188 },
1189 );
1190 ns_props.insert(
1191 "y".to_string(),
1192 PropertyDef {
1193 name: "y".to_string(),
1194 is_static: false,
1195 is_constant: false,
1196 is_dependent: false,
1197 get_access: Access::Public,
1198 set_access: Access::Public,
1199 default_value: Some(Value::Num(2.0)),
1200 },
1201 );
1202 let ns_methods = std::collections::HashMap::new();
1203 runmat_builtins::register_class(ClassDef {
1204 name: "pkg.PointNS".to_string(),
1205 parent: None,
1206 properties: ns_props,
1207 methods: ns_methods,
1208 });
1209
1210 let shape_props = std::collections::HashMap::new();
1212 let mut shape_methods = std::collections::HashMap::new();
1213 shape_methods.insert(
1214 "area".to_string(),
1215 MethodDef {
1216 name: "area".to_string(),
1217 is_static: false,
1218 is_abstract: false,
1219 is_sealed: false,
1220 access: Access::Public,
1221 function_name: "Shape.area".to_string(),
1222 implicit_class_argument: None,
1223 },
1224 );
1225 runmat_builtins::register_class(ClassDef {
1226 name: "Shape".to_string(),
1227 parent: None,
1228 properties: shape_props,
1229 methods: shape_methods,
1230 });
1231
1232 let mut circle_props = std::collections::HashMap::new();
1233 circle_props.insert(
1234 "r".to_string(),
1235 PropertyDef {
1236 name: "r".to_string(),
1237 is_static: false,
1238 is_constant: false,
1239 is_dependent: false,
1240 get_access: Access::Public,
1241 set_access: Access::Public,
1242 default_value: Some(Value::Num(0.0)),
1243 },
1244 );
1245 let mut circle_methods = std::collections::HashMap::new();
1246 circle_methods.insert(
1247 "area".to_string(),
1248 MethodDef {
1249 name: "area".to_string(),
1250 is_static: false,
1251 is_abstract: false,
1252 is_sealed: false,
1253 access: Access::Public,
1254 function_name: "Circle.area".to_string(),
1255 implicit_class_argument: None,
1256 },
1257 );
1258 runmat_builtins::register_class(ClassDef {
1259 name: "Circle".to_string(),
1260 parent: Some("Shape".to_string()),
1261 properties: circle_props,
1262 methods: circle_methods,
1263 });
1264
1265 let ctor_props = std::collections::HashMap::new();
1267 let mut ctor_methods = std::collections::HashMap::new();
1268 ctor_methods.insert(
1269 "Ctor".to_string(),
1270 MethodDef {
1271 name: "Ctor".to_string(),
1272 is_static: true,
1273 is_abstract: false,
1274 is_sealed: false,
1275 access: Access::Public,
1276 function_name: "Ctor.Ctor".to_string(),
1277 implicit_class_argument: None,
1278 },
1279 );
1280 runmat_builtins::register_class(ClassDef {
1281 name: "Ctor".to_string(),
1282 parent: None,
1283 properties: ctor_props,
1284 methods: ctor_methods,
1285 });
1286
1287 let overidx_props = std::collections::HashMap::new();
1289 let mut overidx_methods = std::collections::HashMap::new();
1290 overidx_methods.insert(
1291 OBJECT_SUBSREF_METHOD.to_string(),
1292 MethodDef {
1293 name: OBJECT_SUBSREF_METHOD.to_string(),
1294 is_static: false,
1295 is_abstract: false,
1296 is_sealed: false,
1297 access: Access::Public,
1298 function_name: format!("OverIdx.{OBJECT_SUBSREF_METHOD}"),
1299 implicit_class_argument: None,
1300 },
1301 );
1302 overidx_methods.insert(
1303 OBJECT_SUBSASGN_METHOD.to_string(),
1304 MethodDef {
1305 name: OBJECT_SUBSASGN_METHOD.to_string(),
1306 is_static: false,
1307 is_abstract: false,
1308 is_sealed: false,
1309 access: Access::Public,
1310 function_name: format!("OverIdx.{OBJECT_SUBSASGN_METHOD}"),
1311 implicit_class_argument: None,
1312 },
1313 );
1314 overidx_methods.insert(
1315 crate::builtins::introspection::object_indexing::NUM_ARGUMENTS_FROM_SUBSCRIPT_METHOD
1316 .to_string(),
1317 MethodDef {
1318 name:
1319 crate::builtins::introspection::object_indexing::NUM_ARGUMENTS_FROM_SUBSCRIPT_METHOD
1320 .to_string(),
1321 is_static: false,
1322 is_abstract: false,
1323 is_sealed: false,
1324 access: Access::Public,
1325 function_name: format!(
1326 "OverIdx.{}",
1327 crate::builtins::introspection::object_indexing::NUM_ARGUMENTS_FROM_SUBSCRIPT_METHOD
1328 ),
1329 implicit_class_argument: None,
1330 },
1331 );
1332 for (name, is_static) in [
1333 (
1334 crate::builtins::introspection::object_serialization::SAVEOBJ_METHOD,
1335 false,
1336 ),
1337 (
1338 crate::builtins::introspection::object_serialization::LOADOBJ_METHOD,
1339 true,
1340 ),
1341 ] {
1342 overidx_methods.insert(
1343 name.to_string(),
1344 MethodDef {
1345 name: name.to_string(),
1346 is_static,
1347 is_abstract: false,
1348 is_sealed: false,
1349 access: Access::Public,
1350 function_name: format!("OverIdx.{name}"),
1351 implicit_class_argument: None,
1352 },
1353 );
1354 }
1355 for name in [
1356 "plus", "times", "mtimes", "lt", "gt", "eq", "uplus", "rdivide", "mrdivide", "ldivide",
1357 "mldivide", "and", "or", "xor",
1358 ] {
1359 overidx_methods.insert(
1360 name.to_string(),
1361 MethodDef {
1362 name: name.to_string(),
1363 is_static: false,
1364 is_abstract: false,
1365 is_sealed: false,
1366 access: Access::Public,
1367 function_name: format!("OverIdx.{name}"),
1368 implicit_class_argument: None,
1369 },
1370 );
1371 }
1372 runmat_builtins::register_class(ClassDef {
1373 name: "OverIdx".to_string(),
1374 parent: None,
1375 properties: overidx_props,
1376 methods: overidx_methods,
1377 });
1378
1379 runmat_builtins::register_class(ClassDef {
1381 name: "NoIdx".to_string(),
1382 parent: None,
1383 properties: std::collections::HashMap::new(),
1384 methods: std::collections::HashMap::new(),
1385 });
1386 Ok(Value::Num(1.0))
1387}
1388
1389#[cfg(feature = "test-classes")]
1390pub async fn test_register_classes() {
1391 let _ = register_test_classes_builtin().await;
1392}
1393
1394const FEVAL_ERROR_HANDLE_NAME_INVALID: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
1396 code: "RM.FEVAL.HANDLE_NAME_INVALID",
1397 identifier: Some("RunMat:FevalHandleNameInvalid"),
1398 when: "A function or method handle name is empty.",
1399 message: "feval: function handle name must not be empty",
1400};
1401
1402const FEVAL_ERROR_HANDLE_STRING_INVALID: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
1403 code: "RM.FEVAL.HANDLE_STRING_INVALID",
1404 identifier: Some("RunMat:FevalHandleStringInvalid"),
1405 when: "Text handle input does not start with '@'.",
1406 message: "feval: expected function handle string starting with '@'",
1407};
1408
1409const FEVAL_ERROR_HANDLE_SHAPE_INVALID: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
1410 code: "RM.FEVAL.HANDLE_SHAPE_INVALID",
1411 identifier: Some("RunMat:FevalHandleShapeInvalid"),
1412 when: "Text handle input has invalid char/string array shape.",
1413 message: "feval: function handle text input must be scalar row text",
1414};
1415
1416const FEVAL_ERROR_SEMANTIC_UNAVAILABLE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
1417 code: "RM.FEVAL.SEMANTIC_UNAVAILABLE",
1418 identifier: Some("RunMat:SemanticFunctionUnavailable"),
1419 when: "Semantic function identity cannot be invoked in current runtime state.",
1420 message: "feval: semantic function handle is unavailable",
1421};
1422
1423const FEVAL_ERROR_FUNCTION_VALUE_UNSUPPORTED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
1424 code: "RM.FEVAL.FUNCTION_VALUE_UNSUPPORTED",
1425 identifier: Some("RunMat:FevalFunctionValueUnsupported"),
1426 when: "The first argument is not a supported callable value.",
1427 message: "feval: unsupported function value",
1428};
1429
1430pub(crate) const FEVAL_ERRORS: [BuiltinErrorDescriptor; 5] = [
1431 FEVAL_ERROR_HANDLE_NAME_INVALID,
1432 FEVAL_ERROR_HANDLE_STRING_INVALID,
1433 FEVAL_ERROR_HANDLE_SHAPE_INVALID,
1434 FEVAL_ERROR_SEMANTIC_UNAVAILABLE,
1435 FEVAL_ERROR_FUNCTION_VALUE_UNSUPPORTED,
1436];
1437
1438pub(crate) async fn feval_builtin(f: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
1439 fn normalize_feval_handle_name(name: &str) -> Option<String> {
1440 let trimmed = name.trim();
1441 (!trimmed.is_empty()).then(|| trimmed.to_string())
1442 }
1443
1444 async fn call_by_identity(
1445 identity: runmat_hir::CallableIdentity,
1446 fallback_policy: runmat_hir::CallableFallbackPolicy,
1447 args: &[Value],
1448 requested_outputs: usize,
1449 ) -> crate::BuiltinResult<Value> {
1450 dispatch_callable_with_policy(identity, fallback_policy, args.to_vec(), requested_outputs)
1451 .await
1452 }
1453
1454 async fn call_by_name(
1455 name: &str,
1456 args: &[Value],
1457 requested_outputs: usize,
1458 ) -> crate::BuiltinResult<Value> {
1459 let normalized = normalize_feval_handle_name(name)
1460 .ok_or_else(|| runtime_descriptor_error("feval", &FEVAL_ERROR_HANDLE_NAME_INVALID))?;
1461 let (identity, fallback_policy) = callable_identity_for_handle_name(&normalized);
1462 call_by_identity(identity, fallback_policy, args, requested_outputs).await
1463 }
1464
1465 let requested_outputs = crate::output_count::current_output_count().unwrap_or(1);
1466
1467 match f {
1468 Value::String(s) => {
1470 if let Some(name) = s.strip_prefix('@') {
1471 call_by_name(name, &rest, requested_outputs).await
1472 } else {
1473 Err(runtime_descriptor_error_with_detail(
1474 "feval",
1475 &FEVAL_ERROR_HANDLE_STRING_INVALID,
1476 format!("got {s}"),
1477 ))
1478 }
1479 }
1480 Value::CharArray(ca) => {
1482 if ca.rows == 1 {
1483 let s: String = ca.data.iter().collect();
1484 if let Some(name) = s.strip_prefix('@') {
1485 call_by_name(name, &rest, requested_outputs).await
1486 } else {
1487 Err(runtime_descriptor_error_with_detail(
1488 "feval",
1489 &FEVAL_ERROR_HANDLE_STRING_INVALID,
1490 format!("got {s}"),
1491 ))
1492 }
1493 } else {
1494 Err(runtime_descriptor_error_with_detail(
1495 "feval",
1496 &FEVAL_ERROR_HANDLE_SHAPE_INVALID,
1497 "char array must be a row vector",
1498 ))
1499 }
1500 }
1501 Value::StringArray(sa) => {
1502 if sa.data.len() == 1 {
1503 let s = &sa.data[0];
1504 if let Some(name) = s.strip_prefix('@') {
1505 call_by_name(name, &rest, requested_outputs).await
1506 } else {
1507 Err(runtime_descriptor_error_with_detail(
1508 "feval",
1509 &FEVAL_ERROR_HANDLE_STRING_INVALID,
1510 format!("got {s}"),
1511 ))
1512 }
1513 } else {
1514 Err(runtime_descriptor_error_with_detail(
1515 "feval",
1516 &FEVAL_ERROR_HANDLE_SHAPE_INVALID,
1517 "string array must be scalar",
1518 ))
1519 }
1520 }
1521 Value::FunctionHandle(name) => call_by_name(&name, &rest, requested_outputs).await,
1522 Value::ExternalFunctionHandle(name) => call_by_name(&name, &rest, requested_outputs).await,
1523 Value::MethodFunctionHandle(name) => {
1524 let method_name = name.trim().to_string();
1525 if method_name.is_empty() {
1526 return Err(runtime_descriptor_error(
1527 "feval",
1528 &FEVAL_ERROR_HANDLE_NAME_INVALID,
1529 ));
1530 }
1531 dispatch_callable_with_policy(
1532 runmat_hir::CallableIdentity::Method(runmat_hir::MethodId(method_name)),
1533 runmat_hir::CallableFallbackPolicy::RuntimeNameResolution,
1534 rest,
1535 requested_outputs,
1536 )
1537 .await
1538 }
1539 Value::BoundFunctionHandle { name, function } => {
1540 let request = crate::user_functions::CallableRequest::semantic(
1541 function,
1542 rest.clone(),
1543 requested_outputs,
1544 );
1545 if let Some(result) = crate::user_functions::try_call_semantic_descriptor(request).await
1546 {
1547 return result;
1548 }
1549 Err(runtime_descriptor_error_with_detail(
1550 "feval",
1551 &FEVAL_ERROR_SEMANTIC_UNAVAILABLE,
1552 format!("semantic function handle '{name}' ({function}) is unavailable"),
1553 ))
1554 }
1555 Value::Closure(c) => {
1556 if let Some(function) = c.bound_function {
1557 let mut args = c.captures.clone();
1558 args.extend(rest);
1559 let request = crate::user_functions::CallableRequest::semantic(
1560 function,
1561 args.clone(),
1562 requested_outputs,
1563 );
1564 if let Some(result) =
1565 crate::user_functions::try_call_semantic_descriptor(request).await
1566 {
1567 return result;
1568 }
1569 return Err(runtime_descriptor_error_with_detail(
1570 "feval",
1571 &FEVAL_ERROR_SEMANTIC_UNAVAILABLE,
1572 format!(
1573 "semantic closure '{}' ({function}) is unavailable",
1574 c.function_name
1575 ),
1576 ));
1577 }
1578
1579 if c.function_name == CALL_METHOD_BUILTIN_NAME && c.captures.len() >= 2 {
1580 let base = c.captures[0].clone();
1581 let method = match &c.captures[1] {
1582 Value::String(name) => name.clone(),
1583 Value::CharArray(chars) if chars.rows == 1 => chars.data.iter().collect(),
1584 _ => {
1585 return Err(build_runtime_error(
1586 "call_method: closure captures must include method name text",
1587 )
1588 .with_builtin("call_method")
1589 .with_identifier("RunMat:CallMethodNameInvalid")
1590 .build())
1591 }
1592 };
1593 let mut method_args = c.captures.iter().skip(2).cloned().collect::<Vec<_>>();
1594 method_args.extend(rest);
1595 return crate::builtins::introspection::call_method::dispatch_call_method(
1596 base,
1597 method,
1598 method_args,
1599 )
1600 .await;
1601 }
1602
1603 let mut args = c.captures.clone();
1604 args.extend(rest);
1605 if let Some(function) =
1606 crate::user_functions::resolve_semantic_function_by_name(&c.function_name)
1607 {
1608 let request = crate::user_functions::CallableRequest::semantic(
1609 function,
1610 args.clone(),
1611 requested_outputs,
1612 );
1613 if let Some(result) =
1614 crate::user_functions::try_call_semantic_descriptor(request).await
1615 {
1616 return result;
1617 }
1618 }
1619 call_by_name(&c.function_name, &args, requested_outputs).await
1620 }
1621 receiver @ Value::Object(_) | receiver @ Value::HandleObject(_) => {
1622 let payload = Value::Cell(build_shape_checked_cell(
1623 rest.clone(),
1624 1,
1625 rest.len(),
1626 "feval object index payload",
1627 )?);
1628 crate::builtins::introspection::object_indexing::dispatch_subsref(
1629 receiver,
1630 OBJECT_INDEX_PAREN.to_string(),
1631 payload,
1632 )
1633 .await
1634 }
1635 other => Err(runtime_descriptor_error_with_detail(
1636 "feval",
1637 &FEVAL_ERROR_FUNCTION_VALUE_UNSUPPORTED,
1638 format!("{other:?}"),
1639 )),
1640 }
1641}
1642
1643#[cfg(test)]
1644mod tests {
1645 use super::*;
1646 use crate::builtins::introspection::test_methods::*;
1647 use futures::executor::block_on;
1648 use runmat_builtins::{register_class, Access, ClassDef, HandleRef, PropertyDef};
1649 use std::collections::HashMap;
1650 use std::sync::{
1651 atomic::{AtomicU64, AtomicUsize, Ordering},
1652 Arc, Mutex,
1653 };
1654
1655 static TEST_CLASS_COUNTER: AtomicU64 = AtomicU64::new(0);
1656 static LISTENER_TEST_LOCK: Mutex<()> = Mutex::new(());
1657
1658 fn unique_class_name(prefix: &str) -> String {
1659 let id = TEST_CLASS_COUNTER.fetch_add(1, Ordering::Relaxed);
1660 format!("{}_{}", prefix, id)
1661 }
1662
1663 fn listener_gc_test(test: impl FnOnce()) {
1664 let _guard = LISTENER_TEST_LOCK
1665 .lock()
1666 .unwrap_or_else(|poisoned| poisoned.into_inner());
1667 reset_event_registry_for_test();
1668 test();
1669 reset_event_registry_for_test();
1670 }
1671
1672 #[test]
1673 fn descriptor_migration_covers_lib_runtime_builtins() {
1674 let cases = [
1675 ("deal", "[varargout] = deal(varargin)"),
1676 ("rethrow", "rethrow(err)"),
1677 ("call_method", "[out] = call_method(base, method, varargin)"),
1678 (
1679 "new_handle_object",
1680 "handle = new_handle_object(class_name)",
1681 ),
1682 (
1683 "addlistener",
1684 "listener = addlistener(target, event_name, callback)",
1685 ),
1686 ("notify", "status = notify(target, event_name, varargin)"),
1687 ("get.p", "value = get.p(obj)"),
1688 ("set.p", "obj = set.p(obj, value)"),
1689 ("make_anon", "handle_text = make_anon(params, body)"),
1690 ("classref", "ref = classref(class_name)"),
1691 (
1692 "__register_test_classes",
1693 "status = __register_test_classes()",
1694 ),
1695 ("Point.move", "obj = Point.move(obj, dx, dy)"),
1696 ("Circle.area", "area = Circle.area(obj)"),
1697 ("Ctor.Ctor", "obj = Ctor.Ctor(x)"),
1698 ("PkgF.foo", "value = PkgF.foo()"),
1699 ("OverIdx.plus", "out = OverIdx.plus(obj, rhs)"),
1700 (
1701 "OverIdx.subsref",
1702 "out = OverIdx.subsref(obj, kind, payload)",
1703 ),
1704 ("feval", "[varargout] = feval(f, varargin)"),
1705 ("str2func", "fh = str2func(name)"),
1706 ("func2str", "name = func2str(fh)"),
1707 ("functions", "info = functions(fh)"),
1708 ("inputname", "name = inputname(argNumber)"),
1709 ("localfunctions", "handles = localfunctions()"),
1710 ("narginchk", "narginchk(minArgs, maxArgs)"),
1711 ("nargoutchk", "nargoutchk(minArgs, maxArgs)"),
1712 ("mfilename", "name = mfilename()"),
1713 ("getmethod", "fh = getmethod(obj_or_class, name)"),
1714 ];
1715
1716 for (name, label) in cases {
1717 let builtin = runmat_builtins::builtin_function_by_name(name)
1718 .unwrap_or_else(|| panic!("builtin {name} not registered"));
1719 let descriptor = builtin
1720 .descriptor
1721 .unwrap_or_else(|| panic!("descriptor missing for {name}"));
1722 assert!(
1723 descriptor.signatures.iter().any(|sig| sig.label == label),
1724 "missing signature {label} for {name}"
1725 );
1726 }
1727 }
1728
1729 #[test]
1730 fn non_object_handle_targets_are_invalid() {
1731 let target = runmat_gc::gc_allocate(Value::Num(1.0)).expect("gc allocate target");
1732 let handle = HandleRef {
1733 class_name: "MalformedHandle".to_string(),
1734 target,
1735 valid: true,
1736 };
1737
1738 assert!(!is_handle_valid(&handle));
1739 }
1740
1741 #[test]
1742 fn feval_closure_uses_semantic_function_identity() {
1743 let _guard = crate::user_functions::install_semantic_function_invoker(Some(Arc::new(
1744 |function, args, requested_outputs| {
1745 assert_eq!(function, 42);
1746 assert_eq!(requested_outputs, 1);
1747 assert_eq!(args, &[Value::Num(2.0)]);
1748 Box::pin(async { Ok(Value::Num(7.0)) })
1749 },
1750 )));
1751 let closure = Value::Closure(runmat_builtins::Closure {
1752 function_name: "function_target".to_string(),
1753 bound_function: Some(42),
1754 captures: Vec::new(),
1755 });
1756
1757 let result = block_on(feval_builtin(closure, vec![Value::Num(2.0)]))
1758 .expect("semantic closure feval succeeds");
1759 assert_eq!(result, Value::Num(7.0));
1760 }
1761
1762 #[test]
1763 fn feval_semantic_function_handle_uses_semantic_identity() {
1764 let _guard = crate::user_functions::install_semantic_function_invoker(Some(Arc::new(
1765 |function, args, requested_outputs| {
1766 assert_eq!(function, 43);
1767 assert_eq!(requested_outputs, 1);
1768 assert_eq!(args, &[Value::Num(3.0)]);
1769 Box::pin(async { Ok(Value::Num(9.0)) })
1770 },
1771 )));
1772 let handle = Value::BoundFunctionHandle {
1773 name: "function_target".to_string(),
1774 function: 43,
1775 };
1776
1777 let result = block_on(feval_builtin(handle, vec![Value::Num(3.0)]))
1778 .expect("semantic function handle feval succeeds");
1779 assert_eq!(result, Value::Num(9.0));
1780 }
1781
1782 #[test]
1783 fn feval_semantic_function_handle_errors_when_semantic_invoker_unavailable() {
1784 let _guard = crate::user_functions::install_semantic_function_invoker(None);
1785 let handle = Value::BoundFunctionHandle {
1786 name: "function_target".to_string(),
1787 function: 9043,
1788 };
1789
1790 let err = block_on(feval_builtin(handle, vec![Value::Num(3.0)])).expect_err(
1791 "semantic function handle should not fall back to name-based dispatch when unavailable",
1792 );
1793 assert_eq!(err.identifier(), Some("RunMat:SemanticFunctionUnavailable"));
1794 assert!(
1795 err.message()
1796 .contains("semantic function handle 'function_target' (9043) is unavailable"),
1797 "unexpected error: {err:?}"
1798 );
1799 }
1800
1801 #[test]
1802 fn feval_name_only_handle_uses_semantic_resolver() {
1803 let _resolver_guard =
1804 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
1805 (name == "resolved_target").then_some(45)
1806 })));
1807 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
1808 Arc::new(|function, args, requested_outputs| {
1809 assert_eq!(function, 45);
1810 assert_eq!(requested_outputs, 1);
1811 assert_eq!(args, &[Value::Num(4.0)]);
1812 Box::pin(async { Ok(Value::Num(11.0)) })
1813 }),
1814 ));
1815
1816 let result = block_on(feval_builtin(
1817 Value::FunctionHandle("resolved_target".to_string()),
1818 vec![Value::Num(4.0)],
1819 ))
1820 .expect("resolved name-only handle feval succeeds");
1821 assert_eq!(result, Value::Num(11.0));
1822 }
1823
1824 #[test]
1825 fn feval_method_function_handle_uses_semantic_resolver() {
1826 let _resolver_guard =
1827 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
1828 (name == "resolved_method").then_some(5045)
1829 })));
1830 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
1831 Arc::new(|function, args, requested_outputs| {
1832 assert_eq!(function, 5045);
1833 assert_eq!(requested_outputs, 1);
1834 assert_eq!(args, &[Value::Num(4.0)]);
1835 Box::pin(async { Ok(Value::Num(15.0)) })
1836 }),
1837 ));
1838
1839 let result = block_on(feval_builtin(
1840 Value::MethodFunctionHandle("resolved_method".to_string()),
1841 vec![Value::Num(4.0)],
1842 ))
1843 .expect("resolved method handle feval succeeds");
1844 assert_eq!(result, Value::Num(15.0));
1845 }
1846
1847 #[test]
1848 fn feval_method_function_handle_does_not_fallback_to_builtin_name() {
1849 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
1850 let err = block_on(feval_builtin(
1851 Value::MethodFunctionHandle("sqrt".to_string()),
1852 vec![Value::Num(9.0)],
1853 ))
1854 .expect_err("method function handle should not fallback to builtin name dispatch");
1855 assert_eq!(err.identifier(), Some("RunMat:UndefinedFunction"));
1856 }
1857
1858 #[test]
1859 fn feval_name_only_closure_uses_semantic_resolver() {
1860 let _resolver_guard =
1861 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
1862 (name == "resolved_target").then_some(145)
1863 })));
1864 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
1865 Arc::new(|function, args, requested_outputs| {
1866 assert_eq!(function, 145);
1867 assert_eq!(requested_outputs, 1);
1868 assert_eq!(args, &[Value::Num(9.0), Value::Num(4.0)]);
1869 Box::pin(async { Ok(Value::Num(13.0)) })
1870 }),
1871 ));
1872
1873 let closure = Value::Closure(runmat_builtins::Closure {
1874 function_name: "resolved_target".to_string(),
1875 bound_function: None,
1876 captures: vec![Value::Num(9.0)],
1877 });
1878
1879 let result = block_on(feval_builtin(closure, vec![Value::Num(4.0)]))
1880 .expect("resolved name-only closure feval succeeds");
1881 assert_eq!(result, Value::Num(13.0));
1882 }
1883
1884 #[test]
1885 fn feval_name_only_closure_falls_back_when_semantic_invoker_unavailable() {
1886 let _resolver_guard =
1887 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
1888 (name == "sin").then_some(245)
1889 })));
1890 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(None);
1891
1892 let closure = Value::Closure(runmat_builtins::Closure {
1893 function_name: "sin".to_string(),
1894 bound_function: None,
1895 captures: Vec::new(),
1896 });
1897
1898 let result =
1899 block_on(feval_builtin(closure, vec![Value::Num(0.0)])).expect("sin fallback works");
1900 assert_eq!(result, Value::Num(0.0));
1901 }
1902
1903 #[test]
1904 fn feval_external_function_handle_errors_when_unresolved() {
1905 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
1906 let err = block_on(feval_builtin(
1907 Value::ExternalFunctionHandle("missing.external".to_string()),
1908 vec![Value::Num(1.0)],
1909 ))
1910 .expect_err("external function handle should error when unresolved");
1911 assert_eq!(err.identifier(), Some("RunMat:UndefinedFunction"));
1912 assert!(
1913 err.message().contains("missing.external"),
1914 "unexpected error: {err:?}"
1915 );
1916 }
1917
1918 #[test]
1919 fn feval_single_segment_external_function_handle_uses_runtime_name_resolution() {
1920 let _resolver_guard =
1921 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
1922 (name == "resolved_target").then_some(4501)
1923 })));
1924 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
1925 Arc::new(|function, args, requested_outputs| {
1926 assert_eq!(function, 4501);
1927 assert_eq!(requested_outputs, 1);
1928 assert_eq!(args, &[Value::Num(4.0)]);
1929 Box::pin(async { Ok(Value::Num(12.0)) })
1930 }),
1931 ));
1932
1933 let result = block_on(feval_builtin(
1934 Value::ExternalFunctionHandle("resolved_target".to_string()),
1935 vec![Value::Num(4.0)],
1936 ))
1937 .expect("single-segment external function handle should use runtime-name resolution");
1938 assert_eq!(result, Value::Num(12.0));
1939 }
1940
1941 #[test]
1942 fn feval_rejects_string_without_at_with_identifier() {
1943 let err = block_on(feval_builtin(
1944 Value::String("sin".to_string()),
1945 vec![Value::Num(0.0)],
1946 ))
1947 .expect_err("feval string handle without @ should fail");
1948 assert_eq!(err.identifier(), Some("RunMat:FevalHandleStringInvalid"));
1949 }
1950
1951 #[test]
1952 fn feval_rejects_char_handle_without_at_with_identifier() {
1953 let err = block_on(feval_builtin(
1954 Value::CharArray(runmat_builtins::CharArray::new_row("sin")),
1955 vec![Value::Num(0.0)],
1956 ))
1957 .expect_err("feval char handle without @ should fail");
1958 assert_eq!(err.identifier(), Some("RunMat:FevalHandleStringInvalid"));
1959 }
1960
1961 #[test]
1962 fn feval_rejects_non_row_char_handle_with_identifier() {
1963 let chars = runmat_builtins::CharArray::new(vec!['@', 's'], 2, 1)
1964 .expect("char array construction should succeed");
1965 let err = block_on(feval_builtin(
1966 Value::CharArray(chars),
1967 vec![Value::Num(0.0)],
1968 ))
1969 .expect_err("feval non-row char handle should fail");
1970 assert_eq!(err.identifier(), Some("RunMat:FevalHandleShapeInvalid"));
1971 }
1972
1973 #[test]
1974 fn feval_rejects_empty_at_string_handle_with_identifier() {
1975 let err = block_on(feval_builtin(
1976 Value::String("@".to_string()),
1977 vec![Value::Num(0.0)],
1978 ))
1979 .expect_err("feval empty @string handle should fail");
1980 assert_eq!(err.identifier(), Some("RunMat:FevalHandleNameInvalid"));
1981 }
1982
1983 #[test]
1984 fn feval_rejects_empty_function_handle_value_with_identifier() {
1985 let err = block_on(feval_builtin(
1986 Value::FunctionHandle(String::new()),
1987 vec![Value::Num(0.0)],
1988 ))
1989 .expect_err("feval empty function-handle value should fail");
1990 assert_eq!(err.identifier(), Some("RunMat:FevalHandleNameInvalid"));
1991 }
1992
1993 #[test]
1994 fn feval_trims_text_handle_name_for_resolution() {
1995 let _resolver_guard =
1996 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
1997 (name == "resolved_target").then_some(9876)
1998 })));
1999 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
2000 Arc::new(|function, args, requested_outputs| {
2001 assert_eq!(function, 9876);
2002 assert_eq!(requested_outputs, 1);
2003 assert_eq!(args, &[Value::Num(4.0)]);
2004 Box::pin(async { Ok(Value::Num(12.0)) })
2005 }),
2006 ));
2007
2008 let value = block_on(feval_builtin(
2009 Value::String("@ resolved_target ".to_string()),
2010 vec![Value::Num(4.0)],
2011 ))
2012 .expect("trimmed text handle should resolve");
2013 assert_eq!(value, Value::Num(12.0));
2014 }
2015
2016 #[test]
2017 fn str2func_returns_semantic_handle_when_resolver_can_resolve() {
2018 let _resolver_guard =
2019 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2020 (name == "resolved_target").then_some(145)
2021 })));
2022 let value = crate::builtins::introspection::function_handle_text::dispatch_str2func(
2023 Value::String("resolved_target".to_string()),
2024 )
2025 .expect("str2func should succeed");
2026 assert_eq!(
2027 value,
2028 Value::BoundFunctionHandle {
2029 name: "resolved_target".to_string(),
2030 function: 145,
2031 }
2032 );
2033 }
2034
2035 #[test]
2036 fn str2func_returns_dynamic_handle_when_resolver_cannot_resolve() {
2037 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
2038 let value = crate::builtins::introspection::function_handle_text::dispatch_str2func(
2039 Value::String("@missing_target".to_string()),
2040 )
2041 .expect("str2func should succeed");
2042 assert_eq!(value, Value::FunctionHandle("missing_target".to_string()));
2043 }
2044
2045 #[test]
2046 fn str2func_returns_external_handle_for_qualified_name() {
2047 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
2048 let value = crate::builtins::introspection::function_handle_text::dispatch_str2func(
2049 Value::String("Point.origin".to_string()),
2050 )
2051 .expect("str2func should succeed");
2052 assert_eq!(
2053 value,
2054 Value::ExternalFunctionHandle("Point.origin".to_string())
2055 );
2056 }
2057
2058 #[test]
2059 fn str2func_malformed_qualified_name_returns_dynamic_handle() {
2060 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
2061 let value = crate::builtins::introspection::function_handle_text::dispatch_str2func(
2062 Value::String("Point..origin".to_string()),
2063 )
2064 .expect("str2func should succeed");
2065 assert_eq!(value, Value::FunctionHandle("Point..origin".to_string()));
2066 }
2067
2068 #[test]
2069 fn func2str_rejects_non_handle_with_identifier() {
2070 let err = crate::builtins::introspection::function_handle_text::dispatch_func2str(
2071 Value::Num(1.0),
2072 )
2073 .expect_err("func2str non-handle input should fail");
2074 assert_eq!(err.identifier(), Some("RunMat:Func2StrHandleTypeInvalid"));
2075 }
2076
2077 #[test]
2078 fn str2func_rejects_empty_name_with_identifier() {
2079 let err = crate::builtins::introspection::function_handle_text::dispatch_str2func(
2080 Value::String(" ".to_string()),
2081 )
2082 .expect_err("empty function name should fail");
2083 assert_eq!(err.identifier(), Some("RunMat:Str2FuncNameInvalid"));
2084 }
2085
2086 #[test]
2087 fn str2func_rejects_non_row_char_name_with_identifier() {
2088 let chars = runmat_builtins::CharArray::new(vec!['a', 'b'], 2, 1)
2089 .expect("char array construction should succeed");
2090 let err = crate::builtins::introspection::function_handle_text::dispatch_str2func(
2091 Value::CharArray(chars),
2092 )
2093 .expect_err("non-row char-array function name should fail");
2094 assert_eq!(err.identifier(), Some("RunMat:Str2FuncNameShapeInvalid"));
2095 }
2096
2097 #[test]
2098 fn str2func_rejects_non_text_name_with_identifier() {
2099 let err = crate::builtins::introspection::function_handle_text::dispatch_str2func(
2100 Value::Num(1.0),
2101 )
2102 .expect_err("non-text function name should fail");
2103 assert_eq!(err.identifier(), Some("RunMat:Str2FuncNameTypeInvalid"));
2104 }
2105
2106 #[test]
2107 fn str2func_accepts_scalar_string_array_name() {
2108 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
2109 let value = crate::builtins::introspection::function_handle_text::dispatch_str2func(
2110 Value::StringArray(
2111 runmat_builtins::StringArray::new(vec!["@missing_target".to_string()], vec![1, 1])
2112 .expect("string array construction should succeed"),
2113 ),
2114 )
2115 .expect("scalar string-array function name should succeed");
2116 assert_eq!(value, Value::FunctionHandle("missing_target".to_string()));
2117 }
2118
2119 #[test]
2120 fn str2func_rejects_nonscalar_string_array_name_with_identifier() {
2121 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
2122 let value = Value::StringArray(
2123 runmat_builtins::StringArray::new(vec!["@a".to_string(), "@b".to_string()], vec![1, 2])
2124 .expect("string array construction should succeed"),
2125 );
2126 let err = crate::builtins::introspection::function_handle_text::dispatch_str2func(value)
2127 .expect_err("nonscalar string-array function name must fail");
2128 assert_eq!(err.identifier(), Some("RunMat:Str2FuncNameShapeInvalid"));
2129 }
2130
2131 #[test]
2132 fn str2func_scalar_string_array_prefers_semantic_handle_when_resolved() {
2133 let _resolver_guard =
2134 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2135 (name == "resolved_target").then_some(445)
2136 })));
2137 let value = crate::builtins::introspection::function_handle_text::dispatch_str2func(
2138 Value::StringArray(
2139 runmat_builtins::StringArray::new(vec!["@resolved_target".to_string()], vec![1, 1])
2140 .expect("string array construction should succeed"),
2141 ),
2142 )
2143 .expect("scalar string-array function name should resolve semantically");
2144 assert_eq!(
2145 value,
2146 Value::BoundFunctionHandle {
2147 name: "resolved_target".to_string(),
2148 function: 445,
2149 }
2150 );
2151 }
2152
2153 #[test]
2154 fn str2func_scalar_string_array_returns_external_handle_for_qualified_name() {
2155 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
2156 let value = crate::builtins::introspection::function_handle_text::dispatch_str2func(
2157 Value::StringArray(
2158 runmat_builtins::StringArray::new(vec!["Point.origin".to_string()], vec![1, 1])
2159 .expect("string array construction should succeed"),
2160 ),
2161 )
2162 .expect("scalar string-array qualified name should succeed");
2163 assert_eq!(
2164 value,
2165 Value::ExternalFunctionHandle("Point.origin".to_string())
2166 );
2167 }
2168
2169 #[test]
2170 fn str2func_scalar_string_array_malformed_qualified_name_returns_dynamic_handle() {
2171 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
2172 let value = crate::builtins::introspection::function_handle_text::dispatch_str2func(
2173 Value::StringArray(
2174 runmat_builtins::StringArray::new(vec!["Point..origin".to_string()], vec![1, 1])
2175 .expect("string array construction should succeed"),
2176 ),
2177 )
2178 .expect("scalar string-array malformed qualified name should succeed");
2179 assert_eq!(value, Value::FunctionHandle("Point..origin".to_string()));
2180 }
2181
2182 #[test]
2183 fn str2func_scalar_string_array_rejects_empty_name_with_identifier() {
2184 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
2185 let err = crate::builtins::introspection::function_handle_text::dispatch_str2func(
2186 Value::StringArray(
2187 runmat_builtins::StringArray::new(vec![" ".to_string()], vec![1, 1])
2188 .expect("string array construction should succeed"),
2189 ),
2190 )
2191 .expect_err("scalar string-array empty function name should fail");
2192 assert_eq!(err.identifier(), Some("RunMat:Str2FuncNameInvalid"));
2193 }
2194
2195 #[test]
2196 fn str2func_scalar_string_array_qualified_name_prefers_semantic_handle_when_resolved() {
2197 let _resolver_guard =
2198 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2199 (name == "pkg.resolved_target").then_some(446)
2200 })));
2201 let value = crate::builtins::introspection::function_handle_text::dispatch_str2func(
2202 Value::StringArray(
2203 runmat_builtins::StringArray::new(
2204 vec!["@pkg.resolved_target".to_string()],
2205 vec![1, 1],
2206 )
2207 .expect("string array construction should succeed"),
2208 ),
2209 )
2210 .expect("scalar string-array qualified function name should resolve semantically");
2211 assert_eq!(
2212 value,
2213 Value::BoundFunctionHandle {
2214 name: "pkg.resolved_target".to_string(),
2215 function: 446,
2216 }
2217 );
2218 }
2219
2220 #[test]
2221 fn getmethod_classref_returns_typed_external_function_handle() {
2222 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
2223 let value = crate::builtins::introspection::getmethod::dispatch_getmethod(
2224 Value::ClassRef("Point".to_string()),
2225 "origin".to_string(),
2226 )
2227 .expect("getmethod should resolve classref method handle");
2228 assert_eq!(
2229 value,
2230 Value::ExternalFunctionHandle("Point.origin".to_string())
2231 );
2232 }
2233
2234 #[test]
2235 fn getmethod_rejects_empty_method_name() {
2236 let err = crate::builtins::introspection::getmethod::dispatch_getmethod(
2237 Value::ClassRef("Point".to_string()),
2238 " ".to_string(),
2239 )
2240 .expect_err("empty method name should be rejected");
2241 assert_eq!(err.identifier(), Some("RunMat:GetMethodNameInvalid"));
2242 }
2243
2244 #[test]
2245 fn getmethod_rejects_unsupported_receiver_with_identifier() {
2246 let err = crate::builtins::introspection::getmethod::dispatch_getmethod(
2247 Value::Num(1.0),
2248 "origin".to_string(),
2249 )
2250 .expect_err("unsupported receiver should be rejected");
2251 assert_eq!(
2252 err.identifier(),
2253 Some("RunMat:GetMethodReceiverUnsupported")
2254 );
2255 }
2256
2257 #[test]
2258 fn create_class_object_handles_class_parent_cycles() {
2259 let class_a = unique_class_name("runtime_ctor_cycle_a");
2260 let class_b = unique_class_name("runtime_ctor_cycle_b");
2261
2262 let mut props_a = HashMap::new();
2263 props_a.insert(
2264 "fromA".to_string(),
2265 PropertyDef {
2266 name: "fromA".to_string(),
2267 is_static: false,
2268 is_constant: false,
2269 is_dependent: false,
2270 get_access: Access::Public,
2271 set_access: Access::Public,
2272 default_value: Some(Value::Num(1.0)),
2273 },
2274 );
2275 let mut props_b = HashMap::new();
2276 props_b.insert(
2277 "fromB".to_string(),
2278 PropertyDef {
2279 name: "fromB".to_string(),
2280 is_static: false,
2281 is_constant: false,
2282 is_dependent: false,
2283 get_access: Access::Public,
2284 set_access: Access::Public,
2285 default_value: Some(Value::Num(2.0)),
2286 },
2287 );
2288
2289 register_class(ClassDef {
2290 name: class_a.clone(),
2291 parent: Some(class_b.clone()),
2292 properties: props_a,
2293 methods: HashMap::new(),
2294 });
2295 register_class(ClassDef {
2296 name: class_b,
2297 parent: Some(class_a.clone()),
2298 properties: props_b,
2299 methods: HashMap::new(),
2300 });
2301
2302 let value = block_on(create_class_object(class_a.clone()))
2303 .expect("constructor should terminate under parent-cycle metadata");
2304 let Value::Object(obj) = value else {
2305 panic!("expected object result");
2306 };
2307 assert_eq!(obj.class_name, class_a);
2308 assert_eq!(obj.properties.get("fromA"), Some(&Value::Num(1.0)));
2309 assert_eq!(obj.properties.get("fromB"), Some(&Value::Num(2.0)));
2310 }
2311
2312 #[test]
2313 fn create_class_object_abstract_class_reports_stable_identifier() {
2314 let class_name = unique_class_name("runtime_ctor_abstract");
2315 runmat_builtins::register_class_with_modifiers(
2316 ClassDef {
2317 name: class_name.clone(),
2318 parent: None,
2319 properties: HashMap::new(),
2320 methods: HashMap::new(),
2321 },
2322 false,
2323 true,
2324 );
2325
2326 let err = block_on(create_class_object(class_name))
2327 .expect_err("abstract class instantiation should fail");
2328 assert_eq!(err.identifier(), Some("RunMat:AbstractMethodMissing"));
2329 assert!(err.message().contains("Cannot instantiate abstract class"));
2330 }
2331
2332 #[test]
2333 fn callable_identity_for_malformed_handle_name_stays_dynamic() {
2334 let (identity, fallback_policy) = callable_identity_for_handle_name("pkg..remote_inc");
2335 assert!(matches!(
2336 identity,
2337 runmat_hir::CallableIdentity::DynamicName(runmat_hir::SymbolName(name))
2338 if name == "pkg..remote_inc"
2339 ));
2340 assert_eq!(
2341 fallback_policy,
2342 runmat_hir::CallableFallbackPolicy::RuntimeNameResolution
2343 );
2344 }
2345
2346 #[test]
2347 fn unresolved_callable_without_display_name_reports_typed_identity() {
2348 let err = block_on(dispatch_callable_with_policy(
2349 runmat_hir::CallableIdentity::AnonymousFunction(runmat_hir::FunctionId(77)),
2350 runmat_hir::CallableFallbackPolicy::RuntimeNameResolution,
2351 vec![],
2352 1,
2353 ))
2354 .expect_err("anonymous callable identity should fail unresolved");
2355 assert_eq!(err.identifier(), Some("RunMat:UndefinedFunction"));
2356 assert!(
2357 err.message().contains("AnonymousFunction(FunctionId(77))"),
2358 "unexpected error: {err:?}"
2359 );
2360 }
2361
2362 #[test]
2363 fn unresolved_malformed_external_callable_reports_typed_identity() {
2364 let err = block_on(dispatch_callable_with_policy(
2365 runmat_hir::CallableIdentity::ExternalName(runmat_hir::QualifiedName(vec![
2366 runmat_hir::SymbolName("pkg".to_string()),
2367 runmat_hir::SymbolName("".to_string()),
2368 runmat_hir::SymbolName("remote".to_string()),
2369 ])),
2370 runmat_hir::CallableFallbackPolicy::ExternalBoundary,
2371 vec![],
2372 1,
2373 ))
2374 .expect_err("malformed external callable identity should fail unresolved");
2375 assert_eq!(err.identifier(), Some("RunMat:UndefinedFunction"));
2376 assert!(
2377 err.message()
2378 .contains("ExternalName(QualifiedName([SymbolName(\"pkg\"), SymbolName(\"\"), SymbolName(\"remote\")]))"),
2379 "unexpected error: {err:?}"
2380 );
2381 }
2382
2383 #[test]
2384 fn unresolved_method_callable_reports_typed_identity() {
2385 let err = block_on(dispatch_callable_with_policy(
2386 runmat_hir::CallableIdentity::Method(runmat_hir::MethodId(
2387 "missing_method".to_string(),
2388 )),
2389 runmat_hir::CallableFallbackPolicy::RuntimeNameResolution,
2390 vec![],
2391 1,
2392 ))
2393 .expect_err("method callable identity should fail unresolved");
2394 assert_eq!(err.identifier(), Some("RunMat:UndefinedFunction"));
2395 assert!(
2396 err.message()
2397 .contains("Method(MethodId(\"missing_method\"))"),
2398 "unexpected error: {err:?}"
2399 );
2400 assert!(
2401 !err.message()
2402 .contains("Undefined function 'missing_method'"),
2403 "method identity should not use fallback display-name text: {err:?}"
2404 );
2405 }
2406
2407 #[test]
2408 fn feval_qualified_at_handle_errors_as_unresolved_external() {
2409 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
2410 let err = block_on(feval_builtin(
2411 Value::String("@missing.external".to_string()),
2412 vec![Value::Num(1.0)],
2413 ))
2414 .expect_err("qualified @handle should error when unresolved");
2415 assert_eq!(err.identifier(), Some("RunMat:UndefinedFunction"));
2416 assert!(
2417 err.message().contains("missing.external"),
2418 "unexpected error: {err:?}"
2419 );
2420 }
2421
2422 #[test]
2423 fn func2str_extracts_name_from_function_handles() {
2424 assert_eq!(
2425 crate::builtins::introspection::function_handle_text::dispatch_func2str(
2426 Value::FunctionHandle("sin".to_string())
2427 )
2428 .expect("func2str"),
2429 Value::String("sin".to_string())
2430 );
2431 assert_eq!(
2432 crate::builtins::introspection::function_handle_text::dispatch_func2str(
2433 Value::ExternalFunctionHandle("Point.origin".to_string())
2434 )
2435 .expect("func2str"),
2436 Value::String("Point.origin".to_string())
2437 );
2438 assert_eq!(
2439 crate::builtins::introspection::function_handle_text::dispatch_func2str(
2440 Value::BoundFunctionHandle {
2441 name: "local_fn".to_string(),
2442 function: 44,
2443 }
2444 )
2445 .expect("func2str"),
2446 Value::String("local_fn".to_string())
2447 );
2448 assert_eq!(
2449 crate::builtins::introspection::function_handle_text::dispatch_func2str(
2450 Value::Closure(runmat_builtins::Closure {
2451 function_name: "captured_fn".to_string(),
2452 bound_function: None,
2453 captures: Vec::new(),
2454 })
2455 )
2456 .expect("func2str"),
2457 Value::String("captured_fn".to_string())
2458 );
2459 }
2460
2461 #[test]
2462 fn none_policy_does_not_use_semantic_resolver() {
2463 let _resolver_guard =
2464 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2465 (name == "resolved_target").then_some(45)
2466 })));
2467 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
2468 Arc::new(|function, args, requested_outputs| {
2469 assert_eq!(function, 45);
2470 assert_eq!(requested_outputs, 1);
2471 assert_eq!(args, &[Value::Num(4.0)]);
2472 Box::pin(async { Ok(Value::Num(11.0)) })
2473 }),
2474 ));
2475
2476 let request = crate::user_functions::CallableRequest::resolved(
2477 runmat_hir::CallableIdentity::DynamicName(runmat_hir::SymbolName(
2478 "resolved_target".to_string(),
2479 )),
2480 runmat_hir::CallableFallbackPolicy::None,
2481 vec![Value::Num(4.0)],
2482 1,
2483 );
2484
2485 let result = block_on(crate::user_functions::try_call_semantic_descriptor(request));
2486 assert!(result.is_none());
2487 }
2488
2489 #[test]
2490 fn runtime_name_resolution_policy_uses_semantic_resolver() {
2491 let _resolver_guard =
2492 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2493 (name == "resolved_target").then_some(45)
2494 })));
2495 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
2496 Arc::new(|function, args, requested_outputs| {
2497 assert_eq!(function, 45);
2498 assert_eq!(requested_outputs, 1);
2499 assert_eq!(args, &[Value::Num(4.0)]);
2500 Box::pin(async { Ok(Value::Num(11.0)) })
2501 }),
2502 ));
2503
2504 let request = crate::user_functions::CallableRequest::resolved(
2505 runmat_hir::CallableIdentity::DynamicName(runmat_hir::SymbolName(
2506 "resolved_target".to_string(),
2507 )),
2508 runmat_hir::CallableFallbackPolicy::RuntimeNameResolution,
2509 vec![Value::Num(4.0)],
2510 1,
2511 );
2512
2513 let result = block_on(crate::user_functions::try_call_semantic_descriptor(request))
2514 .expect("runtime resolution should attempt semantic resolver")
2515 .expect("semantic invoker should succeed");
2516 assert_eq!(result, Value::Num(11.0));
2517 }
2518
2519 #[test]
2520 fn object_dispatch_policy_does_not_use_semantic_resolver() {
2521 let _resolver_guard =
2522 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2523 (name == "resolved_target").then_some(45)
2524 })));
2525 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
2526 Arc::new(|function, args, requested_outputs| {
2527 assert_eq!(function, 45);
2528 assert_eq!(requested_outputs, 1);
2529 assert_eq!(args, &[Value::Num(4.0)]);
2530 Box::pin(async { Ok(Value::Num(11.0)) })
2531 }),
2532 ));
2533
2534 let request = crate::user_functions::CallableRequest::resolved(
2535 runmat_hir::CallableIdentity::DynamicName(runmat_hir::SymbolName(
2536 "resolved_target".to_string(),
2537 )),
2538 runmat_hir::CallableFallbackPolicy::ObjectDispatch,
2539 vec![Value::Num(4.0)],
2540 1,
2541 );
2542
2543 let result = block_on(crate::user_functions::try_call_semantic_descriptor(request));
2544 assert!(result.is_none());
2545 }
2546
2547 #[test]
2548 fn external_name_runtime_name_resolution_policy_does_not_use_semantic_resolver() {
2549 let _resolver_guard =
2550 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2551 (name == "resolved_target").then_some(45)
2552 })));
2553 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
2554 Arc::new(|function, args, requested_outputs| {
2555 assert_eq!(function, 45);
2556 assert_eq!(requested_outputs, 1);
2557 assert_eq!(args, &[Value::Num(4.0)]);
2558 Box::pin(async { Ok(Value::Num(11.0)) })
2559 }),
2560 ));
2561
2562 let request = crate::user_functions::CallableRequest::resolved(
2563 runmat_hir::CallableIdentity::ExternalName(runmat_hir::QualifiedName(vec![
2564 runmat_hir::SymbolName("resolved_target".to_string()),
2565 ])),
2566 runmat_hir::CallableFallbackPolicy::RuntimeNameResolution,
2567 vec![Value::Num(4.0)],
2568 1,
2569 );
2570
2571 let result = block_on(crate::user_functions::try_call_semantic_descriptor(request));
2572 assert!(result.is_none());
2573 }
2574
2575 #[test]
2576 fn external_boundary_policy_uses_semantic_resolver() {
2577 let _resolver_guard =
2578 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2579 (name == "pkg.resolved_target").then_some(45)
2580 })));
2581 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
2582 Arc::new(|function, args, requested_outputs| {
2583 assert_eq!(function, 45);
2584 assert_eq!(requested_outputs, 1);
2585 assert_eq!(args, &[Value::Num(4.0)]);
2586 Box::pin(async { Ok(Value::Num(11.0)) })
2587 }),
2588 ));
2589
2590 let request = crate::user_functions::CallableRequest::resolved(
2591 runmat_hir::CallableIdentity::ExternalName(runmat_hir::QualifiedName(vec![
2592 runmat_hir::SymbolName("pkg".to_string()),
2593 runmat_hir::SymbolName("resolved_target".to_string()),
2594 ])),
2595 runmat_hir::CallableFallbackPolicy::ExternalBoundary,
2596 vec![Value::Num(4.0)],
2597 1,
2598 );
2599
2600 let result = block_on(crate::user_functions::try_call_semantic_descriptor(request))
2601 .expect("external boundary policy should attempt semantic resolver")
2602 .expect("semantic invoker should succeed");
2603 assert_eq!(result, Value::Num(11.0));
2604 }
2605
2606 #[test]
2607 fn external_boundary_policy_malformed_external_identity_does_not_use_semantic_resolver() {
2608 let _resolver_guard =
2609 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2610 (name == "pkg..resolved_target").then_some(45)
2611 })));
2612 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
2613 Arc::new(|function, args, requested_outputs| {
2614 assert_eq!(function, 45);
2615 assert_eq!(requested_outputs, 1);
2616 assert_eq!(args, &[Value::Num(4.0)]);
2617 Box::pin(async { Ok(Value::Num(11.0)) })
2618 }),
2619 ));
2620
2621 let request = crate::user_functions::CallableRequest::resolved(
2622 runmat_hir::CallableIdentity::ExternalName(runmat_hir::QualifiedName(vec![
2623 runmat_hir::SymbolName("pkg..resolved_target".to_string()),
2624 ])),
2625 runmat_hir::CallableFallbackPolicy::ExternalBoundary,
2626 vec![Value::Num(4.0)],
2627 1,
2628 );
2629
2630 let result = block_on(crate::user_functions::try_call_semantic_descriptor(request));
2631 assert!(result.is_none());
2632 }
2633
2634 #[test]
2635 fn runtime_name_resolution_policy_uses_semantic_resolver_after_object_probe() {
2636 let _resolver_guard =
2637 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2638 (name == "resolved_target").then_some(45)
2639 })));
2640 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
2641 Arc::new(|function, args, requested_outputs| {
2642 assert_eq!(function, 45);
2643 assert_eq!(requested_outputs, 1);
2644 assert_eq!(args, &[Value::Num(4.0)]);
2645 Box::pin(async { Ok(Value::Num(11.0)) })
2646 }),
2647 ));
2648
2649 let request = crate::user_functions::CallableRequest::resolved(
2650 runmat_hir::CallableIdentity::DynamicName(runmat_hir::SymbolName(
2651 "resolved_target".to_string(),
2652 )),
2653 runmat_hir::CallableFallbackPolicy::RuntimeNameResolution,
2654 vec![Value::Num(4.0)],
2655 1,
2656 );
2657
2658 let result = block_on(crate::user_functions::try_call_semantic_descriptor(request))
2659 .expect("post-object-probe runtime-name policy should attempt semantic resolver")
2660 .expect("semantic invoker should succeed");
2661 assert_eq!(result, Value::Num(11.0));
2662 }
2663
2664 #[test]
2665 fn method_identity_runtime_name_resolution_policy_uses_semantic_resolver() {
2666 let _resolver_guard =
2667 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2668 (name == "resolved_target").then_some(45)
2669 })));
2670 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
2671 Arc::new(|function, args, requested_outputs| {
2672 assert_eq!(function, 45);
2673 assert_eq!(requested_outputs, 1);
2674 assert_eq!(args, &[Value::Num(4.0)]);
2675 Box::pin(async { Ok(Value::Num(11.0)) })
2676 }),
2677 ));
2678
2679 let request = crate::user_functions::CallableRequest::resolved(
2680 runmat_hir::CallableIdentity::Method(runmat_hir::MethodId(
2681 "resolved_target".to_string(),
2682 )),
2683 runmat_hir::CallableFallbackPolicy::RuntimeNameResolution,
2684 vec![Value::Num(4.0)],
2685 1,
2686 );
2687
2688 let result = block_on(crate::user_functions::try_call_semantic_descriptor(request))
2689 .expect("method runtime-name policy should attempt semantic resolver")
2690 .expect("semantic invoker should succeed");
2691 assert_eq!(result, Value::Num(11.0));
2692 }
2693
2694 #[test]
2695 fn imported_identity_runtime_name_resolution_policy_uses_semantic_resolver() {
2696 let _resolver_guard =
2697 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2698 (name == "Point.origin").then_some(45)
2699 })));
2700 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
2701 Arc::new(|function, args, requested_outputs| {
2702 assert_eq!(function, 45);
2703 assert_eq!(requested_outputs, 1);
2704 assert_eq!(args, &[Value::Num(4.0)]);
2705 Box::pin(async { Ok(Value::Num(11.0)) })
2706 }),
2707 ));
2708
2709 let request = crate::user_functions::CallableRequest::resolved(
2710 runmat_hir::CallableIdentity::Imported(runmat_hir::DefPath {
2711 package: runmat_hir::PackageName("Point".to_string()),
2712 module: runmat_hir::QualifiedName(vec![
2713 runmat_hir::SymbolName("Point".to_string()),
2714 runmat_hir::SymbolName("origin".to_string()),
2715 ]),
2716 item: vec![runmat_hir::DefPathSegment::Function(
2717 runmat_hir::SymbolName("origin".to_string()),
2718 )],
2719 }),
2720 runmat_hir::CallableFallbackPolicy::RuntimeNameResolution,
2721 vec![Value::Num(4.0)],
2722 1,
2723 );
2724
2725 let result = block_on(crate::user_functions::try_call_semantic_descriptor(request))
2726 .expect("imported runtime-name policy should attempt semantic resolver")
2727 .expect("semantic invoker should succeed");
2728 assert_eq!(result, Value::Num(11.0));
2729 }
2730
2731 #[test]
2732 fn imported_identity_runtime_name_resolution_policy_rejects_malformed_path_without_semantic_probe(
2733 ) {
2734 let resolver_calls = Arc::new(AtomicUsize::new(0));
2735 let resolver_calls_for_closure = Arc::clone(&resolver_calls);
2736 let _resolver_guard =
2737 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(move |_| {
2738 resolver_calls_for_closure.fetch_add(1, Ordering::Relaxed);
2739 Some(45)
2740 })));
2741 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
2742 Arc::new(|function, args, requested_outputs| {
2743 assert_eq!(function, 45);
2744 assert_eq!(requested_outputs, 1);
2745 assert_eq!(args, &[Value::Num(4.0)]);
2746 Box::pin(async { Ok(Value::Num(11.0)) })
2747 }),
2748 ));
2749
2750 let request = crate::user_functions::CallableRequest::resolved(
2751 runmat_hir::CallableIdentity::Imported(runmat_hir::DefPath {
2752 package: runmat_hir::PackageName("Point".to_string()),
2753 module: runmat_hir::QualifiedName(vec![
2754 runmat_hir::SymbolName("Point".to_string()),
2755 runmat_hir::SymbolName("origin".to_string()),
2756 ]),
2757 item: vec![runmat_hir::DefPathSegment::Function(
2758 runmat_hir::SymbolName("other".to_string()),
2759 )],
2760 }),
2761 runmat_hir::CallableFallbackPolicy::RuntimeNameResolution,
2762 vec![Value::Num(4.0)],
2763 1,
2764 );
2765
2766 let result = block_on(crate::user_functions::try_call_semantic_descriptor(request));
2767 assert!(
2768 result.is_none(),
2769 "mismatched imported identity should not attempt semantic resolver"
2770 );
2771 assert_eq!(
2772 resolver_calls.load(Ordering::Relaxed),
2773 0,
2774 "malformed imported identity should be rejected before resolver probe"
2775 );
2776 }
2777
2778 #[test]
2779 fn call_method_fallback_preserves_requested_outputs() {
2780 let _output_guard = crate::output_count::push_output_count(Some(2));
2781 let base = Value::Object(runmat_builtins::ObjectInstance::new(
2782 "NoSuchMethodClass".to_string(),
2783 ));
2784 let result = block_on(
2785 crate::builtins::introspection::call_method::dispatch_call_method(
2786 base.clone(),
2787 "deal".to_string(),
2788 vec![Value::Num(9.0), Value::Num(10.0)],
2789 ),
2790 )
2791 .expect("call_method fallback should succeed");
2792 match result {
2793 Value::OutputList(values) => {
2794 assert!(values.len() >= 2);
2795 assert_eq!(values[0], base);
2796 assert_eq!(values[1], Value::Num(9.0));
2797 }
2798 other => {
2799 panic!("expected output list from multi-output call_method fallback, got {other:?}")
2800 }
2801 }
2802 }
2803
2804 #[test]
2805 fn call_method_trims_method_name_for_resolution() {
2806 let _output_guard = crate::output_count::push_output_count(Some(2));
2807 let base = Value::Object(runmat_builtins::ObjectInstance::new(
2808 "NoSuchMethodClass".to_string(),
2809 ));
2810 let result = block_on(
2811 crate::builtins::introspection::call_method::dispatch_call_method(
2812 base.clone(),
2813 " deal ".to_string(),
2814 vec![Value::Num(9.0), Value::Num(10.0)],
2815 ),
2816 )
2817 .expect("call_method fallback should succeed after method-name trimming");
2818 match result {
2819 Value::OutputList(values) => {
2820 assert!(values.len() >= 2);
2821 assert_eq!(values[0], base);
2822 assert_eq!(values[1], Value::Num(9.0));
2823 }
2824 other => {
2825 panic!("expected output list from trimmed-name call_method fallback, got {other:?}")
2826 }
2827 }
2828 }
2829
2830 #[test]
2831 fn feval_call_method_closure_fast_path_preserves_requested_outputs() {
2832 let _output_guard = crate::output_count::push_output_count(Some(2));
2833 let base = Value::Object(runmat_builtins::ObjectInstance::new(
2834 "NoSuchMethodClass".to_string(),
2835 ));
2836 let closure = Value::Closure(runmat_builtins::Closure {
2837 function_name: CALL_METHOD_BUILTIN_NAME.to_string(),
2838 bound_function: None,
2839 captures: vec![
2840 base.clone(),
2841 Value::String("deal".to_string()),
2842 Value::Num(9.0),
2843 ],
2844 });
2845 let result = block_on(feval_builtin(closure, vec![Value::Num(10.0)]))
2846 .expect("feval call_method closure should succeed");
2847 match result {
2848 Value::OutputList(values) => {
2849 assert!(values.len() >= 2);
2850 assert_eq!(values[0], base);
2851 assert_eq!(values[1], Value::Num(9.0));
2852 }
2853 other => {
2854 panic!(
2855 "expected output list from feval call_method closure fast path, got {other:?}"
2856 )
2857 }
2858 }
2859 }
2860
2861 #[test]
2862 fn feval_call_method_closure_fast_path_trims_method_name_for_resolution() {
2863 let _output_guard = crate::output_count::push_output_count(Some(2));
2864 let base = Value::Object(runmat_builtins::ObjectInstance::new(
2865 "NoSuchMethodClass".to_string(),
2866 ));
2867 let closure = Value::Closure(runmat_builtins::Closure {
2868 function_name: CALL_METHOD_BUILTIN_NAME.to_string(),
2869 bound_function: None,
2870 captures: vec![
2871 base.clone(),
2872 Value::String(" deal ".to_string()),
2873 Value::Num(9.0),
2874 ],
2875 });
2876 let result = block_on(feval_builtin(closure, vec![Value::Num(10.0)]))
2877 .expect("feval call_method closure should succeed after method-name trimming");
2878 match result {
2879 Value::OutputList(values) => {
2880 assert!(values.len() >= 2);
2881 assert_eq!(values[0], base);
2882 assert_eq!(values[1], Value::Num(9.0));
2883 }
2884 other => {
2885 panic!(
2886 "expected output list from trimmed call_method closure fast path, got {other:?}"
2887 )
2888 }
2889 }
2890 }
2891
2892 #[test]
2893 fn feval_call_method_closure_rejects_nontext_method_capture_with_identifier() {
2894 let closure = Value::Closure(runmat_builtins::Closure {
2895 function_name: CALL_METHOD_BUILTIN_NAME.to_string(),
2896 bound_function: None,
2897 captures: vec![
2898 Value::Object(runmat_builtins::ObjectInstance::new("Point".to_string())),
2899 Value::Num(1.0),
2900 ],
2901 });
2902 let err = block_on(feval_builtin(closure, Vec::new()))
2903 .expect_err("feval call_method closure should reject nontext method capture");
2904 assert_eq!(err.identifier(), Some("RunMat:CallMethodNameInvalid"));
2905 }
2906
2907 #[test]
2908 fn call_method_rejects_non_object_receiver_with_identifier() {
2909 let err = block_on(
2910 crate::builtins::introspection::call_method::dispatch_call_method(
2911 Value::Num(1.0),
2912 "origin".to_string(),
2913 Vec::new(),
2914 ),
2915 )
2916 .expect_err("non-object receiver should fail");
2917 assert_eq!(err.identifier(), Some("RunMat:InvalidObjectDispatch"));
2918 }
2919
2920 #[test]
2921 fn call_method_rejects_empty_method_name_with_identifier() {
2922 let err = block_on(
2923 crate::builtins::introspection::call_method::dispatch_call_method(
2924 Value::Object(runmat_builtins::ObjectInstance::new("Point".to_string())),
2925 " ".to_string(),
2926 Vec::new(),
2927 ),
2928 )
2929 .expect_err("empty method name should fail");
2930 assert_eq!(err.identifier(), Some("RunMat:CallMethodNameInvalid"));
2931 }
2932
2933 #[test]
2934 fn subsref_rejects_non_object_receiver_with_identifier() {
2935 let err = block_on(
2936 crate::builtins::introspection::object_indexing::dispatch_subsref(
2937 Value::Num(1.0),
2938 OBJECT_INDEX_PAREN.to_string(),
2939 Value::Num(2.0),
2940 ),
2941 )
2942 .expect_err("non-object subsref receiver should fail");
2943 assert_eq!(err.identifier(), Some("RunMat:InvalidObjectDispatch"));
2944 }
2945
2946 #[test]
2947 fn subsasgn_rejects_non_object_receiver_with_identifier() {
2948 let err = block_on(
2949 crate::builtins::introspection::object_indexing::dispatch_subsasgn(
2950 Value::Num(1.0),
2951 OBJECT_INDEX_PAREN.to_string(),
2952 Value::Num(2.0),
2953 Value::Num(3.0),
2954 ),
2955 )
2956 .expect_err("non-object subsasgn receiver should fail");
2957 assert_eq!(err.identifier(), Some("RunMat:InvalidObjectDispatch"));
2958 }
2959
2960 #[test]
2961 fn subsref_missing_protocol_errors_with_identifier() {
2962 let err = block_on(
2963 crate::builtins::introspection::object_indexing::dispatch_subsref(
2964 Value::Object(runmat_builtins::ObjectInstance::new(
2965 "NoSubsrefProtocolClass".to_string(),
2966 )),
2967 OBJECT_INDEX_PAREN.to_string(),
2968 Value::Cell(runmat_builtins::CellArray::new(vec![Value::Num(1.0)], 1, 1).unwrap()),
2969 ),
2970 )
2971 .expect_err("missing subsref protocol should fail");
2972 assert_eq!(err.identifier(), Some("RunMat:MissingSubsref"));
2973 }
2974
2975 #[test]
2976 fn subsasgn_missing_protocol_errors_with_identifier() {
2977 let err = block_on(
2978 crate::builtins::introspection::object_indexing::dispatch_subsasgn(
2979 Value::Object(runmat_builtins::ObjectInstance::new(
2980 "NoSubsasgnProtocolClass".to_string(),
2981 )),
2982 OBJECT_INDEX_PAREN.to_string(),
2983 Value::Cell(runmat_builtins::CellArray::new(vec![Value::Num(1.0)], 1, 1).unwrap()),
2984 Value::Num(3.0),
2985 ),
2986 )
2987 .expect_err("missing subsasgn protocol should fail");
2988 assert_eq!(err.identifier(), Some("RunMat:MissingSubsasgn"));
2989 }
2990
2991 #[test]
2992 fn get_p_rejects_non_object_receiver_with_identifier() {
2993 let err = block_on(get_p_builtin(Value::Num(1.0)))
2994 .expect_err("get.p should reject non-object receiver");
2995 assert_eq!(err.identifier(), Some("RunMat:GetPReceiverInvalid"));
2996 }
2997
2998 #[test]
2999 fn set_p_rejects_non_object_receiver_with_identifier() {
3000 let err = block_on(set_p_builtin(Value::Num(1.0), Value::Num(2.0)))
3001 .expect_err("set.p should reject non-object receiver");
3002 assert_eq!(err.identifier(), Some("RunMat:SetPReceiverInvalid"));
3003 }
3004
3005 #[test]
3006 fn point_move_rejects_non_object_receiver_with_identifier() {
3007 let err = block_on(point_move_method(Value::Num(1.0), 2.0, 3.0))
3008 .expect_err("Point.move should reject non-object receiver");
3009 assert_eq!(err.identifier(), Some("RunMat:PointMoveReceiverInvalid"));
3010 }
3011
3012 #[test]
3013 fn circle_area_rejects_non_object_receiver_with_identifier() {
3014 let err = block_on(circle_area_method(Value::Num(1.0)))
3015 .expect_err("Circle.area should reject non-object receiver");
3016 assert_eq!(err.identifier(), Some("RunMat:CircleAreaReceiverInvalid"));
3017 }
3018
3019 #[test]
3020 fn overidx_plus_rejects_non_object_receiver_with_identifier() {
3021 let err = block_on(overidx_plus(Value::Num(1.0), Value::Num(2.0)))
3022 .expect_err("OverIdx.plus should reject non-object receiver");
3023 assert_eq!(err.identifier(), Some("RunMat:OverIdxReceiverInvalid"));
3024 }
3025
3026 #[test]
3027 fn overidx_subsref_unsupported_payload_errors_with_identifier() {
3028 let err = block_on(overidx_subsref(
3029 Value::Object(runmat_builtins::ObjectInstance::new("OverIdx".to_string())),
3030 OBJECT_INDEX_PAREN.to_string(),
3031 Value::Num(1.0),
3032 ))
3033 .expect_err("OverIdx.subsref unsupported payload should fail");
3034 assert_eq!(
3035 err.identifier(),
3036 Some("RunMat:OverIdxSubsrefPayloadUnsupported")
3037 );
3038 }
3039
3040 #[test]
3041 fn overidx_subsasgn_unsupported_payload_errors_with_identifier() {
3042 let err = block_on(overidx_subsasgn(
3043 Value::Object(runmat_builtins::ObjectInstance::new("OverIdx".to_string())),
3044 OBJECT_INDEX_PAREN.to_string(),
3045 Value::Num(1.0),
3046 Value::Num(2.0),
3047 ))
3048 .expect_err("OverIdx.subsasgn unsupported payload should fail");
3049 assert_eq!(
3050 err.identifier(),
3051 Some("RunMat:OverIdxSubsasgnPayloadUnsupported")
3052 );
3053 }
3054
3055 #[test]
3056 fn feval_object_receiver_routes_to_subsref_identifier() {
3057 let err = block_on(feval_builtin(
3058 Value::Object(runmat_builtins::ObjectInstance::new(
3059 "NoSubsrefProtocolClass".to_string(),
3060 )),
3061 vec![Value::Num(1.0)],
3062 ))
3063 .expect_err("feval(object, ...) should route through subsref dispatch");
3064 assert_eq!(err.identifier(), Some("RunMat:MissingSubsref"));
3065 }
3066
3067 #[test]
3068 fn feval_unsupported_callable_value_errors_with_identifier() {
3069 let err = block_on(feval_builtin(Value::Num(1.0), vec![Value::Num(2.0)]))
3070 .expect_err("numeric callable value should fail");
3071 assert_eq!(
3072 err.identifier(),
3073 Some("RunMat:FevalFunctionValueUnsupported")
3074 );
3075 }
3076
3077 #[test]
3078 fn shape_checked_cell_builder_maps_shape_identifier() {
3079 let err = super::build_shape_checked_cell(vec![Value::Num(1.0)], 2, 2, "test")
3080 .expect_err("expected shape mismatch");
3081 assert_eq!(err.identifier(), Some("RunMat:ShapeMismatch"));
3082 }
3083
3084 #[test]
3085 fn feval_accepts_scalar_string_array_handle() {
3086 let handle =
3087 runmat_builtins::StringArray::new(vec!["@sin".to_string()], vec![1, 1]).expect("sa");
3088 let result = block_on(feval_builtin(
3089 Value::StringArray(handle),
3090 vec![Value::Num(0.0)],
3091 ))
3092 .expect("string-array handle feval should succeed");
3093 assert_eq!(result, Value::Num(0.0));
3094 }
3095
3096 #[test]
3097 fn feval_rejects_nonscalar_string_array_handle_with_identifier() {
3098 let handle = runmat_builtins::StringArray::new(
3099 vec!["@sin".to_string(), "@cos".to_string()],
3100 vec![1, 2],
3101 )
3102 .expect("sa");
3103 let err = block_on(feval_builtin(
3104 Value::StringArray(handle),
3105 vec![Value::Num(0.0)],
3106 ))
3107 .expect_err("nonscalar string-array handle should fail");
3108 assert_eq!(err.identifier(), Some("RunMat:FevalHandleShapeInvalid"));
3109 }
3110
3111 #[test]
3112 fn call_feval_async_with_outputs_preserves_unresolved_identifier() {
3113 let err = block_on(super::call_feval_async_with_outputs(
3114 Value::ExternalFunctionHandle("missing.external".to_string()),
3115 &[Value::Num(3.0)],
3116 1,
3117 ))
3118 .expect_err("unresolved external handle should fail");
3119 assert_eq!(err.identifier(), Some("RunMat:UndefinedFunction"));
3120 }
3121
3122 #[test]
3123 fn addlistener_rejects_non_object_target_with_identifier() {
3124 let err = block_on(addlistener_builtin(
3125 Value::Num(1.0),
3126 "Changed".to_string(),
3127 Value::FunctionHandle("sin".to_string()),
3128 ))
3129 .expect_err("addlistener should reject non-object target");
3130 assert_eq!(err.identifier(), Some("RunMat:AddListenerTargetInvalid"));
3131 }
3132
3133 #[test]
3134 fn addlistener_rejects_invalid_handle_target_with_identifier() {
3135 listener_gc_test(|| {
3136 let target = runmat_builtins::HandleRef {
3137 class_name: "EventTarget".to_string(),
3138 target: runmat_gc::gc_allocate(Value::Object(
3139 runmat_builtins::ObjectInstance::new("EventTarget".to_string()),
3140 ))
3141 .expect("allocate target"),
3142 valid: true,
3143 };
3144 assert!(set_handle_valid(&target, false));
3145
3146 let err = block_on(addlistener_builtin(
3147 Value::HandleObject(target),
3148 "Changed".to_string(),
3149 Value::FunctionHandle("sin".to_string()),
3150 ))
3151 .expect_err("addlistener should reject invalid handle target");
3152 assert_eq!(err.identifier(), Some("RunMat:AddListenerTargetInvalid"));
3153 });
3154 }
3155
3156 #[test]
3157 fn addlistener_preserves_handle_target_when_callback_allocation_collects() {
3158 listener_gc_test(|| {
3159 reset_event_registry_for_test();
3160 struct ConfigGuard(runmat_gc::GcConfig);
3161
3162 impl Drop for ConfigGuard {
3163 fn drop(&mut self) {
3164 runmat_gc::gc_configure(self.0.clone())
3165 .expect("restore GC configuration after listener test");
3166 }
3167 }
3168
3169 let _config_guard = ConfigGuard(runmat_gc::gc_get_config());
3170 let config = runmat_gc::GcConfig {
3171 young_generation_size: 64 * 1024 * 1024,
3172 minor_gc_threshold: 0.35,
3173 major_gc_threshold: 0.9,
3174 ..runmat_gc::GcConfig::default()
3175 };
3176 runmat_gc::gc_configure(config).expect("configure aggressive periodic minor GC");
3177
3178 for i in 0..30 {
3179 let _ = runmat_gc::gc_allocate(Value::Num(i as f64)).expect("seed allocation");
3180 }
3181
3182 let target =
3183 block_on(new_handle_object_builtin("EventTarget".to_string())).expect("target");
3184 let listener = block_on(addlistener_builtin(
3185 target,
3186 "ChangedSoundness".to_string(),
3187 Value::FunctionHandle("sin".to_string()),
3188 ))
3189 .expect("listener registered");
3190
3191 let Value::Listener(listener) = listener else {
3192 panic!("expected listener value");
3193 };
3194 let target = runmat_gc::gc_clone_value(&listener.target)
3195 .expect("listener target should survive construction");
3196 assert!(matches!(
3197 target,
3198 Value::Object(ref object) if object.class_name == "EventTarget"
3199 ));
3200 assert_eq!(
3201 runmat_gc::gc_clone_value(&listener.callback)
3202 .expect("listener callback should survive construction"),
3203 Value::FunctionHandle("sin".to_string())
3204 );
3205 });
3206 }
3207
3208 #[test]
3209 fn notify_rejects_non_object_target_with_identifier() {
3210 let err = block_on(notify_builtin(
3211 Value::Num(1.0),
3212 "Changed".to_string(),
3213 Vec::new(),
3214 ))
3215 .expect_err("notify should reject non-object target");
3216 assert_eq!(err.identifier(), Some("RunMat:NotifyTargetInvalid"));
3217 }
3218
3219 #[test]
3220 fn addlistener_function_handle_prefers_semantic_identity_when_resolved() {
3221 listener_gc_test(|| {
3222 let _resolver_guard =
3223 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
3224 (name == "event_callback").then_some(61)
3225 })));
3226 let target = block_on(new_handle_object_builtin("EventTarget".to_string()))
3227 .expect("handle target");
3228 let listener = block_on(addlistener_builtin(
3229 target,
3230 "Changed".to_string(),
3231 Value::FunctionHandle("event_callback".to_string()),
3232 ))
3233 .expect("listener registered");
3234 let Value::Listener(listener) = listener else {
3235 panic!("expected listener value");
3236 };
3237 let callback = runmat_gc::gc_clone_value(&listener.callback).expect("callback value");
3238 assert!(matches!(
3239 &callback,
3240 Value::BoundFunctionHandle { name, function }
3241 if name == "event_callback" && *function == 61
3242 ));
3243 });
3244 }
3245
3246 #[test]
3247 fn addlistener_external_function_handle_prefers_semantic_identity_when_resolved() {
3248 listener_gc_test(|| {
3249 let _resolver_guard =
3250 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
3251 (name == "pkg.event_callback").then_some(62)
3252 })));
3253 let target = block_on(new_handle_object_builtin("EventTarget".to_string()))
3254 .expect("handle target");
3255 let listener = block_on(addlistener_builtin(
3256 target,
3257 "Changed".to_string(),
3258 Value::ExternalFunctionHandle("pkg.event_callback".to_string()),
3259 ))
3260 .expect("listener registered");
3261 let Value::Listener(listener) = listener else {
3262 panic!("expected listener value");
3263 };
3264 let callback = runmat_gc::gc_clone_value(&listener.callback).expect("callback value");
3265 assert!(matches!(
3266 &callback,
3267 Value::BoundFunctionHandle { name, function }
3268 if name == "pkg.event_callback" && *function == 62
3269 ));
3270 });
3271 }
3272
3273 #[test]
3274 fn addlistener_string_handle_prefers_semantic_identity_when_resolved() {
3275 listener_gc_test(|| {
3276 let _resolver_guard =
3277 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
3278 (name == "event_callback").then_some(63)
3279 })));
3280 let target = block_on(new_handle_object_builtin("EventTarget".to_string()))
3281 .expect("handle target");
3282 let listener = block_on(addlistener_builtin(
3283 target,
3284 "Changed".to_string(),
3285 Value::String("@event_callback".to_string()),
3286 ))
3287 .expect("listener registered");
3288 let Value::Listener(listener) = listener else {
3289 panic!("expected listener value");
3290 };
3291 let callback = runmat_gc::gc_clone_value(&listener.callback).expect("callback value");
3292 assert!(matches!(
3293 &callback,
3294 Value::BoundFunctionHandle { name, function }
3295 if name == "event_callback" && *function == 63
3296 ));
3297 });
3298 }
3299
3300 #[test]
3301 fn addlistener_char_handle_prefers_semantic_identity_when_resolved() {
3302 listener_gc_test(|| {
3303 let _resolver_guard =
3304 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
3305 (name == "event_callback").then_some(64)
3306 })));
3307 let target = block_on(new_handle_object_builtin("EventTarget".to_string()))
3308 .expect("handle target");
3309 let listener = block_on(addlistener_builtin(
3310 target,
3311 "Changed".to_string(),
3312 Value::CharArray(runmat_builtins::CharArray::new_row("@event_callback")),
3313 ))
3314 .expect("listener registered");
3315 let Value::Listener(listener) = listener else {
3316 panic!("expected listener value");
3317 };
3318 let callback = runmat_gc::gc_clone_value(&listener.callback).expect("callback value");
3319 assert!(matches!(
3320 &callback,
3321 Value::BoundFunctionHandle { name, function }
3322 if name == "event_callback" && *function == 64
3323 ));
3324 });
3325 }
3326
3327 #[test]
3328 fn addlistener_string_array_handle_prefers_semantic_identity_when_resolved() {
3329 listener_gc_test(|| {
3330 let _resolver_guard =
3331 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
3332 (name == "event_callback").then_some(66)
3333 })));
3334 let target = block_on(new_handle_object_builtin("EventTarget".to_string()))
3335 .expect("handle target");
3336 let callback =
3337 runmat_builtins::StringArray::new(vec!["@event_callback".to_string()], vec![1, 1])
3338 .expect("string array");
3339 let listener = block_on(addlistener_builtin(
3340 target,
3341 "Changed".to_string(),
3342 Value::StringArray(callback),
3343 ))
3344 .expect("listener registered");
3345 let Value::Listener(listener) = listener else {
3346 panic!("expected listener value");
3347 };
3348 let callback = runmat_gc::gc_clone_value(&listener.callback).expect("callback value");
3349 assert!(matches!(
3350 &callback,
3351 Value::BoundFunctionHandle { name, function }
3352 if name == "event_callback" && *function == 66
3353 ));
3354 });
3355 }
3356
3357 #[test]
3358 fn addlistener_closure_prefers_embedded_semantic_identity_when_resolved() {
3359 listener_gc_test(|| {
3360 let _resolver_guard =
3361 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
3362 (name == "event_callback").then_some(65)
3363 })));
3364 let target = block_on(new_handle_object_builtin("EventTarget".to_string()))
3365 .expect("handle target");
3366 let callback = Value::Closure(runmat_builtins::Closure {
3367 function_name: "event_callback".to_string(),
3368 bound_function: None,
3369 captures: vec![Value::Num(9.0)],
3370 });
3371 let listener = block_on(addlistener_builtin(target, "Changed".to_string(), callback))
3372 .expect("listener registered");
3373 let Value::Listener(listener) = listener else {
3374 panic!("expected listener value");
3375 };
3376 let callback = runmat_gc::gc_clone_value(&listener.callback).expect("callback value");
3377 assert!(matches!(
3378 &callback,
3379 Value::Closure(runmat_builtins::Closure {
3380 function_name,
3381 bound_function: Some(65),
3382 captures,
3383 }) if function_name == "event_callback" && captures == &vec![Value::Num(9.0)]
3384 ));
3385 });
3386 }
3387
3388 #[test]
3389 fn notify_semantic_function_handle_uses_semantic_identity() {
3390 listener_gc_test(|| {
3391 let calls = Arc::new(AtomicUsize::new(0));
3392 let seen_calls = Arc::clone(&calls);
3393 let _guard = crate::user_functions::install_semantic_function_invoker(Some(Arc::new(
3394 move |function, args, requested_outputs| {
3395 assert_eq!(function, 44);
3396 assert_eq!(requested_outputs, 0);
3397 assert_eq!(args.len(), 1);
3398 assert!(matches!(args[0], Value::HandleObject(_)));
3399 seen_calls.fetch_add(1, Ordering::SeqCst);
3400 Box::pin(async { Ok(Value::Num(0.0)) })
3401 },
3402 )));
3403 let target = block_on(new_handle_object_builtin("EventTarget".to_string()))
3404 .expect("handle target");
3405 let callback = Value::BoundFunctionHandle {
3406 name: "event_callback".to_string(),
3407 function: 44,
3408 };
3409
3410 block_on(addlistener_builtin(
3411 target.clone(),
3412 "Changed".to_string(),
3413 callback,
3414 ))
3415 .expect("listener registered");
3416 block_on(notify_builtin(target, "Changed".to_string(), Vec::new()))
3417 .expect("notify succeeds");
3418 assert_eq!(calls.load(Ordering::SeqCst), 1);
3419 });
3420 }
3421
3422 #[test]
3423 fn notify_char_handle_callback_surfaces_unresolved_identifier() {
3424 listener_gc_test(|| {
3425 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
3426 let target = block_on(new_handle_object_builtin("EventTarget".to_string()))
3427 .expect("handle target");
3428 block_on(addlistener_builtin(
3429 target.clone(),
3430 "Changed".to_string(),
3431 Value::CharArray(runmat_builtins::CharArray::new_row(
3432 "@definitely_missing_callback",
3433 )),
3434 ))
3435 .expect("listener registered");
3436 let err = block_on(notify_builtin(target, "Changed".to_string(), Vec::new()))
3437 .expect_err("unresolved char callback should fail");
3438 assert_eq!(err.identifier(), Some("RunMat:UndefinedFunction"));
3439 });
3440 }
3441
3442 #[test]
3443 fn notify_string_array_handle_callback_surfaces_unresolved_identifier() {
3444 listener_gc_test(|| {
3445 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
3446 let target = block_on(new_handle_object_builtin("EventTarget".to_string()))
3447 .expect("handle target");
3448 let callback = runmat_builtins::StringArray::new(
3449 vec!["@definitely_missing_callback".to_string()],
3450 vec![1, 1],
3451 )
3452 .expect("string array");
3453 block_on(addlistener_builtin(
3454 target.clone(),
3455 "Changed".to_string(),
3456 Value::StringArray(callback),
3457 ))
3458 .expect("listener registered");
3459 let err = block_on(notify_builtin(target, "Changed".to_string(), Vec::new()))
3460 .expect_err("unresolved string-array callback should fail");
3461 assert_eq!(err.identifier(), Some("RunMat:UndefinedFunction"));
3462 });
3463 }
3464
3465 #[test]
3466 fn feval_semantic_handle_honors_zero_requested_outputs() {
3467 let _guard = crate::user_functions::install_semantic_function_invoker(Some(Arc::new(
3468 |function, args, requested_outputs| {
3469 assert_eq!(function, 46);
3470 assert_eq!(requested_outputs, 0);
3471 assert_eq!(args, &[Value::Num(5.0)]);
3472 Box::pin(async { Ok(Value::OutputList(Vec::new())) })
3473 },
3474 )));
3475 let _output_guard = crate::output_count::push_output_count(Some(0));
3476 let handle = Value::BoundFunctionHandle {
3477 name: "function_target".to_string(),
3478 function: 46,
3479 };
3480
3481 let result = block_on(feval_builtin(handle, vec![Value::Num(5.0)]))
3482 .expect("semantic function handle feval succeeds");
3483 assert_eq!(result, Value::OutputList(Vec::new()));
3484 }
3485
3486 #[test]
3487 fn feval_semantic_handle_honors_multi_requested_outputs() {
3488 let _guard = crate::user_functions::install_semantic_function_invoker(Some(Arc::new(
3489 |function, args, requested_outputs| {
3490 assert_eq!(function, 47);
3491 assert_eq!(requested_outputs, 2);
3492 assert_eq!(args, &[Value::Num(6.0)]);
3493 Box::pin(async { Ok(Value::OutputList(vec![Value::Num(1.0), Value::Num(2.0)])) })
3494 },
3495 )));
3496 let _output_guard = crate::output_count::push_output_count(Some(2));
3497 let handle = Value::BoundFunctionHandle {
3498 name: "function_target".to_string(),
3499 function: 47,
3500 };
3501
3502 let result = block_on(feval_builtin(handle, vec![Value::Num(6.0)]))
3503 .expect("semantic function handle feval succeeds");
3504 assert_eq!(
3505 result,
3506 Value::OutputList(vec![Value::Num(1.0), Value::Num(2.0)])
3507 );
3508 }
3509
3510 #[test]
3511 fn feval_semantic_closure_errors_when_semantic_invoker_unavailable() {
3512 let _guard = crate::user_functions::install_semantic_function_invoker(None);
3513 let closure = Value::Closure(runmat_builtins::Closure {
3514 function_name: "function_target".to_string(),
3515 bound_function: Some(9044),
3516 captures: vec![Value::Num(1.0)],
3517 });
3518
3519 let err = block_on(feval_builtin(closure, vec![Value::Num(2.0)]))
3520 .expect_err("semantic closure should not fall back to name-based dispatch");
3521 assert_eq!(err.identifier(), Some("RunMat:SemanticFunctionUnavailable"));
3522 assert!(
3523 err.message()
3524 .contains("semantic closure 'function_target' (9044) is unavailable"),
3525 "unexpected error: {err:?}"
3526 );
3527 }
3528}