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