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 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}