Skip to main content

runmat_runtime/object/
dispatch.rs

1use crate::call::closures::{
2    caller_class_for_function, method_access_permitted, resolve_method_semantic_function_id,
3};
4use crate::call::descriptor::{
5    execute_callable_descriptor, try_execute_callable_descriptor, CallableCallKind,
6    CallableDescriptor,
7};
8use crate::call::identity::external_qualified_identity;
9use crate::object::indexing::{
10    build_matlab_substruct_arg, class_name_from_base, ObjectIndexDescriptor, ObjectIndexOp,
11};
12use crate::runtime_error::semantic_error;
13use crate::RuntimeError;
14use runmat_types::{CallableFallbackPolicy, CallableIdentity, MethodId, QualifiedName, SymbolName};
15use runmat_value::Value;
16
17fn caller_has_internal_class_access(caller_function_name: Option<&str>, class_name: &str) -> bool {
18    caller_class_for_function(caller_function_name).is_some_and(|caller_class| {
19        crate::class_registry::is_class_or_subclass(&caller_class, class_name)
20            || crate::class_registry::is_class_or_subclass(class_name, &caller_class)
21    })
22}
23
24fn method_member_name(identity: &CallableIdentity) -> Option<String> {
25    match identity {
26        CallableIdentity::DynamicName(runmat_types::SymbolName(name)) => {
27            let trimmed = name.trim();
28            (!trimmed.is_empty()).then_some(trimmed.to_string())
29        }
30        CallableIdentity::Method(runmat_types::MethodId(name)) => {
31            let trimmed = name.trim();
32            (!trimmed.is_empty()).then_some(trimmed.to_string())
33        }
34        CallableIdentity::ExternalName(runmat_types::QualifiedName(segments))
35            if segments.len() == 1 && !segments[0].0.trim().is_empty() =>
36        {
37            Some(segments[0].0.trim().to_string())
38        }
39        _ => None,
40    }
41}
42
43fn runtime_named_identity(name: &str) -> (CallableIdentity, CallableFallbackPolicy) {
44    if let Some(function) = crate::user_functions::resolve_semantic_function_by_name(name.trim()) {
45        return (
46            CallableIdentity::BoundFunction(runmat_types::FunctionId(function)),
47            CallableFallbackPolicy::None,
48        );
49    }
50    let segments: Vec<&str> = name.split('.').collect();
51    if segments.len() > 1 && segments.iter().all(|segment| !segment.trim().is_empty()) {
52        let qualified = QualifiedName(
53            segments
54                .into_iter()
55                .map(|segment| SymbolName(segment.trim().to_string()))
56                .collect(),
57        );
58        (
59            CallableIdentity::ExternalName(qualified),
60            CallableFallbackPolicy::ExternalBoundary,
61        )
62    } else {
63        (
64            CallableIdentity::DynamicName(SymbolName(name.trim().to_string())),
65            CallableFallbackPolicy::RuntimeNameResolution,
66        )
67    }
68}
69
70fn method_function_identity(
71    owner: &str,
72    method_name: &str,
73    function_name: &str,
74) -> (CallableIdentity, CallableFallbackPolicy) {
75    let trimmed = function_name.trim();
76    if let Some(function) = resolve_method_semantic_function_id(owner, method_name, trimmed) {
77        return (
78            CallableIdentity::BoundFunction(runmat_types::FunctionId(function)),
79            CallableFallbackPolicy::None,
80        );
81    }
82    if !trimmed.is_empty() && runmat_builtins::builtin_name_is_known(trimmed) {
83        return runtime_named_identity(trimmed);
84    }
85    if trimmed.is_empty() {
86        return (
87            external_qualified_identity(owner, method_name),
88            CallableFallbackPolicy::ExternalBoundary,
89        );
90    }
91    if trimmed.contains('.') {
92        return runtime_named_identity(trimmed);
93    }
94    (
95        external_qualified_identity(owner, trimmed),
96        CallableFallbackPolicy::ExternalBoundary,
97    )
98}
99
100fn is_operator_overload_name(name: &str) -> bool {
101    matches!(
102        name,
103        "plus"
104            | "minus"
105            | "times"
106            | "mtimes"
107            | "rdivide"
108            | "mrdivide"
109            | "ldivide"
110            | "mldivide"
111            | "power"
112            | "mpower"
113            | "uminus"
114            | "uplus"
115            | "lt"
116            | "le"
117            | "gt"
118            | "ge"
119            | "eq"
120            | "ne"
121            | "and"
122            | "or"
123            | "xor"
124            | "not"
125    )
126}
127
128fn is_receiver_validation_error(err: &RuntimeError) -> bool {
129    err.identifier()
130        .is_some_and(|identifier| identifier.ends_with("ReceiverInvalid"))
131}
132
133async fn call_identity_with_policy(
134    identity: CallableIdentity,
135    args: Vec<Value>,
136    requested_outputs: usize,
137    fallback_policy: CallableFallbackPolicy,
138) -> Result<Value, RuntimeError> {
139    Box::pin(execute_callable_descriptor(CallableDescriptor::resolved(
140        identity,
141        args,
142        requested_outputs,
143        fallback_policy,
144        CallableCallKind::Direct,
145    )))
146    .await
147}
148
149async fn try_call_identity_with_policy(
150    identity: CallableIdentity,
151    args: Vec<Value>,
152    requested_outputs: usize,
153    fallback_policy: CallableFallbackPolicy,
154) -> Result<Option<Value>, RuntimeError> {
155    Box::pin(try_execute_callable_descriptor(
156        CallableDescriptor::resolved(
157            identity,
158            args,
159            requested_outputs,
160            fallback_policy,
161            CallableCallKind::Direct,
162        ),
163    ))
164    .await
165}
166
167async fn call_member_index_on_object_like(
168    receiver: Value,
169    class_name: &str,
170    name: String,
171    args: Vec<Value>,
172    requested_outputs: usize,
173    caller_function_name: Option<&str>,
174) -> Result<Value, RuntimeError> {
175    if args.is_empty()
176        && crate::class_registry::get_class(class_name)
177            .is_some_and(|class_def| class_defines_member_subsref(&class_def))
178        && !caller_has_internal_class_access(caller_function_name, class_name)
179    {
180        return Box::pin(call_object_member_subsref(receiver, name)).await;
181    }
182    if let Some((m, owner)) = crate::class_registry::lookup_method(class_name, &name) {
183        if m.is_static {
184            return Err(semantic_error(
185                "MethodStaticOnInstance",
186                format!(
187                    "Method '{}' is static; use classref({}).{}",
188                    name, class_name, name
189                ),
190            ));
191        }
192        if !method_access_permitted(&owner, &m.access, caller_function_name) {
193            return Err(semantic_error(
194                "MethodPrivate",
195                format!("Method '{}' is private", name),
196            ));
197        }
198        let mut full_args = Vec::with_capacity(1 + args.len());
199        full_args.push(receiver.clone());
200        full_args.extend(args.iter().cloned());
201        let (identity, fallback_policy) = method_function_identity(&owner, &name, &m.function_name);
202        return call_identity_with_policy(identity, full_args, requested_outputs, fallback_policy)
203            .await;
204    }
205
206    let mut method_args = Vec::with_capacity(1 + args.len());
207    method_args.push(receiver.clone());
208    method_args.extend(args.iter().cloned());
209    let qualified_identity = external_qualified_identity(class_name, &name);
210    if let Some(v) = try_call_identity_with_policy(
211        qualified_identity.clone(),
212        method_args.clone(),
213        requested_outputs,
214        CallableFallbackPolicy::ExternalBoundary,
215    )
216    .await?
217    {
218        return Ok(v);
219    }
220    // Prevent recursive re-entry for operator overloading (e.g. builtin `plus` calling back
221    // into object dispatch). If class-qualified lookup fails, surface the miss to arithmetic
222    // fallback instead of resolving unqualified operator names at runtime.
223    if is_operator_overload_name(&name) {
224        return call_identity_with_policy(
225            qualified_identity,
226            method_args,
227            requested_outputs,
228            CallableFallbackPolicy::ExternalBoundary,
229        )
230        .await;
231    }
232
233    let (name_identity, name_fallback) = runtime_named_identity(&name);
234    if let Some(v) = try_call_identity_with_policy(
235        name_identity.clone(),
236        method_args.clone(),
237        requested_outputs,
238        name_fallback,
239    )
240    .await?
241    {
242        return Ok(v);
243    }
244
245    match call_identity_with_policy(
246        qualified_identity,
247        method_args.clone(),
248        requested_outputs,
249        CallableFallbackPolicy::ExternalBoundary,
250    )
251    .await
252    {
253        Ok(v) => return Ok(v),
254        Err(err) if err.identifier() == Some("RunMat:UndefinedFunction") => {}
255        Err(err) => return Err(err),
256    }
257
258    match call_identity_with_policy(name_identity, method_args, requested_outputs, name_fallback)
259        .await
260    {
261        Ok(v) => return Ok(v),
262        Err(err) if err.identifier() == Some("RunMat:UndefinedFunction") => {}
263        Err(err) => return Err(err),
264    }
265
266    if name == crate::OBJECT_INDEX_PAREN || name == crate::OBJECT_INDEX_BRACE {
267        return Err(semantic_error(
268            "MissingSubsref",
269            "class does not define subsref for indexing operation",
270        ));
271    }
272
273    call_getfield_with_indices(receiver, name, args, requested_outputs).await
274}
275
276pub async fn call_rhs_operator_method_ordered_with_outputs(
277    lhs: Value,
278    rhs: Value,
279    name: String,
280    requested_outputs: usize,
281    caller_function_name: Option<&str>,
282) -> Result<Value, RuntimeError> {
283    let class_name = match &rhs {
284        Value::Object(obj) => obj.class_name.clone(),
285        Value::HandleObject(handle) => handle.class_name.clone(),
286        _ => {
287            return Err(semantic_error(
288                "InvalidObjectDispatch",
289                "right-hand operator dispatch requires an object operand",
290            ));
291        }
292    };
293
294    let method_args = vec![lhs.clone(), rhs.clone()];
295    if let Some((m, owner)) = crate::class_registry::lookup_method(&class_name, &name) {
296        if m.is_static {
297            return Err(semantic_error(
298                "MethodStaticOnInstance",
299                format!(
300                    "Method '{}' is static; use classref({}).{}",
301                    name, class_name, name
302                ),
303            ));
304        }
305        if !method_access_permitted(&owner, &m.access, caller_function_name) {
306            return Err(semantic_error(
307                "MethodPrivate",
308                format!("Method '{}' is private", name),
309            ));
310        }
311        let (identity, fallback_policy) = method_function_identity(&owner, &name, &m.function_name);
312        return match call_identity_with_policy(
313            identity.clone(),
314            method_args,
315            requested_outputs,
316            fallback_policy,
317        )
318        .await
319        {
320            Ok(v) => Ok(v),
321            Err(err) if is_receiver_validation_error(&err) && is_operator_overload_name(&name) => {
322                call_identity_with_policy(
323                    identity,
324                    vec![rhs.clone(), lhs.clone()],
325                    requested_outputs,
326                    fallback_policy,
327                )
328                .await
329            }
330            Err(err) => Err(err),
331        };
332    }
333
334    let qualified_identity = external_qualified_identity(&class_name, &name);
335    let ordered_result = call_identity_with_policy(
336        qualified_identity.clone(),
337        method_args.clone(),
338        requested_outputs,
339        CallableFallbackPolicy::ExternalBoundary,
340    )
341    .await;
342    match ordered_result {
343        Ok(v) => Ok(v),
344        Err(ordered_err) => {
345            if ordered_err.identifier() != Some("RunMat:UndefinedFunction")
346                && !is_receiver_validation_error(&ordered_err)
347            {
348                return Err(ordered_err);
349            }
350            let receiver_first_args = vec![rhs.clone(), lhs.clone()];
351            match call_identity_with_policy(
352                qualified_identity.clone(),
353                receiver_first_args,
354                requested_outputs,
355                CallableFallbackPolicy::ExternalBoundary,
356            )
357            .await
358            {
359                Ok(v) => Ok(v),
360                Err(receiver_err) => Err(receiver_err),
361            }
362        }
363    }
364}
365
366pub async fn call_getfield_with_indices(
367    base: Value,
368    field: String,
369    indices: Vec<Value>,
370    _requested_outputs: usize,
371) -> Result<Value, RuntimeError> {
372    let mut getfield_args = Vec::with_capacity(2);
373    getfield_args.push(Value::String(field));
374    if !indices.is_empty() {
375        let idx_count = indices.len();
376        let idx_cell = build_cell_array_with_shape(indices, 1, idx_count, "getfield idx build")?;
377        getfield_args.push(Value::Cell(idx_cell));
378    }
379    crate::builtins::structs::core::getfield::getfield_internal(base, getfield_args).await
380}
381
382pub async fn call_object_operator_method(
383    base: Value,
384    method: &str,
385    arg: Value,
386) -> Result<Value, RuntimeError> {
387    call_method_or_member_index_with_outputs(
388        base,
389        CallableIdentity::Method(MethodId(method.to_string())),
390        vec![arg],
391        1,
392        None,
393        CallableFallbackPolicy::ObjectDispatch,
394    )
395    .await
396}
397
398pub async fn call_rhs_object_operator_method_ordered(
399    lhs: Value,
400    rhs: Value,
401    method: &str,
402) -> Result<Value, RuntimeError> {
403    call_rhs_operator_method_ordered_with_outputs(lhs, rhs, method.to_string(), 1, None).await
404}
405
406pub async fn call_object_named_method_with_outputs(
407    base: Value,
408    method: String,
409    args: Vec<Value>,
410    requested_outputs: usize,
411) -> Result<Value, RuntimeError> {
412    call_method_or_member_index_with_outputs(
413        base,
414        CallableIdentity::Method(MethodId(method.clone())),
415        args,
416        requested_outputs,
417        None,
418        CallableFallbackPolicy::ObjectDispatch,
419    )
420    .await
421}
422
423pub async fn call_object_property_getter_with_outputs(
424    base: Value,
425    field: &str,
426    requested_outputs: usize,
427) -> Result<Value, RuntimeError> {
428    call_object_named_method_with_outputs(
429        base,
430        crate::object_property_getter_name(field),
431        vec![],
432        requested_outputs,
433    )
434    .await
435}
436
437pub async fn call_object_property_setter_with_outputs(
438    base: Value,
439    field: &str,
440    value: Value,
441    requested_outputs: usize,
442) -> Result<Value, RuntimeError> {
443    call_object_named_method_with_outputs(
444        base,
445        crate::object_property_setter_name(field),
446        vec![value],
447        requested_outputs,
448    )
449    .await
450}
451
452async fn call_object_member_method(
453    base: Value,
454    op: ObjectIndexOp,
455    field: String,
456    rhs: Option<Value>,
457) -> Result<Value, RuntimeError> {
458    call_object_index_descriptor_method(ObjectIndexDescriptor::member(base, op, field, rhs)).await
459}
460
461pub async fn call_object_member_subsref(base: Value, field: String) -> Result<Value, RuntimeError> {
462    call_object_member_method(base, ObjectIndexOp::Subsref, field, None).await
463}
464
465pub async fn call_object_member_subsasgn(
466    base: Value,
467    field: String,
468    rhs: Value,
469) -> Result<Value, RuntimeError> {
470    call_object_member_method(base, ObjectIndexOp::Subsasgn, field, Some(rhs)).await
471}
472
473pub fn class_defines_member_subsref(class: &crate::class_registry::RuntimeClass) -> bool {
474    crate::class_registry::lookup_method(&class.name, ObjectIndexOp::Subsref.protocol_name())
475        .is_some()
476}
477
478pub fn class_defines_member_subsasgn(class: &crate::class_registry::RuntimeClass) -> bool {
479    crate::class_registry::lookup_method(&class.name, ObjectIndexOp::Subsasgn.protocol_name())
480        .is_some()
481}
482
483pub async fn call_object_index_descriptor_method(
484    descriptor: ObjectIndexDescriptor,
485) -> Result<Value, RuntimeError> {
486    call_object_index_descriptor_method_with_outputs(descriptor, 1).await
487}
488
489pub async fn call_object_index_descriptor_method_with_outputs(
490    descriptor: ObjectIndexDescriptor,
491    requested_outputs: usize,
492) -> Result<Value, RuntimeError> {
493    if let Some(class_name) = class_name_from_base(descriptor.base()) {
494        if let Some((method, owner)) =
495            crate::class_registry::lookup_method(class_name, descriptor.operation().protocol_name())
496        {
497            let mut semantic_args = vec![
498                descriptor.base().clone(),
499                build_matlab_substruct_arg(&descriptor)?,
500            ];
501            if let Some(rhs) = descriptor.rhs() {
502                semantic_args.push(rhs.clone());
503            }
504            if let Some(result) = crate::user_functions::try_call_semantic_function_by_name(
505                &method.function_name,
506                &semantic_args,
507                requested_outputs,
508            )
509            .await
510            {
511                return result;
512            }
513            let owner_qualified = format!("{}.{}", owner, descriptor.operation().protocol_name());
514            if owner_qualified != method.function_name {
515                if let Some(result) = crate::user_functions::try_call_semantic_function_by_name(
516                    &owner_qualified,
517                    &semantic_args,
518                    requested_outputs,
519                )
520                .await
521                {
522                    return result;
523                }
524            }
525        }
526    }
527    let (base, method, args) = descriptor.into_method_invocation()?;
528    call_method_or_member_index_with_outputs(
529        base,
530        CallableIdentity::Method(MethodId(method.clone())),
531        args,
532        requested_outputs,
533        None,
534        CallableFallbackPolicy::ObjectDispatch,
535    )
536    .await
537}
538
539pub async fn call_method_or_member_index_with_outputs(
540    base: Value,
541    identity: CallableIdentity,
542    args: Vec<Value>,
543    requested_outputs: usize,
544    caller_function_name: Option<&str>,
545    _fallback_policy: CallableFallbackPolicy,
546) -> Result<Value, RuntimeError> {
547    let name = method_member_name(&identity).ok_or_else(|| {
548        semantic_error(
549            "MethodCallCalleeInvalid",
550            format!(
551                "method/member-index call requires method-like callable identity, got {identity:?}"
552            ),
553        )
554    })?;
555    call_method_or_member_index_named_with_outputs(
556        base,
557        name,
558        args,
559        requested_outputs,
560        caller_function_name,
561    )
562    .await
563}
564
565pub async fn call_method_or_member_index_named_with_outputs(
566    base: Value,
567    name: String,
568    args: Vec<Value>,
569    requested_outputs: usize,
570    caller_function_name: Option<&str>,
571) -> Result<Value, RuntimeError> {
572    match base {
573        Value::Object(obj) => {
574            let class_name = obj.class_name.clone();
575            call_member_index_on_object_like(
576                Value::Object(obj),
577                &class_name,
578                name,
579                args,
580                requested_outputs,
581                caller_function_name,
582            )
583            .await
584        }
585        Value::HandleObject(handle) => {
586            let class_name = handle.class_name.clone();
587            call_member_index_on_object_like(
588                Value::HandleObject(handle),
589                &class_name,
590                name,
591                args,
592                requested_outputs,
593                caller_function_name,
594            )
595            .await
596        }
597        Value::ClassRef(cls) => {
598            if let Some((m, owner)) = crate::class_registry::lookup_method(&cls, &name) {
599                if !m.is_static {
600                    return Err(semantic_error(
601                        "MethodNotStatic",
602                        format!("Method '{}' is not static", name),
603                    ));
604                }
605                if !method_access_permitted(&owner, &m.access, caller_function_name) {
606                    return Err(semantic_error(
607                        "MethodPrivate",
608                        format!("Method '{}' is private", name),
609                    ));
610                }
611                let (identity, fallback_policy) = runtime_named_identity(&m.function_name);
612                return call_identity_with_policy(
613                    identity,
614                    args,
615                    requested_outputs,
616                    fallback_policy,
617                )
618                .await;
619            }
620            if crate::class_registry::get_class(&cls).is_none() {
621                return Err(semantic_error(
622                    "UndefinedFunction",
623                    format!("Undefined function in direct call: {cls}.{name}"),
624                ));
625            }
626
627            let qualified_identity = external_qualified_identity(&cls, &name);
628            call_identity_with_policy(
629                qualified_identity,
630                args,
631                requested_outputs,
632                CallableFallbackPolicy::ExternalBoundary,
633            )
634            .await
635        }
636        other => call_getfield_with_indices(other, name, args, requested_outputs).await,
637    }
638}
639
640fn build_cell_array_with_shape(
641    values: Vec<Value>,
642    rows: usize,
643    cols: usize,
644    context: &str,
645) -> Result<runmat_value::CellArray, RuntimeError> {
646    runmat_value::CellArray::new(values, rows, cols)
647        .map_err(|error| semantic_error("ShapeMismatch", format!("{context}: {error}")))
648}