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