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 for name in [
1088 "plus", "times", "mtimes", "lt", "gt", "eq", "uplus", "rdivide", "mrdivide", "ldivide",
1089 "mldivide", "and", "or", "xor",
1090 ] {
1091 overidx_methods.insert(
1092 name.to_string(),
1093 MethodDef {
1094 name: name.to_string(),
1095 is_static: false,
1096 is_abstract: false,
1097 is_sealed: false,
1098 access: Access::Public,
1099 function_name: format!("OverIdx.{name}"),
1100 implicit_class_argument: None,
1101 },
1102 );
1103 }
1104 runmat_builtins::register_class(ClassDef {
1105 name: "OverIdx".to_string(),
1106 parent: None,
1107 properties: overidx_props,
1108 methods: overidx_methods,
1109 });
1110
1111 runmat_builtins::register_class(ClassDef {
1113 name: "NoIdx".to_string(),
1114 parent: None,
1115 properties: std::collections::HashMap::new(),
1116 methods: std::collections::HashMap::new(),
1117 });
1118 Ok(Value::Num(1.0))
1119}
1120
1121#[cfg(feature = "test-classes")]
1122pub async fn test_register_classes() {
1123 let _ = register_test_classes_builtin().await;
1124}
1125
1126const FEVAL_ERROR_HANDLE_NAME_INVALID: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
1128 code: "RM.FEVAL.HANDLE_NAME_INVALID",
1129 identifier: Some("RunMat:FevalHandleNameInvalid"),
1130 when: "A function or method handle name is empty.",
1131 message: "feval: function handle name must not be empty",
1132};
1133
1134const FEVAL_ERROR_HANDLE_STRING_INVALID: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
1135 code: "RM.FEVAL.HANDLE_STRING_INVALID",
1136 identifier: Some("RunMat:FevalHandleStringInvalid"),
1137 when: "Text handle input does not start with '@'.",
1138 message: "feval: expected function handle string starting with '@'",
1139};
1140
1141const FEVAL_ERROR_HANDLE_SHAPE_INVALID: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
1142 code: "RM.FEVAL.HANDLE_SHAPE_INVALID",
1143 identifier: Some("RunMat:FevalHandleShapeInvalid"),
1144 when: "Text handle input has invalid char/string array shape.",
1145 message: "feval: function handle text input must be scalar row text",
1146};
1147
1148const FEVAL_ERROR_SEMANTIC_UNAVAILABLE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
1149 code: "RM.FEVAL.SEMANTIC_UNAVAILABLE",
1150 identifier: Some("RunMat:SemanticFunctionUnavailable"),
1151 when: "Semantic function identity cannot be invoked in current runtime state.",
1152 message: "feval: semantic function handle is unavailable",
1153};
1154
1155const FEVAL_ERROR_FUNCTION_VALUE_UNSUPPORTED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
1156 code: "RM.FEVAL.FUNCTION_VALUE_UNSUPPORTED",
1157 identifier: Some("RunMat:FevalFunctionValueUnsupported"),
1158 when: "The first argument is not a supported callable value.",
1159 message: "feval: unsupported function value",
1160};
1161
1162pub(crate) const FEVAL_ERRORS: [BuiltinErrorDescriptor; 5] = [
1163 FEVAL_ERROR_HANDLE_NAME_INVALID,
1164 FEVAL_ERROR_HANDLE_STRING_INVALID,
1165 FEVAL_ERROR_HANDLE_SHAPE_INVALID,
1166 FEVAL_ERROR_SEMANTIC_UNAVAILABLE,
1167 FEVAL_ERROR_FUNCTION_VALUE_UNSUPPORTED,
1168];
1169
1170pub(crate) async fn feval_builtin(f: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
1171 fn normalize_feval_handle_name(name: &str) -> Option<String> {
1172 let trimmed = name.trim();
1173 (!trimmed.is_empty()).then(|| trimmed.to_string())
1174 }
1175
1176 async fn call_by_identity(
1177 identity: runmat_hir::CallableIdentity,
1178 fallback_policy: runmat_hir::CallableFallbackPolicy,
1179 args: &[Value],
1180 requested_outputs: usize,
1181 ) -> crate::BuiltinResult<Value> {
1182 dispatch_callable_with_policy(identity, fallback_policy, args.to_vec(), requested_outputs)
1183 .await
1184 }
1185
1186 async fn call_by_name(
1187 name: &str,
1188 args: &[Value],
1189 requested_outputs: usize,
1190 ) -> crate::BuiltinResult<Value> {
1191 let normalized = normalize_feval_handle_name(name)
1192 .ok_or_else(|| runtime_descriptor_error("feval", &FEVAL_ERROR_HANDLE_NAME_INVALID))?;
1193 let (identity, fallback_policy) = callable_identity_for_handle_name(&normalized);
1194 call_by_identity(identity, fallback_policy, args, requested_outputs).await
1195 }
1196
1197 let requested_outputs = crate::output_count::current_output_count().unwrap_or(1);
1198
1199 match f {
1200 Value::String(s) => {
1202 if let Some(name) = s.strip_prefix('@') {
1203 call_by_name(name, &rest, requested_outputs).await
1204 } else {
1205 Err(runtime_descriptor_error_with_detail(
1206 "feval",
1207 &FEVAL_ERROR_HANDLE_STRING_INVALID,
1208 format!("got {s}"),
1209 ))
1210 }
1211 }
1212 Value::CharArray(ca) => {
1214 if ca.rows == 1 {
1215 let s: String = ca.data.iter().collect();
1216 if let Some(name) = s.strip_prefix('@') {
1217 call_by_name(name, &rest, requested_outputs).await
1218 } else {
1219 Err(runtime_descriptor_error_with_detail(
1220 "feval",
1221 &FEVAL_ERROR_HANDLE_STRING_INVALID,
1222 format!("got {s}"),
1223 ))
1224 }
1225 } else {
1226 Err(runtime_descriptor_error_with_detail(
1227 "feval",
1228 &FEVAL_ERROR_HANDLE_SHAPE_INVALID,
1229 "char array must be a row vector",
1230 ))
1231 }
1232 }
1233 Value::StringArray(sa) => {
1234 if sa.data.len() == 1 {
1235 let s = &sa.data[0];
1236 if let Some(name) = s.strip_prefix('@') {
1237 call_by_name(name, &rest, requested_outputs).await
1238 } else {
1239 Err(runtime_descriptor_error_with_detail(
1240 "feval",
1241 &FEVAL_ERROR_HANDLE_STRING_INVALID,
1242 format!("got {s}"),
1243 ))
1244 }
1245 } else {
1246 Err(runtime_descriptor_error_with_detail(
1247 "feval",
1248 &FEVAL_ERROR_HANDLE_SHAPE_INVALID,
1249 "string array must be scalar",
1250 ))
1251 }
1252 }
1253 Value::FunctionHandle(name) => call_by_name(&name, &rest, requested_outputs).await,
1254 Value::ExternalFunctionHandle(name) => call_by_name(&name, &rest, requested_outputs).await,
1255 Value::MethodFunctionHandle(name) => {
1256 let method_name = name.trim().to_string();
1257 if method_name.is_empty() {
1258 return Err(runtime_descriptor_error(
1259 "feval",
1260 &FEVAL_ERROR_HANDLE_NAME_INVALID,
1261 ));
1262 }
1263 dispatch_callable_with_policy(
1264 runmat_hir::CallableIdentity::Method(runmat_hir::MethodId(method_name)),
1265 runmat_hir::CallableFallbackPolicy::RuntimeNameResolution,
1266 rest,
1267 requested_outputs,
1268 )
1269 .await
1270 }
1271 Value::BoundFunctionHandle { name, function } => {
1272 let request = crate::user_functions::CallableRequest::semantic(
1273 function,
1274 rest.clone(),
1275 requested_outputs,
1276 );
1277 if let Some(result) = crate::user_functions::try_call_semantic_descriptor(request).await
1278 {
1279 return result;
1280 }
1281 Err(runtime_descriptor_error_with_detail(
1282 "feval",
1283 &FEVAL_ERROR_SEMANTIC_UNAVAILABLE,
1284 format!("semantic function handle '{name}' ({function}) is unavailable"),
1285 ))
1286 }
1287 Value::Closure(c) => {
1288 if let Some(function) = c.bound_function {
1289 let mut args = c.captures.clone();
1290 args.extend(rest);
1291 let request = crate::user_functions::CallableRequest::semantic(
1292 function,
1293 args.clone(),
1294 requested_outputs,
1295 );
1296 if let Some(result) =
1297 crate::user_functions::try_call_semantic_descriptor(request).await
1298 {
1299 return result;
1300 }
1301 return Err(runtime_descriptor_error_with_detail(
1302 "feval",
1303 &FEVAL_ERROR_SEMANTIC_UNAVAILABLE,
1304 format!(
1305 "semantic closure '{}' ({function}) is unavailable",
1306 c.function_name
1307 ),
1308 ));
1309 }
1310
1311 if c.function_name == CALL_METHOD_BUILTIN_NAME && c.captures.len() >= 2 {
1312 let base = c.captures[0].clone();
1313 let method = match &c.captures[1] {
1314 Value::String(name) => name.clone(),
1315 Value::CharArray(chars) if chars.rows == 1 => chars.data.iter().collect(),
1316 _ => {
1317 return Err(build_runtime_error(
1318 "call_method: closure captures must include method name text",
1319 )
1320 .with_builtin("call_method")
1321 .with_identifier("RunMat:CallMethodNameInvalid")
1322 .build())
1323 }
1324 };
1325 let mut method_args = c.captures.iter().skip(2).cloned().collect::<Vec<_>>();
1326 method_args.extend(rest);
1327 return crate::builtins::introspection::call_method::dispatch_call_method(
1328 base,
1329 method,
1330 method_args,
1331 )
1332 .await;
1333 }
1334
1335 let mut args = c.captures.clone();
1336 args.extend(rest);
1337 if let Some(function) =
1338 crate::user_functions::resolve_semantic_function_by_name(&c.function_name)
1339 {
1340 let request = crate::user_functions::CallableRequest::semantic(
1341 function,
1342 args.clone(),
1343 requested_outputs,
1344 );
1345 if let Some(result) =
1346 crate::user_functions::try_call_semantic_descriptor(request).await
1347 {
1348 return result;
1349 }
1350 }
1351 call_by_name(&c.function_name, &args, requested_outputs).await
1352 }
1353 receiver @ Value::Object(_) | receiver @ Value::HandleObject(_) => {
1354 let payload = Value::Cell(build_shape_checked_cell(
1355 rest.clone(),
1356 1,
1357 rest.len(),
1358 "feval object index payload",
1359 )?);
1360 crate::builtins::introspection::object_indexing::dispatch_subsref(
1361 receiver,
1362 OBJECT_INDEX_PAREN.to_string(),
1363 payload,
1364 )
1365 .await
1366 }
1367 other => Err(runtime_descriptor_error_with_detail(
1368 "feval",
1369 &FEVAL_ERROR_FUNCTION_VALUE_UNSUPPORTED,
1370 format!("{other:?}"),
1371 )),
1372 }
1373}
1374
1375#[cfg(test)]
1376mod tests {
1377 use super::*;
1378 use crate::builtins::introspection::test_methods::*;
1379 use futures::executor::block_on;
1380 use runmat_builtins::{register_class, Access, ClassDef, PropertyDef};
1381 use std::collections::HashMap;
1382 use std::sync::{
1383 atomic::{AtomicU64, AtomicUsize, Ordering},
1384 Arc,
1385 };
1386
1387 static TEST_CLASS_COUNTER: AtomicU64 = AtomicU64::new(0);
1388
1389 fn unique_class_name(prefix: &str) -> String {
1390 let id = TEST_CLASS_COUNTER.fetch_add(1, Ordering::Relaxed);
1391 format!("{}_{}", prefix, id)
1392 }
1393
1394 #[test]
1395 fn descriptor_migration_covers_lib_runtime_builtins() {
1396 let cases = [
1397 ("deal", "[varargout] = deal(varargin)"),
1398 ("rethrow", "rethrow(err)"),
1399 ("call_method", "[out] = call_method(base, method, varargin)"),
1400 (
1401 "new_handle_object",
1402 "handle = new_handle_object(class_name)",
1403 ),
1404 (
1405 "addlistener",
1406 "listener = addlistener(target, event_name, callback)",
1407 ),
1408 ("notify", "status = notify(target, event_name, varargin)"),
1409 ("get.p", "value = get.p(obj)"),
1410 ("set.p", "obj = set.p(obj, value)"),
1411 ("make_anon", "handle_text = make_anon(params, body)"),
1412 ("classref", "ref = classref(class_name)"),
1413 (
1414 "__register_test_classes",
1415 "status = __register_test_classes()",
1416 ),
1417 ("Point.move", "obj = Point.move(obj, dx, dy)"),
1418 ("Circle.area", "area = Circle.area(obj)"),
1419 ("Ctor.Ctor", "obj = Ctor.Ctor(x)"),
1420 ("PkgF.foo", "value = PkgF.foo()"),
1421 ("OverIdx.plus", "out = OverIdx.plus(obj, rhs)"),
1422 (
1423 "OverIdx.subsref",
1424 "out = OverIdx.subsref(obj, kind, payload)",
1425 ),
1426 ("feval", "[varargout] = feval(f, varargin)"),
1427 ("str2func", "fh = str2func(name)"),
1428 ("func2str", "name = func2str(fh)"),
1429 ("functions", "info = functions(fh)"),
1430 ("inputname", "name = inputname(argNumber)"),
1431 ("localfunctions", "handles = localfunctions()"),
1432 ("narginchk", "narginchk(minArgs, maxArgs)"),
1433 ("nargoutchk", "nargoutchk(minArgs, maxArgs)"),
1434 ("mfilename", "name = mfilename()"),
1435 ("getmethod", "fh = getmethod(obj_or_class, name)"),
1436 ];
1437
1438 for (name, label) in cases {
1439 let builtin = runmat_builtins::builtin_function_by_name(name)
1440 .unwrap_or_else(|| panic!("builtin {name} not registered"));
1441 let descriptor = builtin
1442 .descriptor
1443 .unwrap_or_else(|| panic!("descriptor missing for {name}"));
1444 assert!(
1445 descriptor.signatures.iter().any(|sig| sig.label == label),
1446 "missing signature {label} for {name}"
1447 );
1448 }
1449 }
1450
1451 #[test]
1452 fn feval_closure_uses_semantic_function_identity() {
1453 let _guard = crate::user_functions::install_semantic_function_invoker(Some(Arc::new(
1454 |function, args, requested_outputs| {
1455 assert_eq!(function, 42);
1456 assert_eq!(requested_outputs, 1);
1457 assert_eq!(args, &[Value::Num(2.0)]);
1458 Box::pin(async { Ok(Value::Num(7.0)) })
1459 },
1460 )));
1461 let closure = Value::Closure(runmat_builtins::Closure {
1462 function_name: "function_target".to_string(),
1463 bound_function: Some(42),
1464 captures: Vec::new(),
1465 });
1466
1467 let result = block_on(feval_builtin(closure, vec![Value::Num(2.0)]))
1468 .expect("semantic closure feval succeeds");
1469 assert_eq!(result, Value::Num(7.0));
1470 }
1471
1472 #[test]
1473 fn feval_semantic_function_handle_uses_semantic_identity() {
1474 let _guard = crate::user_functions::install_semantic_function_invoker(Some(Arc::new(
1475 |function, args, requested_outputs| {
1476 assert_eq!(function, 43);
1477 assert_eq!(requested_outputs, 1);
1478 assert_eq!(args, &[Value::Num(3.0)]);
1479 Box::pin(async { Ok(Value::Num(9.0)) })
1480 },
1481 )));
1482 let handle = Value::BoundFunctionHandle {
1483 name: "function_target".to_string(),
1484 function: 43,
1485 };
1486
1487 let result = block_on(feval_builtin(handle, vec![Value::Num(3.0)]))
1488 .expect("semantic function handle feval succeeds");
1489 assert_eq!(result, Value::Num(9.0));
1490 }
1491
1492 #[test]
1493 fn feval_semantic_function_handle_errors_when_semantic_invoker_unavailable() {
1494 let _guard = crate::user_functions::install_semantic_function_invoker(None);
1495 let handle = Value::BoundFunctionHandle {
1496 name: "function_target".to_string(),
1497 function: 9043,
1498 };
1499
1500 let err = block_on(feval_builtin(handle, vec![Value::Num(3.0)])).expect_err(
1501 "semantic function handle should not fall back to name-based dispatch when unavailable",
1502 );
1503 assert_eq!(err.identifier(), Some("RunMat:SemanticFunctionUnavailable"));
1504 assert!(
1505 err.message()
1506 .contains("semantic function handle 'function_target' (9043) is unavailable"),
1507 "unexpected error: {err:?}"
1508 );
1509 }
1510
1511 #[test]
1512 fn feval_name_only_handle_uses_semantic_resolver() {
1513 let _resolver_guard =
1514 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
1515 (name == "resolved_target").then_some(45)
1516 })));
1517 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
1518 Arc::new(|function, args, requested_outputs| {
1519 assert_eq!(function, 45);
1520 assert_eq!(requested_outputs, 1);
1521 assert_eq!(args, &[Value::Num(4.0)]);
1522 Box::pin(async { Ok(Value::Num(11.0)) })
1523 }),
1524 ));
1525
1526 let result = block_on(feval_builtin(
1527 Value::FunctionHandle("resolved_target".to_string()),
1528 vec![Value::Num(4.0)],
1529 ))
1530 .expect("resolved name-only handle feval succeeds");
1531 assert_eq!(result, Value::Num(11.0));
1532 }
1533
1534 #[test]
1535 fn feval_method_function_handle_uses_semantic_resolver() {
1536 let _resolver_guard =
1537 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
1538 (name == "resolved_method").then_some(5045)
1539 })));
1540 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
1541 Arc::new(|function, args, requested_outputs| {
1542 assert_eq!(function, 5045);
1543 assert_eq!(requested_outputs, 1);
1544 assert_eq!(args, &[Value::Num(4.0)]);
1545 Box::pin(async { Ok(Value::Num(15.0)) })
1546 }),
1547 ));
1548
1549 let result = block_on(feval_builtin(
1550 Value::MethodFunctionHandle("resolved_method".to_string()),
1551 vec![Value::Num(4.0)],
1552 ))
1553 .expect("resolved method handle feval succeeds");
1554 assert_eq!(result, Value::Num(15.0));
1555 }
1556
1557 #[test]
1558 fn feval_method_function_handle_does_not_fallback_to_builtin_name() {
1559 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
1560 let err = block_on(feval_builtin(
1561 Value::MethodFunctionHandle("sqrt".to_string()),
1562 vec![Value::Num(9.0)],
1563 ))
1564 .expect_err("method function handle should not fallback to builtin name dispatch");
1565 assert_eq!(err.identifier(), Some("RunMat:UndefinedFunction"));
1566 }
1567
1568 #[test]
1569 fn feval_name_only_closure_uses_semantic_resolver() {
1570 let _resolver_guard =
1571 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
1572 (name == "resolved_target").then_some(145)
1573 })));
1574 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
1575 Arc::new(|function, args, requested_outputs| {
1576 assert_eq!(function, 145);
1577 assert_eq!(requested_outputs, 1);
1578 assert_eq!(args, &[Value::Num(9.0), Value::Num(4.0)]);
1579 Box::pin(async { Ok(Value::Num(13.0)) })
1580 }),
1581 ));
1582
1583 let closure = Value::Closure(runmat_builtins::Closure {
1584 function_name: "resolved_target".to_string(),
1585 bound_function: None,
1586 captures: vec![Value::Num(9.0)],
1587 });
1588
1589 let result = block_on(feval_builtin(closure, vec![Value::Num(4.0)]))
1590 .expect("resolved name-only closure feval succeeds");
1591 assert_eq!(result, Value::Num(13.0));
1592 }
1593
1594 #[test]
1595 fn feval_name_only_closure_falls_back_when_semantic_invoker_unavailable() {
1596 let _resolver_guard =
1597 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
1598 (name == "sin").then_some(245)
1599 })));
1600 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(None);
1601
1602 let closure = Value::Closure(runmat_builtins::Closure {
1603 function_name: "sin".to_string(),
1604 bound_function: None,
1605 captures: Vec::new(),
1606 });
1607
1608 let result =
1609 block_on(feval_builtin(closure, vec![Value::Num(0.0)])).expect("sin fallback works");
1610 assert_eq!(result, Value::Num(0.0));
1611 }
1612
1613 #[test]
1614 fn feval_external_function_handle_errors_when_unresolved() {
1615 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
1616 let err = block_on(feval_builtin(
1617 Value::ExternalFunctionHandle("missing.external".to_string()),
1618 vec![Value::Num(1.0)],
1619 ))
1620 .expect_err("external function handle should error when unresolved");
1621 assert_eq!(err.identifier(), Some("RunMat:UndefinedFunction"));
1622 assert!(
1623 err.message().contains("missing.external"),
1624 "unexpected error: {err:?}"
1625 );
1626 }
1627
1628 #[test]
1629 fn feval_single_segment_external_function_handle_uses_runtime_name_resolution() {
1630 let _resolver_guard =
1631 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
1632 (name == "resolved_target").then_some(4501)
1633 })));
1634 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
1635 Arc::new(|function, args, requested_outputs| {
1636 assert_eq!(function, 4501);
1637 assert_eq!(requested_outputs, 1);
1638 assert_eq!(args, &[Value::Num(4.0)]);
1639 Box::pin(async { Ok(Value::Num(12.0)) })
1640 }),
1641 ));
1642
1643 let result = block_on(feval_builtin(
1644 Value::ExternalFunctionHandle("resolved_target".to_string()),
1645 vec![Value::Num(4.0)],
1646 ))
1647 .expect("single-segment external function handle should use runtime-name resolution");
1648 assert_eq!(result, Value::Num(12.0));
1649 }
1650
1651 #[test]
1652 fn feval_rejects_string_without_at_with_identifier() {
1653 let err = block_on(feval_builtin(
1654 Value::String("sin".to_string()),
1655 vec![Value::Num(0.0)],
1656 ))
1657 .expect_err("feval string handle without @ should fail");
1658 assert_eq!(err.identifier(), Some("RunMat:FevalHandleStringInvalid"));
1659 }
1660
1661 #[test]
1662 fn feval_rejects_char_handle_without_at_with_identifier() {
1663 let err = block_on(feval_builtin(
1664 Value::CharArray(runmat_builtins::CharArray::new_row("sin")),
1665 vec![Value::Num(0.0)],
1666 ))
1667 .expect_err("feval char handle without @ should fail");
1668 assert_eq!(err.identifier(), Some("RunMat:FevalHandleStringInvalid"));
1669 }
1670
1671 #[test]
1672 fn feval_rejects_non_row_char_handle_with_identifier() {
1673 let chars = runmat_builtins::CharArray::new(vec!['@', 's'], 2, 1)
1674 .expect("char array construction should succeed");
1675 let err = block_on(feval_builtin(
1676 Value::CharArray(chars),
1677 vec![Value::Num(0.0)],
1678 ))
1679 .expect_err("feval non-row char handle should fail");
1680 assert_eq!(err.identifier(), Some("RunMat:FevalHandleShapeInvalid"));
1681 }
1682
1683 #[test]
1684 fn feval_rejects_empty_at_string_handle_with_identifier() {
1685 let err = block_on(feval_builtin(
1686 Value::String("@".to_string()),
1687 vec![Value::Num(0.0)],
1688 ))
1689 .expect_err("feval empty @string handle should fail");
1690 assert_eq!(err.identifier(), Some("RunMat:FevalHandleNameInvalid"));
1691 }
1692
1693 #[test]
1694 fn feval_rejects_empty_function_handle_value_with_identifier() {
1695 let err = block_on(feval_builtin(
1696 Value::FunctionHandle(String::new()),
1697 vec![Value::Num(0.0)],
1698 ))
1699 .expect_err("feval empty function-handle value should fail");
1700 assert_eq!(err.identifier(), Some("RunMat:FevalHandleNameInvalid"));
1701 }
1702
1703 #[test]
1704 fn feval_trims_text_handle_name_for_resolution() {
1705 let _resolver_guard =
1706 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
1707 (name == "resolved_target").then_some(9876)
1708 })));
1709 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
1710 Arc::new(|function, args, requested_outputs| {
1711 assert_eq!(function, 9876);
1712 assert_eq!(requested_outputs, 1);
1713 assert_eq!(args, &[Value::Num(4.0)]);
1714 Box::pin(async { Ok(Value::Num(12.0)) })
1715 }),
1716 ));
1717
1718 let value = block_on(feval_builtin(
1719 Value::String("@ resolved_target ".to_string()),
1720 vec![Value::Num(4.0)],
1721 ))
1722 .expect("trimmed text handle should resolve");
1723 assert_eq!(value, Value::Num(12.0));
1724 }
1725
1726 #[test]
1727 fn str2func_returns_semantic_handle_when_resolver_can_resolve() {
1728 let _resolver_guard =
1729 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
1730 (name == "resolved_target").then_some(145)
1731 })));
1732 let value = crate::builtins::introspection::function_handle_text::dispatch_str2func(
1733 Value::String("resolved_target".to_string()),
1734 )
1735 .expect("str2func should succeed");
1736 assert_eq!(
1737 value,
1738 Value::BoundFunctionHandle {
1739 name: "resolved_target".to_string(),
1740 function: 145,
1741 }
1742 );
1743 }
1744
1745 #[test]
1746 fn str2func_returns_dynamic_handle_when_resolver_cannot_resolve() {
1747 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
1748 let value = crate::builtins::introspection::function_handle_text::dispatch_str2func(
1749 Value::String("@missing_target".to_string()),
1750 )
1751 .expect("str2func should succeed");
1752 assert_eq!(value, Value::FunctionHandle("missing_target".to_string()));
1753 }
1754
1755 #[test]
1756 fn str2func_returns_external_handle_for_qualified_name() {
1757 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
1758 let value = crate::builtins::introspection::function_handle_text::dispatch_str2func(
1759 Value::String("Point.origin".to_string()),
1760 )
1761 .expect("str2func should succeed");
1762 assert_eq!(
1763 value,
1764 Value::ExternalFunctionHandle("Point.origin".to_string())
1765 );
1766 }
1767
1768 #[test]
1769 fn str2func_malformed_qualified_name_returns_dynamic_handle() {
1770 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
1771 let value = crate::builtins::introspection::function_handle_text::dispatch_str2func(
1772 Value::String("Point..origin".to_string()),
1773 )
1774 .expect("str2func should succeed");
1775 assert_eq!(value, Value::FunctionHandle("Point..origin".to_string()));
1776 }
1777
1778 #[test]
1779 fn func2str_rejects_non_handle_with_identifier() {
1780 let err = crate::builtins::introspection::function_handle_text::dispatch_func2str(
1781 Value::Num(1.0),
1782 )
1783 .expect_err("func2str non-handle input should fail");
1784 assert_eq!(err.identifier(), Some("RunMat:Func2StrHandleTypeInvalid"));
1785 }
1786
1787 #[test]
1788 fn str2func_rejects_empty_name_with_identifier() {
1789 let err = crate::builtins::introspection::function_handle_text::dispatch_str2func(
1790 Value::String(" ".to_string()),
1791 )
1792 .expect_err("empty function name should fail");
1793 assert_eq!(err.identifier(), Some("RunMat:Str2FuncNameInvalid"));
1794 }
1795
1796 #[test]
1797 fn str2func_rejects_non_row_char_name_with_identifier() {
1798 let chars = runmat_builtins::CharArray::new(vec!['a', 'b'], 2, 1)
1799 .expect("char array construction should succeed");
1800 let err = crate::builtins::introspection::function_handle_text::dispatch_str2func(
1801 Value::CharArray(chars),
1802 )
1803 .expect_err("non-row char-array function name should fail");
1804 assert_eq!(err.identifier(), Some("RunMat:Str2FuncNameShapeInvalid"));
1805 }
1806
1807 #[test]
1808 fn str2func_rejects_non_text_name_with_identifier() {
1809 let err = crate::builtins::introspection::function_handle_text::dispatch_str2func(
1810 Value::Num(1.0),
1811 )
1812 .expect_err("non-text function name should fail");
1813 assert_eq!(err.identifier(), Some("RunMat:Str2FuncNameTypeInvalid"));
1814 }
1815
1816 #[test]
1817 fn str2func_accepts_scalar_string_array_name() {
1818 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
1819 let value = crate::builtins::introspection::function_handle_text::dispatch_str2func(
1820 Value::StringArray(
1821 runmat_builtins::StringArray::new(vec!["@missing_target".to_string()], vec![1, 1])
1822 .expect("string array construction should succeed"),
1823 ),
1824 )
1825 .expect("scalar string-array function name should succeed");
1826 assert_eq!(value, Value::FunctionHandle("missing_target".to_string()));
1827 }
1828
1829 #[test]
1830 fn str2func_rejects_nonscalar_string_array_name_with_identifier() {
1831 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
1832 let value = Value::StringArray(
1833 runmat_builtins::StringArray::new(vec!["@a".to_string(), "@b".to_string()], vec![1, 2])
1834 .expect("string array construction should succeed"),
1835 );
1836 let err = crate::builtins::introspection::function_handle_text::dispatch_str2func(value)
1837 .expect_err("nonscalar string-array function name must fail");
1838 assert_eq!(err.identifier(), Some("RunMat:Str2FuncNameShapeInvalid"));
1839 }
1840
1841 #[test]
1842 fn str2func_scalar_string_array_prefers_semantic_handle_when_resolved() {
1843 let _resolver_guard =
1844 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
1845 (name == "resolved_target").then_some(445)
1846 })));
1847 let value = crate::builtins::introspection::function_handle_text::dispatch_str2func(
1848 Value::StringArray(
1849 runmat_builtins::StringArray::new(vec!["@resolved_target".to_string()], vec![1, 1])
1850 .expect("string array construction should succeed"),
1851 ),
1852 )
1853 .expect("scalar string-array function name should resolve semantically");
1854 assert_eq!(
1855 value,
1856 Value::BoundFunctionHandle {
1857 name: "resolved_target".to_string(),
1858 function: 445,
1859 }
1860 );
1861 }
1862
1863 #[test]
1864 fn str2func_scalar_string_array_returns_external_handle_for_qualified_name() {
1865 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
1866 let value = crate::builtins::introspection::function_handle_text::dispatch_str2func(
1867 Value::StringArray(
1868 runmat_builtins::StringArray::new(vec!["Point.origin".to_string()], vec![1, 1])
1869 .expect("string array construction should succeed"),
1870 ),
1871 )
1872 .expect("scalar string-array qualified name should succeed");
1873 assert_eq!(
1874 value,
1875 Value::ExternalFunctionHandle("Point.origin".to_string())
1876 );
1877 }
1878
1879 #[test]
1880 fn str2func_scalar_string_array_malformed_qualified_name_returns_dynamic_handle() {
1881 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
1882 let value = crate::builtins::introspection::function_handle_text::dispatch_str2func(
1883 Value::StringArray(
1884 runmat_builtins::StringArray::new(vec!["Point..origin".to_string()], vec![1, 1])
1885 .expect("string array construction should succeed"),
1886 ),
1887 )
1888 .expect("scalar string-array malformed qualified name should succeed");
1889 assert_eq!(value, Value::FunctionHandle("Point..origin".to_string()));
1890 }
1891
1892 #[test]
1893 fn str2func_scalar_string_array_rejects_empty_name_with_identifier() {
1894 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
1895 let err = crate::builtins::introspection::function_handle_text::dispatch_str2func(
1896 Value::StringArray(
1897 runmat_builtins::StringArray::new(vec![" ".to_string()], vec![1, 1])
1898 .expect("string array construction should succeed"),
1899 ),
1900 )
1901 .expect_err("scalar string-array empty function name should fail");
1902 assert_eq!(err.identifier(), Some("RunMat:Str2FuncNameInvalid"));
1903 }
1904
1905 #[test]
1906 fn str2func_scalar_string_array_qualified_name_prefers_semantic_handle_when_resolved() {
1907 let _resolver_guard =
1908 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
1909 (name == "pkg.resolved_target").then_some(446)
1910 })));
1911 let value = crate::builtins::introspection::function_handle_text::dispatch_str2func(
1912 Value::StringArray(
1913 runmat_builtins::StringArray::new(
1914 vec!["@pkg.resolved_target".to_string()],
1915 vec![1, 1],
1916 )
1917 .expect("string array construction should succeed"),
1918 ),
1919 )
1920 .expect("scalar string-array qualified function name should resolve semantically");
1921 assert_eq!(
1922 value,
1923 Value::BoundFunctionHandle {
1924 name: "pkg.resolved_target".to_string(),
1925 function: 446,
1926 }
1927 );
1928 }
1929
1930 #[test]
1931 fn getmethod_classref_returns_typed_external_function_handle() {
1932 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
1933 let value = crate::builtins::introspection::getmethod::dispatch_getmethod(
1934 Value::ClassRef("Point".to_string()),
1935 "origin".to_string(),
1936 )
1937 .expect("getmethod should resolve classref method handle");
1938 assert_eq!(
1939 value,
1940 Value::ExternalFunctionHandle("Point.origin".to_string())
1941 );
1942 }
1943
1944 #[test]
1945 fn getmethod_rejects_empty_method_name() {
1946 let err = crate::builtins::introspection::getmethod::dispatch_getmethod(
1947 Value::ClassRef("Point".to_string()),
1948 " ".to_string(),
1949 )
1950 .expect_err("empty method name should be rejected");
1951 assert_eq!(err.identifier(), Some("RunMat:GetMethodNameInvalid"));
1952 }
1953
1954 #[test]
1955 fn getmethod_rejects_unsupported_receiver_with_identifier() {
1956 let err = crate::builtins::introspection::getmethod::dispatch_getmethod(
1957 Value::Num(1.0),
1958 "origin".to_string(),
1959 )
1960 .expect_err("unsupported receiver should be rejected");
1961 assert_eq!(
1962 err.identifier(),
1963 Some("RunMat:GetMethodReceiverUnsupported")
1964 );
1965 }
1966
1967 #[test]
1968 fn create_class_object_handles_class_parent_cycles() {
1969 let class_a = unique_class_name("runtime_ctor_cycle_a");
1970 let class_b = unique_class_name("runtime_ctor_cycle_b");
1971
1972 let mut props_a = HashMap::new();
1973 props_a.insert(
1974 "fromA".to_string(),
1975 PropertyDef {
1976 name: "fromA".to_string(),
1977 is_static: false,
1978 is_constant: false,
1979 is_dependent: false,
1980 get_access: Access::Public,
1981 set_access: Access::Public,
1982 default_value: Some(Value::Num(1.0)),
1983 },
1984 );
1985 let mut props_b = HashMap::new();
1986 props_b.insert(
1987 "fromB".to_string(),
1988 PropertyDef {
1989 name: "fromB".to_string(),
1990 is_static: false,
1991 is_constant: false,
1992 is_dependent: false,
1993 get_access: Access::Public,
1994 set_access: Access::Public,
1995 default_value: Some(Value::Num(2.0)),
1996 },
1997 );
1998
1999 register_class(ClassDef {
2000 name: class_a.clone(),
2001 parent: Some(class_b.clone()),
2002 properties: props_a,
2003 methods: HashMap::new(),
2004 });
2005 register_class(ClassDef {
2006 name: class_b,
2007 parent: Some(class_a.clone()),
2008 properties: props_b,
2009 methods: HashMap::new(),
2010 });
2011
2012 let value = block_on(create_class_object(class_a.clone()))
2013 .expect("constructor should terminate under parent-cycle metadata");
2014 let Value::Object(obj) = value else {
2015 panic!("expected object result");
2016 };
2017 assert_eq!(obj.class_name, class_a);
2018 assert_eq!(obj.properties.get("fromA"), Some(&Value::Num(1.0)));
2019 assert_eq!(obj.properties.get("fromB"), Some(&Value::Num(2.0)));
2020 }
2021
2022 #[test]
2023 fn create_class_object_abstract_class_reports_stable_identifier() {
2024 let class_name = unique_class_name("runtime_ctor_abstract");
2025 runmat_builtins::register_class_with_modifiers(
2026 ClassDef {
2027 name: class_name.clone(),
2028 parent: None,
2029 properties: HashMap::new(),
2030 methods: HashMap::new(),
2031 },
2032 false,
2033 true,
2034 );
2035
2036 let err = block_on(create_class_object(class_name))
2037 .expect_err("abstract class instantiation should fail");
2038 assert_eq!(err.identifier(), Some("RunMat:AbstractMethodMissing"));
2039 assert!(err.message().contains("Cannot instantiate abstract class"));
2040 }
2041
2042 #[test]
2043 fn callable_identity_for_malformed_handle_name_stays_dynamic() {
2044 let (identity, fallback_policy) = callable_identity_for_handle_name("pkg..remote_inc");
2045 assert!(matches!(
2046 identity,
2047 runmat_hir::CallableIdentity::DynamicName(runmat_hir::SymbolName(name))
2048 if name == "pkg..remote_inc"
2049 ));
2050 assert_eq!(
2051 fallback_policy,
2052 runmat_hir::CallableFallbackPolicy::RuntimeNameResolution
2053 );
2054 }
2055
2056 #[test]
2057 fn unresolved_callable_without_display_name_reports_typed_identity() {
2058 let err = block_on(dispatch_callable_with_policy(
2059 runmat_hir::CallableIdentity::AnonymousFunction(runmat_hir::FunctionId(77)),
2060 runmat_hir::CallableFallbackPolicy::RuntimeNameResolution,
2061 vec![],
2062 1,
2063 ))
2064 .expect_err("anonymous callable identity should fail unresolved");
2065 assert_eq!(err.identifier(), Some("RunMat:UndefinedFunction"));
2066 assert!(
2067 err.message().contains("AnonymousFunction(FunctionId(77))"),
2068 "unexpected error: {err:?}"
2069 );
2070 }
2071
2072 #[test]
2073 fn unresolved_malformed_external_callable_reports_typed_identity() {
2074 let err = block_on(dispatch_callable_with_policy(
2075 runmat_hir::CallableIdentity::ExternalName(runmat_hir::QualifiedName(vec![
2076 runmat_hir::SymbolName("pkg".to_string()),
2077 runmat_hir::SymbolName("".to_string()),
2078 runmat_hir::SymbolName("remote".to_string()),
2079 ])),
2080 runmat_hir::CallableFallbackPolicy::ExternalBoundary,
2081 vec![],
2082 1,
2083 ))
2084 .expect_err("malformed external callable identity should fail unresolved");
2085 assert_eq!(err.identifier(), Some("RunMat:UndefinedFunction"));
2086 assert!(
2087 err.message()
2088 .contains("ExternalName(QualifiedName([SymbolName(\"pkg\"), SymbolName(\"\"), SymbolName(\"remote\")]))"),
2089 "unexpected error: {err:?}"
2090 );
2091 }
2092
2093 #[test]
2094 fn unresolved_method_callable_reports_typed_identity() {
2095 let err = block_on(dispatch_callable_with_policy(
2096 runmat_hir::CallableIdentity::Method(runmat_hir::MethodId(
2097 "missing_method".to_string(),
2098 )),
2099 runmat_hir::CallableFallbackPolicy::RuntimeNameResolution,
2100 vec![],
2101 1,
2102 ))
2103 .expect_err("method callable identity should fail unresolved");
2104 assert_eq!(err.identifier(), Some("RunMat:UndefinedFunction"));
2105 assert!(
2106 err.message()
2107 .contains("Method(MethodId(\"missing_method\"))"),
2108 "unexpected error: {err:?}"
2109 );
2110 assert!(
2111 !err.message()
2112 .contains("Undefined function 'missing_method'"),
2113 "method identity should not use fallback display-name text: {err:?}"
2114 );
2115 }
2116
2117 #[test]
2118 fn feval_qualified_at_handle_errors_as_unresolved_external() {
2119 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
2120 let err = block_on(feval_builtin(
2121 Value::String("@missing.external".to_string()),
2122 vec![Value::Num(1.0)],
2123 ))
2124 .expect_err("qualified @handle should error when unresolved");
2125 assert_eq!(err.identifier(), Some("RunMat:UndefinedFunction"));
2126 assert!(
2127 err.message().contains("missing.external"),
2128 "unexpected error: {err:?}"
2129 );
2130 }
2131
2132 #[test]
2133 fn func2str_extracts_name_from_function_handles() {
2134 assert_eq!(
2135 crate::builtins::introspection::function_handle_text::dispatch_func2str(
2136 Value::FunctionHandle("sin".to_string())
2137 )
2138 .expect("func2str"),
2139 Value::String("sin".to_string())
2140 );
2141 assert_eq!(
2142 crate::builtins::introspection::function_handle_text::dispatch_func2str(
2143 Value::ExternalFunctionHandle("Point.origin".to_string())
2144 )
2145 .expect("func2str"),
2146 Value::String("Point.origin".to_string())
2147 );
2148 assert_eq!(
2149 crate::builtins::introspection::function_handle_text::dispatch_func2str(
2150 Value::BoundFunctionHandle {
2151 name: "local_fn".to_string(),
2152 function: 44,
2153 }
2154 )
2155 .expect("func2str"),
2156 Value::String("local_fn".to_string())
2157 );
2158 assert_eq!(
2159 crate::builtins::introspection::function_handle_text::dispatch_func2str(
2160 Value::Closure(runmat_builtins::Closure {
2161 function_name: "captured_fn".to_string(),
2162 bound_function: None,
2163 captures: Vec::new(),
2164 })
2165 )
2166 .expect("func2str"),
2167 Value::String("captured_fn".to_string())
2168 );
2169 }
2170
2171 #[test]
2172 fn none_policy_does_not_use_semantic_resolver() {
2173 let _resolver_guard =
2174 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2175 (name == "resolved_target").then_some(45)
2176 })));
2177 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
2178 Arc::new(|function, args, requested_outputs| {
2179 assert_eq!(function, 45);
2180 assert_eq!(requested_outputs, 1);
2181 assert_eq!(args, &[Value::Num(4.0)]);
2182 Box::pin(async { Ok(Value::Num(11.0)) })
2183 }),
2184 ));
2185
2186 let request = crate::user_functions::CallableRequest::resolved(
2187 runmat_hir::CallableIdentity::DynamicName(runmat_hir::SymbolName(
2188 "resolved_target".to_string(),
2189 )),
2190 runmat_hir::CallableFallbackPolicy::None,
2191 vec![Value::Num(4.0)],
2192 1,
2193 );
2194
2195 let result = block_on(crate::user_functions::try_call_semantic_descriptor(request));
2196 assert!(result.is_none());
2197 }
2198
2199 #[test]
2200 fn runtime_name_resolution_policy_uses_semantic_resolver() {
2201 let _resolver_guard =
2202 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2203 (name == "resolved_target").then_some(45)
2204 })));
2205 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
2206 Arc::new(|function, args, requested_outputs| {
2207 assert_eq!(function, 45);
2208 assert_eq!(requested_outputs, 1);
2209 assert_eq!(args, &[Value::Num(4.0)]);
2210 Box::pin(async { Ok(Value::Num(11.0)) })
2211 }),
2212 ));
2213
2214 let request = crate::user_functions::CallableRequest::resolved(
2215 runmat_hir::CallableIdentity::DynamicName(runmat_hir::SymbolName(
2216 "resolved_target".to_string(),
2217 )),
2218 runmat_hir::CallableFallbackPolicy::RuntimeNameResolution,
2219 vec![Value::Num(4.0)],
2220 1,
2221 );
2222
2223 let result = block_on(crate::user_functions::try_call_semantic_descriptor(request))
2224 .expect("runtime resolution should attempt semantic resolver")
2225 .expect("semantic invoker should succeed");
2226 assert_eq!(result, Value::Num(11.0));
2227 }
2228
2229 #[test]
2230 fn object_dispatch_policy_does_not_use_semantic_resolver() {
2231 let _resolver_guard =
2232 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2233 (name == "resolved_target").then_some(45)
2234 })));
2235 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
2236 Arc::new(|function, args, requested_outputs| {
2237 assert_eq!(function, 45);
2238 assert_eq!(requested_outputs, 1);
2239 assert_eq!(args, &[Value::Num(4.0)]);
2240 Box::pin(async { Ok(Value::Num(11.0)) })
2241 }),
2242 ));
2243
2244 let request = crate::user_functions::CallableRequest::resolved(
2245 runmat_hir::CallableIdentity::DynamicName(runmat_hir::SymbolName(
2246 "resolved_target".to_string(),
2247 )),
2248 runmat_hir::CallableFallbackPolicy::ObjectDispatch,
2249 vec![Value::Num(4.0)],
2250 1,
2251 );
2252
2253 let result = block_on(crate::user_functions::try_call_semantic_descriptor(request));
2254 assert!(result.is_none());
2255 }
2256
2257 #[test]
2258 fn external_name_runtime_name_resolution_policy_does_not_use_semantic_resolver() {
2259 let _resolver_guard =
2260 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2261 (name == "resolved_target").then_some(45)
2262 })));
2263 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
2264 Arc::new(|function, args, requested_outputs| {
2265 assert_eq!(function, 45);
2266 assert_eq!(requested_outputs, 1);
2267 assert_eq!(args, &[Value::Num(4.0)]);
2268 Box::pin(async { Ok(Value::Num(11.0)) })
2269 }),
2270 ));
2271
2272 let request = crate::user_functions::CallableRequest::resolved(
2273 runmat_hir::CallableIdentity::ExternalName(runmat_hir::QualifiedName(vec![
2274 runmat_hir::SymbolName("resolved_target".to_string()),
2275 ])),
2276 runmat_hir::CallableFallbackPolicy::RuntimeNameResolution,
2277 vec![Value::Num(4.0)],
2278 1,
2279 );
2280
2281 let result = block_on(crate::user_functions::try_call_semantic_descriptor(request));
2282 assert!(result.is_none());
2283 }
2284
2285 #[test]
2286 fn external_boundary_policy_uses_semantic_resolver() {
2287 let _resolver_guard =
2288 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2289 (name == "pkg.resolved_target").then_some(45)
2290 })));
2291 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
2292 Arc::new(|function, args, requested_outputs| {
2293 assert_eq!(function, 45);
2294 assert_eq!(requested_outputs, 1);
2295 assert_eq!(args, &[Value::Num(4.0)]);
2296 Box::pin(async { Ok(Value::Num(11.0)) })
2297 }),
2298 ));
2299
2300 let request = crate::user_functions::CallableRequest::resolved(
2301 runmat_hir::CallableIdentity::ExternalName(runmat_hir::QualifiedName(vec![
2302 runmat_hir::SymbolName("pkg".to_string()),
2303 runmat_hir::SymbolName("resolved_target".to_string()),
2304 ])),
2305 runmat_hir::CallableFallbackPolicy::ExternalBoundary,
2306 vec![Value::Num(4.0)],
2307 1,
2308 );
2309
2310 let result = block_on(crate::user_functions::try_call_semantic_descriptor(request))
2311 .expect("external boundary policy should attempt semantic resolver")
2312 .expect("semantic invoker should succeed");
2313 assert_eq!(result, Value::Num(11.0));
2314 }
2315
2316 #[test]
2317 fn external_boundary_policy_malformed_external_identity_does_not_use_semantic_resolver() {
2318 let _resolver_guard =
2319 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2320 (name == "pkg..resolved_target").then_some(45)
2321 })));
2322 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
2323 Arc::new(|function, args, requested_outputs| {
2324 assert_eq!(function, 45);
2325 assert_eq!(requested_outputs, 1);
2326 assert_eq!(args, &[Value::Num(4.0)]);
2327 Box::pin(async { Ok(Value::Num(11.0)) })
2328 }),
2329 ));
2330
2331 let request = crate::user_functions::CallableRequest::resolved(
2332 runmat_hir::CallableIdentity::ExternalName(runmat_hir::QualifiedName(vec![
2333 runmat_hir::SymbolName("pkg..resolved_target".to_string()),
2334 ])),
2335 runmat_hir::CallableFallbackPolicy::ExternalBoundary,
2336 vec![Value::Num(4.0)],
2337 1,
2338 );
2339
2340 let result = block_on(crate::user_functions::try_call_semantic_descriptor(request));
2341 assert!(result.is_none());
2342 }
2343
2344 #[test]
2345 fn runtime_name_resolution_policy_uses_semantic_resolver_after_object_probe() {
2346 let _resolver_guard =
2347 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2348 (name == "resolved_target").then_some(45)
2349 })));
2350 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
2351 Arc::new(|function, args, requested_outputs| {
2352 assert_eq!(function, 45);
2353 assert_eq!(requested_outputs, 1);
2354 assert_eq!(args, &[Value::Num(4.0)]);
2355 Box::pin(async { Ok(Value::Num(11.0)) })
2356 }),
2357 ));
2358
2359 let request = crate::user_functions::CallableRequest::resolved(
2360 runmat_hir::CallableIdentity::DynamicName(runmat_hir::SymbolName(
2361 "resolved_target".to_string(),
2362 )),
2363 runmat_hir::CallableFallbackPolicy::RuntimeNameResolution,
2364 vec![Value::Num(4.0)],
2365 1,
2366 );
2367
2368 let result = block_on(crate::user_functions::try_call_semantic_descriptor(request))
2369 .expect("post-object-probe runtime-name policy should attempt semantic resolver")
2370 .expect("semantic invoker should succeed");
2371 assert_eq!(result, Value::Num(11.0));
2372 }
2373
2374 #[test]
2375 fn method_identity_runtime_name_resolution_policy_uses_semantic_resolver() {
2376 let _resolver_guard =
2377 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2378 (name == "resolved_target").then_some(45)
2379 })));
2380 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
2381 Arc::new(|function, args, requested_outputs| {
2382 assert_eq!(function, 45);
2383 assert_eq!(requested_outputs, 1);
2384 assert_eq!(args, &[Value::Num(4.0)]);
2385 Box::pin(async { Ok(Value::Num(11.0)) })
2386 }),
2387 ));
2388
2389 let request = crate::user_functions::CallableRequest::resolved(
2390 runmat_hir::CallableIdentity::Method(runmat_hir::MethodId(
2391 "resolved_target".to_string(),
2392 )),
2393 runmat_hir::CallableFallbackPolicy::RuntimeNameResolution,
2394 vec![Value::Num(4.0)],
2395 1,
2396 );
2397
2398 let result = block_on(crate::user_functions::try_call_semantic_descriptor(request))
2399 .expect("method runtime-name policy should attempt semantic resolver")
2400 .expect("semantic invoker should succeed");
2401 assert_eq!(result, Value::Num(11.0));
2402 }
2403
2404 #[test]
2405 fn imported_identity_runtime_name_resolution_policy_uses_semantic_resolver() {
2406 let _resolver_guard =
2407 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2408 (name == "Point.origin").then_some(45)
2409 })));
2410 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
2411 Arc::new(|function, args, requested_outputs| {
2412 assert_eq!(function, 45);
2413 assert_eq!(requested_outputs, 1);
2414 assert_eq!(args, &[Value::Num(4.0)]);
2415 Box::pin(async { Ok(Value::Num(11.0)) })
2416 }),
2417 ));
2418
2419 let request = crate::user_functions::CallableRequest::resolved(
2420 runmat_hir::CallableIdentity::Imported(runmat_hir::DefPath {
2421 package: runmat_hir::PackageName("Point".to_string()),
2422 module: runmat_hir::QualifiedName(vec![
2423 runmat_hir::SymbolName("Point".to_string()),
2424 runmat_hir::SymbolName("origin".to_string()),
2425 ]),
2426 item: vec![runmat_hir::DefPathSegment::Function(
2427 runmat_hir::SymbolName("origin".to_string()),
2428 )],
2429 }),
2430 runmat_hir::CallableFallbackPolicy::RuntimeNameResolution,
2431 vec![Value::Num(4.0)],
2432 1,
2433 );
2434
2435 let result = block_on(crate::user_functions::try_call_semantic_descriptor(request))
2436 .expect("imported runtime-name policy should attempt semantic resolver")
2437 .expect("semantic invoker should succeed");
2438 assert_eq!(result, Value::Num(11.0));
2439 }
2440
2441 #[test]
2442 fn imported_identity_runtime_name_resolution_policy_rejects_malformed_path_without_semantic_probe(
2443 ) {
2444 let resolver_calls = Arc::new(AtomicUsize::new(0));
2445 let resolver_calls_for_closure = Arc::clone(&resolver_calls);
2446 let _resolver_guard =
2447 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(move |_| {
2448 resolver_calls_for_closure.fetch_add(1, Ordering::Relaxed);
2449 Some(45)
2450 })));
2451 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
2452 Arc::new(|function, args, requested_outputs| {
2453 assert_eq!(function, 45);
2454 assert_eq!(requested_outputs, 1);
2455 assert_eq!(args, &[Value::Num(4.0)]);
2456 Box::pin(async { Ok(Value::Num(11.0)) })
2457 }),
2458 ));
2459
2460 let request = crate::user_functions::CallableRequest::resolved(
2461 runmat_hir::CallableIdentity::Imported(runmat_hir::DefPath {
2462 package: runmat_hir::PackageName("Point".to_string()),
2463 module: runmat_hir::QualifiedName(vec![
2464 runmat_hir::SymbolName("Point".to_string()),
2465 runmat_hir::SymbolName("origin".to_string()),
2466 ]),
2467 item: vec![runmat_hir::DefPathSegment::Function(
2468 runmat_hir::SymbolName("other".to_string()),
2469 )],
2470 }),
2471 runmat_hir::CallableFallbackPolicy::RuntimeNameResolution,
2472 vec![Value::Num(4.0)],
2473 1,
2474 );
2475
2476 let result = block_on(crate::user_functions::try_call_semantic_descriptor(request));
2477 assert!(
2478 result.is_none(),
2479 "mismatched imported identity should not attempt semantic resolver"
2480 );
2481 assert_eq!(
2482 resolver_calls.load(Ordering::Relaxed),
2483 0,
2484 "malformed imported identity should be rejected before resolver probe"
2485 );
2486 }
2487
2488 #[test]
2489 fn call_method_fallback_preserves_requested_outputs() {
2490 let _output_guard = crate::output_count::push_output_count(Some(2));
2491 let base = Value::Object(runmat_builtins::ObjectInstance::new(
2492 "NoSuchMethodClass".to_string(),
2493 ));
2494 let result = block_on(
2495 crate::builtins::introspection::call_method::dispatch_call_method(
2496 base.clone(),
2497 "deal".to_string(),
2498 vec![Value::Num(9.0), Value::Num(10.0)],
2499 ),
2500 )
2501 .expect("call_method fallback should succeed");
2502 match result {
2503 Value::OutputList(values) => {
2504 assert!(values.len() >= 2);
2505 assert_eq!(values[0], base);
2506 assert_eq!(values[1], Value::Num(9.0));
2507 }
2508 other => {
2509 panic!("expected output list from multi-output call_method fallback, got {other:?}")
2510 }
2511 }
2512 }
2513
2514 #[test]
2515 fn call_method_trims_method_name_for_resolution() {
2516 let _output_guard = crate::output_count::push_output_count(Some(2));
2517 let base = Value::Object(runmat_builtins::ObjectInstance::new(
2518 "NoSuchMethodClass".to_string(),
2519 ));
2520 let result = block_on(
2521 crate::builtins::introspection::call_method::dispatch_call_method(
2522 base.clone(),
2523 " deal ".to_string(),
2524 vec![Value::Num(9.0), Value::Num(10.0)],
2525 ),
2526 )
2527 .expect("call_method fallback should succeed after method-name trimming");
2528 match result {
2529 Value::OutputList(values) => {
2530 assert!(values.len() >= 2);
2531 assert_eq!(values[0], base);
2532 assert_eq!(values[1], Value::Num(9.0));
2533 }
2534 other => {
2535 panic!("expected output list from trimmed-name call_method fallback, got {other:?}")
2536 }
2537 }
2538 }
2539
2540 #[test]
2541 fn feval_call_method_closure_fast_path_preserves_requested_outputs() {
2542 let _output_guard = crate::output_count::push_output_count(Some(2));
2543 let base = Value::Object(runmat_builtins::ObjectInstance::new(
2544 "NoSuchMethodClass".to_string(),
2545 ));
2546 let closure = Value::Closure(runmat_builtins::Closure {
2547 function_name: CALL_METHOD_BUILTIN_NAME.to_string(),
2548 bound_function: None,
2549 captures: vec![
2550 base.clone(),
2551 Value::String("deal".to_string()),
2552 Value::Num(9.0),
2553 ],
2554 });
2555 let result = block_on(feval_builtin(closure, vec![Value::Num(10.0)]))
2556 .expect("feval call_method closure should succeed");
2557 match result {
2558 Value::OutputList(values) => {
2559 assert!(values.len() >= 2);
2560 assert_eq!(values[0], base);
2561 assert_eq!(values[1], Value::Num(9.0));
2562 }
2563 other => {
2564 panic!(
2565 "expected output list from feval call_method closure fast path, got {other:?}"
2566 )
2567 }
2568 }
2569 }
2570
2571 #[test]
2572 fn feval_call_method_closure_fast_path_trims_method_name_for_resolution() {
2573 let _output_guard = crate::output_count::push_output_count(Some(2));
2574 let base = Value::Object(runmat_builtins::ObjectInstance::new(
2575 "NoSuchMethodClass".to_string(),
2576 ));
2577 let closure = Value::Closure(runmat_builtins::Closure {
2578 function_name: CALL_METHOD_BUILTIN_NAME.to_string(),
2579 bound_function: None,
2580 captures: vec![
2581 base.clone(),
2582 Value::String(" deal ".to_string()),
2583 Value::Num(9.0),
2584 ],
2585 });
2586 let result = block_on(feval_builtin(closure, vec![Value::Num(10.0)]))
2587 .expect("feval call_method closure should succeed after method-name trimming");
2588 match result {
2589 Value::OutputList(values) => {
2590 assert!(values.len() >= 2);
2591 assert_eq!(values[0], base);
2592 assert_eq!(values[1], Value::Num(9.0));
2593 }
2594 other => {
2595 panic!(
2596 "expected output list from trimmed call_method closure fast path, got {other:?}"
2597 )
2598 }
2599 }
2600 }
2601
2602 #[test]
2603 fn feval_call_method_closure_rejects_nontext_method_capture_with_identifier() {
2604 let closure = Value::Closure(runmat_builtins::Closure {
2605 function_name: CALL_METHOD_BUILTIN_NAME.to_string(),
2606 bound_function: None,
2607 captures: vec![
2608 Value::Object(runmat_builtins::ObjectInstance::new("Point".to_string())),
2609 Value::Num(1.0),
2610 ],
2611 });
2612 let err = block_on(feval_builtin(closure, Vec::new()))
2613 .expect_err("feval call_method closure should reject nontext method capture");
2614 assert_eq!(err.identifier(), Some("RunMat:CallMethodNameInvalid"));
2615 }
2616
2617 #[test]
2618 fn call_method_rejects_non_object_receiver_with_identifier() {
2619 let err = block_on(
2620 crate::builtins::introspection::call_method::dispatch_call_method(
2621 Value::Num(1.0),
2622 "origin".to_string(),
2623 Vec::new(),
2624 ),
2625 )
2626 .expect_err("non-object receiver should fail");
2627 assert_eq!(err.identifier(), Some("RunMat:InvalidObjectDispatch"));
2628 }
2629
2630 #[test]
2631 fn call_method_rejects_empty_method_name_with_identifier() {
2632 let err = block_on(
2633 crate::builtins::introspection::call_method::dispatch_call_method(
2634 Value::Object(runmat_builtins::ObjectInstance::new("Point".to_string())),
2635 " ".to_string(),
2636 Vec::new(),
2637 ),
2638 )
2639 .expect_err("empty method name should fail");
2640 assert_eq!(err.identifier(), Some("RunMat:CallMethodNameInvalid"));
2641 }
2642
2643 #[test]
2644 fn subsref_rejects_non_object_receiver_with_identifier() {
2645 let err = block_on(
2646 crate::builtins::introspection::object_indexing::dispatch_subsref(
2647 Value::Num(1.0),
2648 OBJECT_INDEX_PAREN.to_string(),
2649 Value::Num(2.0),
2650 ),
2651 )
2652 .expect_err("non-object subsref receiver should fail");
2653 assert_eq!(err.identifier(), Some("RunMat:InvalidObjectDispatch"));
2654 }
2655
2656 #[test]
2657 fn subsasgn_rejects_non_object_receiver_with_identifier() {
2658 let err = block_on(
2659 crate::builtins::introspection::object_indexing::dispatch_subsasgn(
2660 Value::Num(1.0),
2661 OBJECT_INDEX_PAREN.to_string(),
2662 Value::Num(2.0),
2663 Value::Num(3.0),
2664 ),
2665 )
2666 .expect_err("non-object subsasgn receiver should fail");
2667 assert_eq!(err.identifier(), Some("RunMat:InvalidObjectDispatch"));
2668 }
2669
2670 #[test]
2671 fn subsref_missing_protocol_errors_with_identifier() {
2672 let err = block_on(
2673 crate::builtins::introspection::object_indexing::dispatch_subsref(
2674 Value::Object(runmat_builtins::ObjectInstance::new(
2675 "NoSubsrefProtocolClass".to_string(),
2676 )),
2677 OBJECT_INDEX_PAREN.to_string(),
2678 Value::Cell(runmat_builtins::CellArray::new(vec![Value::Num(1.0)], 1, 1).unwrap()),
2679 ),
2680 )
2681 .expect_err("missing subsref protocol should fail");
2682 assert_eq!(err.identifier(), Some("RunMat:MissingSubsref"));
2683 }
2684
2685 #[test]
2686 fn subsasgn_missing_protocol_errors_with_identifier() {
2687 let err = block_on(
2688 crate::builtins::introspection::object_indexing::dispatch_subsasgn(
2689 Value::Object(runmat_builtins::ObjectInstance::new(
2690 "NoSubsasgnProtocolClass".to_string(),
2691 )),
2692 OBJECT_INDEX_PAREN.to_string(),
2693 Value::Cell(runmat_builtins::CellArray::new(vec![Value::Num(1.0)], 1, 1).unwrap()),
2694 Value::Num(3.0),
2695 ),
2696 )
2697 .expect_err("missing subsasgn protocol should fail");
2698 assert_eq!(err.identifier(), Some("RunMat:MissingSubsasgn"));
2699 }
2700
2701 #[test]
2702 fn get_p_rejects_non_object_receiver_with_identifier() {
2703 let err = block_on(get_p_builtin(Value::Num(1.0)))
2704 .expect_err("get.p should reject non-object receiver");
2705 assert_eq!(err.identifier(), Some("RunMat:GetPReceiverInvalid"));
2706 }
2707
2708 #[test]
2709 fn set_p_rejects_non_object_receiver_with_identifier() {
2710 let err = block_on(set_p_builtin(Value::Num(1.0), Value::Num(2.0)))
2711 .expect_err("set.p should reject non-object receiver");
2712 assert_eq!(err.identifier(), Some("RunMat:SetPReceiverInvalid"));
2713 }
2714
2715 #[test]
2716 fn point_move_rejects_non_object_receiver_with_identifier() {
2717 let err = block_on(point_move_method(Value::Num(1.0), 2.0, 3.0))
2718 .expect_err("Point.move should reject non-object receiver");
2719 assert_eq!(err.identifier(), Some("RunMat:PointMoveReceiverInvalid"));
2720 }
2721
2722 #[test]
2723 fn circle_area_rejects_non_object_receiver_with_identifier() {
2724 let err = block_on(circle_area_method(Value::Num(1.0)))
2725 .expect_err("Circle.area should reject non-object receiver");
2726 assert_eq!(err.identifier(), Some("RunMat:CircleAreaReceiverInvalid"));
2727 }
2728
2729 #[test]
2730 fn overidx_plus_rejects_non_object_receiver_with_identifier() {
2731 let err = block_on(overidx_plus(Value::Num(1.0), Value::Num(2.0)))
2732 .expect_err("OverIdx.plus should reject non-object receiver");
2733 assert_eq!(err.identifier(), Some("RunMat:OverIdxReceiverInvalid"));
2734 }
2735
2736 #[test]
2737 fn overidx_subsref_unsupported_payload_errors_with_identifier() {
2738 let err = block_on(overidx_subsref(
2739 Value::Object(runmat_builtins::ObjectInstance::new("OverIdx".to_string())),
2740 OBJECT_INDEX_PAREN.to_string(),
2741 Value::Num(1.0),
2742 ))
2743 .expect_err("OverIdx.subsref unsupported payload should fail");
2744 assert_eq!(
2745 err.identifier(),
2746 Some("RunMat:OverIdxSubsrefPayloadUnsupported")
2747 );
2748 }
2749
2750 #[test]
2751 fn overidx_subsasgn_unsupported_payload_errors_with_identifier() {
2752 let err = block_on(overidx_subsasgn(
2753 Value::Object(runmat_builtins::ObjectInstance::new("OverIdx".to_string())),
2754 OBJECT_INDEX_PAREN.to_string(),
2755 Value::Num(1.0),
2756 Value::Num(2.0),
2757 ))
2758 .expect_err("OverIdx.subsasgn unsupported payload should fail");
2759 assert_eq!(
2760 err.identifier(),
2761 Some("RunMat:OverIdxSubsasgnPayloadUnsupported")
2762 );
2763 }
2764
2765 #[test]
2766 fn feval_object_receiver_routes_to_subsref_identifier() {
2767 let err = block_on(feval_builtin(
2768 Value::Object(runmat_builtins::ObjectInstance::new(
2769 "NoSubsrefProtocolClass".to_string(),
2770 )),
2771 vec![Value::Num(1.0)],
2772 ))
2773 .expect_err("feval(object, ...) should route through subsref dispatch");
2774 assert_eq!(err.identifier(), Some("RunMat:MissingSubsref"));
2775 }
2776
2777 #[test]
2778 fn feval_unsupported_callable_value_errors_with_identifier() {
2779 let err = block_on(feval_builtin(Value::Num(1.0), vec![Value::Num(2.0)]))
2780 .expect_err("numeric callable value should fail");
2781 assert_eq!(
2782 err.identifier(),
2783 Some("RunMat:FevalFunctionValueUnsupported")
2784 );
2785 }
2786
2787 #[test]
2788 fn shape_checked_cell_builder_maps_shape_identifier() {
2789 let err = super::build_shape_checked_cell(vec![Value::Num(1.0)], 2, 2, "test")
2790 .expect_err("expected shape mismatch");
2791 assert_eq!(err.identifier(), Some("RunMat:ShapeMismatch"));
2792 }
2793
2794 #[test]
2795 fn feval_accepts_scalar_string_array_handle() {
2796 let handle =
2797 runmat_builtins::StringArray::new(vec!["@sin".to_string()], vec![1, 1]).expect("sa");
2798 let result = block_on(feval_builtin(
2799 Value::StringArray(handle),
2800 vec![Value::Num(0.0)],
2801 ))
2802 .expect("string-array handle feval should succeed");
2803 assert_eq!(result, Value::Num(0.0));
2804 }
2805
2806 #[test]
2807 fn feval_rejects_nonscalar_string_array_handle_with_identifier() {
2808 let handle = runmat_builtins::StringArray::new(
2809 vec!["@sin".to_string(), "@cos".to_string()],
2810 vec![1, 2],
2811 )
2812 .expect("sa");
2813 let err = block_on(feval_builtin(
2814 Value::StringArray(handle),
2815 vec![Value::Num(0.0)],
2816 ))
2817 .expect_err("nonscalar string-array handle should fail");
2818 assert_eq!(err.identifier(), Some("RunMat:FevalHandleShapeInvalid"));
2819 }
2820
2821 #[test]
2822 fn call_feval_async_with_outputs_preserves_unresolved_identifier() {
2823 let err = block_on(super::call_feval_async_with_outputs(
2824 Value::ExternalFunctionHandle("missing.external".to_string()),
2825 &[Value::Num(3.0)],
2826 1,
2827 ))
2828 .expect_err("unresolved external handle should fail");
2829 assert_eq!(err.identifier(), Some("RunMat:UndefinedFunction"));
2830 }
2831
2832 #[test]
2833 fn addlistener_rejects_non_object_target_with_identifier() {
2834 let err = block_on(addlistener_builtin(
2835 Value::Num(1.0),
2836 "Changed".to_string(),
2837 Value::FunctionHandle("sin".to_string()),
2838 ))
2839 .expect_err("addlistener should reject non-object target");
2840 assert_eq!(err.identifier(), Some("RunMat:AddListenerTargetInvalid"));
2841 }
2842
2843 #[test]
2844 fn notify_rejects_non_object_target_with_identifier() {
2845 let err = block_on(notify_builtin(
2846 Value::Num(1.0),
2847 "Changed".to_string(),
2848 Vec::new(),
2849 ))
2850 .expect_err("notify should reject non-object target");
2851 assert_eq!(err.identifier(), Some("RunMat:NotifyTargetInvalid"));
2852 }
2853
2854 #[test]
2855 fn addlistener_function_handle_prefers_semantic_identity_when_resolved() {
2856 let _resolver_guard =
2857 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2858 (name == "event_callback").then_some(61)
2859 })));
2860 let target =
2861 block_on(new_handle_object_builtin("EventTarget".to_string())).expect("handle target");
2862 let listener = block_on(addlistener_builtin(
2863 target,
2864 "Changed".to_string(),
2865 Value::FunctionHandle("event_callback".to_string()),
2866 ))
2867 .expect("listener registered");
2868 let Value::Listener(listener) = listener else {
2869 panic!("expected listener value");
2870 };
2871 assert!(matches!(
2872 &*listener.callback,
2873 Value::BoundFunctionHandle { name, function }
2874 if name == "event_callback" && *function == 61
2875 ));
2876 }
2877
2878 #[test]
2879 fn addlistener_external_function_handle_prefers_semantic_identity_when_resolved() {
2880 let _resolver_guard =
2881 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2882 (name == "pkg.event_callback").then_some(62)
2883 })));
2884 let target =
2885 block_on(new_handle_object_builtin("EventTarget".to_string())).expect("handle target");
2886 let listener = block_on(addlistener_builtin(
2887 target,
2888 "Changed".to_string(),
2889 Value::ExternalFunctionHandle("pkg.event_callback".to_string()),
2890 ))
2891 .expect("listener registered");
2892 let Value::Listener(listener) = listener else {
2893 panic!("expected listener value");
2894 };
2895 assert!(matches!(
2896 &*listener.callback,
2897 Value::BoundFunctionHandle { name, function }
2898 if name == "pkg.event_callback" && *function == 62
2899 ));
2900 }
2901
2902 #[test]
2903 fn addlistener_string_handle_prefers_semantic_identity_when_resolved() {
2904 let _resolver_guard =
2905 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2906 (name == "event_callback").then_some(63)
2907 })));
2908 let target =
2909 block_on(new_handle_object_builtin("EventTarget".to_string())).expect("handle target");
2910 let listener = block_on(addlistener_builtin(
2911 target,
2912 "Changed".to_string(),
2913 Value::String("@event_callback".to_string()),
2914 ))
2915 .expect("listener registered");
2916 let Value::Listener(listener) = listener else {
2917 panic!("expected listener value");
2918 };
2919 assert!(matches!(
2920 &*listener.callback,
2921 Value::BoundFunctionHandle { name, function }
2922 if name == "event_callback" && *function == 63
2923 ));
2924 }
2925
2926 #[test]
2927 fn addlistener_char_handle_prefers_semantic_identity_when_resolved() {
2928 let _resolver_guard =
2929 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2930 (name == "event_callback").then_some(64)
2931 })));
2932 let target =
2933 block_on(new_handle_object_builtin("EventTarget".to_string())).expect("handle target");
2934 let listener = block_on(addlistener_builtin(
2935 target,
2936 "Changed".to_string(),
2937 Value::CharArray(runmat_builtins::CharArray::new_row("@event_callback")),
2938 ))
2939 .expect("listener registered");
2940 let Value::Listener(listener) = listener else {
2941 panic!("expected listener value");
2942 };
2943 assert!(matches!(
2944 &*listener.callback,
2945 Value::BoundFunctionHandle { name, function }
2946 if name == "event_callback" && *function == 64
2947 ));
2948 }
2949
2950 #[test]
2951 fn addlistener_string_array_handle_prefers_semantic_identity_when_resolved() {
2952 let _resolver_guard =
2953 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2954 (name == "event_callback").then_some(66)
2955 })));
2956 let target =
2957 block_on(new_handle_object_builtin("EventTarget".to_string())).expect("handle target");
2958 let callback =
2959 runmat_builtins::StringArray::new(vec!["@event_callback".to_string()], vec![1, 1])
2960 .expect("string array");
2961 let listener = block_on(addlistener_builtin(
2962 target,
2963 "Changed".to_string(),
2964 Value::StringArray(callback),
2965 ))
2966 .expect("listener registered");
2967 let Value::Listener(listener) = listener else {
2968 panic!("expected listener value");
2969 };
2970 assert!(matches!(
2971 &*listener.callback,
2972 Value::BoundFunctionHandle { name, function }
2973 if name == "event_callback" && *function == 66
2974 ));
2975 }
2976
2977 #[test]
2978 fn addlistener_closure_prefers_embedded_semantic_identity_when_resolved() {
2979 let _resolver_guard =
2980 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
2981 (name == "event_callback").then_some(65)
2982 })));
2983 let target =
2984 block_on(new_handle_object_builtin("EventTarget".to_string())).expect("handle target");
2985 let callback = Value::Closure(runmat_builtins::Closure {
2986 function_name: "event_callback".to_string(),
2987 bound_function: None,
2988 captures: vec![Value::Num(9.0)],
2989 });
2990 let listener = block_on(addlistener_builtin(target, "Changed".to_string(), callback))
2991 .expect("listener registered");
2992 let Value::Listener(listener) = listener else {
2993 panic!("expected listener value");
2994 };
2995 assert!(matches!(
2996 &*listener.callback,
2997 Value::Closure(runmat_builtins::Closure {
2998 function_name,
2999 bound_function: Some(65),
3000 captures,
3001 }) if function_name == "event_callback" && captures == &vec![Value::Num(9.0)]
3002 ));
3003 }
3004
3005 #[test]
3006 fn notify_semantic_function_handle_uses_semantic_identity() {
3007 let calls = Arc::new(AtomicUsize::new(0));
3008 let seen_calls = Arc::clone(&calls);
3009 let _guard = crate::user_functions::install_semantic_function_invoker(Some(Arc::new(
3010 move |function, args, requested_outputs| {
3011 assert_eq!(function, 44);
3012 assert_eq!(requested_outputs, 0);
3013 assert_eq!(args.len(), 1);
3014 assert!(matches!(args[0], Value::HandleObject(_)));
3015 seen_calls.fetch_add(1, Ordering::SeqCst);
3016 Box::pin(async { Ok(Value::Num(0.0)) })
3017 },
3018 )));
3019 let target =
3020 block_on(new_handle_object_builtin("EventTarget".to_string())).expect("handle target");
3021 let callback = Value::BoundFunctionHandle {
3022 name: "event_callback".to_string(),
3023 function: 44,
3024 };
3025
3026 block_on(addlistener_builtin(
3027 target.clone(),
3028 "Changed".to_string(),
3029 callback,
3030 ))
3031 .expect("listener registered");
3032 block_on(notify_builtin(target, "Changed".to_string(), Vec::new()))
3033 .expect("notify succeeds");
3034 assert_eq!(calls.load(Ordering::SeqCst), 1);
3035 }
3036
3037 #[test]
3038 fn notify_char_handle_callback_surfaces_unresolved_identifier() {
3039 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
3040 let target =
3041 block_on(new_handle_object_builtin("EventTarget".to_string())).expect("handle target");
3042 block_on(addlistener_builtin(
3043 target.clone(),
3044 "Changed".to_string(),
3045 Value::CharArray(runmat_builtins::CharArray::new_row(
3046 "@definitely_missing_callback",
3047 )),
3048 ))
3049 .expect("listener registered");
3050 let err = block_on(notify_builtin(target, "Changed".to_string(), Vec::new()))
3051 .expect_err("unresolved char callback should fail");
3052 assert_eq!(err.identifier(), Some("RunMat:UndefinedFunction"));
3053 }
3054
3055 #[test]
3056 fn notify_string_array_handle_callback_surfaces_unresolved_identifier() {
3057 let _resolver_guard = crate::user_functions::install_semantic_function_resolver(None);
3058 let target =
3059 block_on(new_handle_object_builtin("EventTarget".to_string())).expect("handle target");
3060 let callback = runmat_builtins::StringArray::new(
3061 vec!["@definitely_missing_callback".to_string()],
3062 vec![1, 1],
3063 )
3064 .expect("string array");
3065 block_on(addlistener_builtin(
3066 target.clone(),
3067 "Changed".to_string(),
3068 Value::StringArray(callback),
3069 ))
3070 .expect("listener registered");
3071 let err = block_on(notify_builtin(target, "Changed".to_string(), Vec::new()))
3072 .expect_err("unresolved string-array callback should fail");
3073 assert_eq!(err.identifier(), Some("RunMat:UndefinedFunction"));
3074 }
3075
3076 #[test]
3077 fn feval_semantic_handle_honors_zero_requested_outputs() {
3078 let _guard = crate::user_functions::install_semantic_function_invoker(Some(Arc::new(
3079 |function, args, requested_outputs| {
3080 assert_eq!(function, 46);
3081 assert_eq!(requested_outputs, 0);
3082 assert_eq!(args, &[Value::Num(5.0)]);
3083 Box::pin(async { Ok(Value::OutputList(Vec::new())) })
3084 },
3085 )));
3086 let _output_guard = crate::output_count::push_output_count(Some(0));
3087 let handle = Value::BoundFunctionHandle {
3088 name: "function_target".to_string(),
3089 function: 46,
3090 };
3091
3092 let result = block_on(feval_builtin(handle, vec![Value::Num(5.0)]))
3093 .expect("semantic function handle feval succeeds");
3094 assert_eq!(result, Value::OutputList(Vec::new()));
3095 }
3096
3097 #[test]
3098 fn feval_semantic_handle_honors_multi_requested_outputs() {
3099 let _guard = crate::user_functions::install_semantic_function_invoker(Some(Arc::new(
3100 |function, args, requested_outputs| {
3101 assert_eq!(function, 47);
3102 assert_eq!(requested_outputs, 2);
3103 assert_eq!(args, &[Value::Num(6.0)]);
3104 Box::pin(async { Ok(Value::OutputList(vec![Value::Num(1.0), Value::Num(2.0)])) })
3105 },
3106 )));
3107 let _output_guard = crate::output_count::push_output_count(Some(2));
3108 let handle = Value::BoundFunctionHandle {
3109 name: "function_target".to_string(),
3110 function: 47,
3111 };
3112
3113 let result = block_on(feval_builtin(handle, vec![Value::Num(6.0)]))
3114 .expect("semantic function handle feval succeeds");
3115 assert_eq!(
3116 result,
3117 Value::OutputList(vec![Value::Num(1.0), Value::Num(2.0)])
3118 );
3119 }
3120
3121 #[test]
3122 fn feval_semantic_closure_errors_when_semantic_invoker_unavailable() {
3123 let _guard = crate::user_functions::install_semantic_function_invoker(None);
3124 let closure = Value::Closure(runmat_builtins::Closure {
3125 function_name: "function_target".to_string(),
3126 bound_function: Some(9044),
3127 captures: vec![Value::Num(1.0)],
3128 });
3129
3130 let err = block_on(feval_builtin(closure, vec![Value::Num(2.0)]))
3131 .expect_err("semantic closure should not fall back to name-based dispatch");
3132 assert_eq!(err.identifier(), Some("RunMat:SemanticFunctionUnavailable"));
3133 assert!(
3134 err.message()
3135 .contains("semantic closure 'function_target' (9044) is unavailable"),
3136 "unexpected error: {err:?}"
3137 );
3138 }
3139}