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