Skip to main content

runmat_runtime/
lib.rs

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
38/// Standard result type for runtime builtins.
39pub 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// Link to Apple's Accelerate framework on macOS
246#[cfg(all(feature = "blas-lapack", target_os = "macos"))]
247#[link(name = "Accelerate", kind = "framework")]
248extern "C" {}
249
250// Ensure OpenBLAS is linked on non-macOS platforms when BLAS/LAPACK is enabled
251#[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
272// Pruned legacy re-exports; prefer builtins::* and explicit shims only
273// Transitional root-level shims for widely used helpers
274pub 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// Explicitly re-export for external users of the VM that build matrices from values
281// (kept above)
282// Note: constants and mathematics modules only contain #[runtime_builtin] functions
283// and don't export public items, so they don't need to be re-exported
284
285#[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            // Convert each row to a string; treat as column vector
326            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    // Return cell row vector of inputs for expansion
379    let cols = rest.len();
380    make_cell(rest, 1, cols).map_err(Into::into)
381}
382
383// Object/handle utilities used by interpreter lowering for OOP/func handles
384
385pub(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
395// -------- Handle classes & events --------
396
397pub(crate) async fn new_handle_object_builtin(class_name: String) -> crate::BuiltinResult<Value> {
398    // Create an underlying object instance and wrap it in a handle
399    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        // Call callback via feval-like protocol.
600        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
625// Test-oriented dependent property handlers (global). If a class defines a Dependent
626// property named 'p', the VM will try to call get.p / set.p. We provide generic
627// implementations that read/write a conventional backing field 'p_backing'.
628pub(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        // Collect class hierarchy from root to leaf for default initialization
676        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        // Walk up to root
680        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        // Reverse to root-first
704        chain.reverse();
705        let mut obj = runmat_builtins::ObjectInstance::new(def.name.clone());
706        // Apply defaults from root to leaf (leaf overrides effectively by later assignment)
707        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
860// handle-object builtins removed for now
861
862pub(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    // Namespaced class example: pkg.PointNS with same shape as Point
950    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    // Inheritance: Shape (base) and Circle (derived)
984    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    // Constructor demo class: Ctor with static constructor method Ctor
1039    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    // Overloaded indexing demo class: OverIdx with subsref/subsasgn
1061    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    // Class without indexing protocol methods, used by negative subsref/subsasgn contracts.
1112    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
1126// Example method implementation: Point.move(obj, dx, dy) -> updated obj
1127const 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        // Function handle strings like "@sin"
1201        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        // Also accept character row vector handles like '@max'
1213        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}