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