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