Skip to main content

runmat_runtime/builtins/introspection/
getmethod.rs

1use runmat_builtins::{
2    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
3    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor, Value,
4};
5use runmat_macros::runtime_builtin;
6
7const GETMETHOD_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
8    name: "fh",
9    ty: BuiltinParamType::Any,
10    arity: BuiltinParamArity::Required,
11    default: None,
12    description: "Bound method closure/handle.",
13}];
14
15const GETMETHOD_INPUTS: [BuiltinParamDescriptor; 2] = [
16    BuiltinParamDescriptor {
17        name: "obj_or_class",
18        ty: BuiltinParamType::Any,
19        arity: BuiltinParamArity::Required,
20        default: None,
21        description: "Object receiver or class reference.",
22    },
23    BuiltinParamDescriptor {
24        name: "name",
25        ty: BuiltinParamType::StringScalar,
26        arity: BuiltinParamArity::Required,
27        default: None,
28        description: "Method name.",
29    },
30];
31
32const GETMETHOD_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
33    label: "fh = getmethod(obj_or_class, name)",
34    inputs: &GETMETHOD_INPUTS,
35    outputs: &GETMETHOD_OUTPUT,
36}];
37
38const GETMETHOD_ERROR_NAME_INVALID: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
39    code: "RM.GETMETHOD.NAME_INVALID",
40    identifier: Some("RunMat:GetMethodNameInvalid"),
41    when: "Method name is empty.",
42    message: "getmethod: method name must not be empty",
43};
44
45const GETMETHOD_ERROR_RECEIVER_UNSUPPORTED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
46    code: "RM.GETMETHOD.RECEIVER_UNSUPPORTED",
47    identifier: Some("RunMat:GetMethodReceiverUnsupported"),
48    when: "Receiver is neither object nor class reference.",
49    message: "getmethod: unsupported receiver",
50};
51
52const GETMETHOD_ERROR_METHOD_PRIVATE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
53    code: "RM.GETMETHOD.METHOD_PRIVATE",
54    identifier: Some("RunMat:MethodPrivate"),
55    when: "Resolved method exists but is inaccessible from the current class scope.",
56    message: "getmethod: method is not accessible from current scope",
57};
58
59const GETMETHOD_ERRORS: [BuiltinErrorDescriptor; 3] = [
60    GETMETHOD_ERROR_NAME_INVALID,
61    GETMETHOD_ERROR_RECEIVER_UNSUPPORTED,
62    GETMETHOD_ERROR_METHOD_PRIVATE,
63];
64
65pub const GETMETHOD_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
66    signatures: &GETMETHOD_SIGNATURES,
67    output_mode: BuiltinOutputMode::Fixed,
68    completion_policy: BuiltinCompletionPolicy::Public,
69    errors: &GETMETHOD_ERRORS,
70};
71
72pub(crate) fn dispatch_getmethod(obj: Value, name: String) -> crate::BuiltinResult<Value> {
73    fn ensure_method_accessible(class_name: &str, method_name: &str) -> crate::BuiltinResult<()> {
74        let Some((method, owner)) = runmat_builtins::lookup_method(class_name, method_name) else {
75            return Ok(());
76        };
77        let caller_class = crate::class_access_context();
78        let access_allowed = match method.access {
79            runmat_builtins::Access::Public => true,
80            runmat_builtins::Access::Private => caller_class.as_deref() == Some(owner.as_str()),
81            runmat_builtins::Access::Protected => caller_class
82                .as_deref()
83                .is_some_and(|caller| runmat_builtins::is_class_or_subclass(caller, &owner)),
84        };
85        if access_allowed {
86            return Ok(());
87        }
88        Err(crate::runtime_descriptor_error_with_detail(
89            "getmethod",
90            &GETMETHOD_ERROR_METHOD_PRIVATE,
91            format!("{}.{}", class_name, method_name),
92        ))
93    }
94
95    let method_name = name.trim();
96    if method_name.is_empty() {
97        return Err(crate::runtime_descriptor_error(
98            "getmethod",
99            &GETMETHOD_ERROR_NAME_INVALID,
100        ));
101    }
102    let caller_scope = crate::class_access_context()
103        .map(Value::String)
104        .unwrap_or_else(|| Value::String(String::new()));
105    match obj {
106        Value::Object(o) => {
107            ensure_method_accessible(&o.class_name, method_name)?;
108            if let Some((resolved, _owner)) =
109                runmat_builtins::lookup_method(&o.class_name, method_name)
110            {
111                return Ok(Value::Closure(runmat_builtins::Closure {
112                    function_name: resolved.function_name.clone(),
113                    bound_function: crate::user_functions::resolve_semantic_function_by_name(
114                        &resolved.function_name,
115                    ),
116                    captures: vec![Value::Object(o)],
117                }));
118            }
119            Ok(Value::Closure(runmat_builtins::Closure {
120                function_name: crate::CALL_BOUND_METHOD_BUILTIN_NAME.to_string(),
121                bound_function: None,
122                captures: vec![
123                    Value::Object(o),
124                    Value::String(method_name.to_string()),
125                    caller_scope.clone(),
126                ],
127            }))
128        }
129        Value::HandleObject(h) => {
130            ensure_method_accessible(&h.class_name, method_name)?;
131            if let Some((resolved, _owner)) =
132                runmat_builtins::lookup_method(&h.class_name, method_name)
133            {
134                return Ok(Value::Closure(runmat_builtins::Closure {
135                    function_name: resolved.function_name.clone(),
136                    bound_function: crate::user_functions::resolve_semantic_function_by_name(
137                        &resolved.function_name,
138                    ),
139                    captures: vec![Value::HandleObject(h)],
140                }));
141            }
142            Ok(Value::Closure(runmat_builtins::Closure {
143                function_name: crate::CALL_BOUND_METHOD_BUILTIN_NAME.to_string(),
144                bound_function: None,
145                captures: vec![
146                    Value::HandleObject(h),
147                    Value::String(method_name.to_string()),
148                    caller_scope,
149                ],
150            }))
151        }
152        Value::ClassRef(cls) => {
153            ensure_method_accessible(&cls, method_name)?;
154            crate::builtins::introspection::function_handle_text::dispatch_str2func(Value::String(
155                format!("@{cls}.{method_name}"),
156            ))
157        }
158        other => Err(crate::runtime_descriptor_error_with_detail(
159            "getmethod",
160            &GETMETHOD_ERROR_RECEIVER_UNSUPPORTED,
161            format!("{other:?}"),
162        )),
163    }
164}
165
166#[runtime_builtin(
167    name = "getmethod",
168    category = "introspection",
169    summary = "Create a method-bound function handle from object/class and method name.",
170    keywords = "method,function_handle,classdef,dispatch",
171    descriptor(crate::builtins::introspection::getmethod::GETMETHOD_DESCRIPTOR),
172    builtin_path = "crate::builtins::introspection::getmethod"
173)]
174pub async fn getmethod_builtin(obj: Value, name: String) -> crate::BuiltinResult<Value> {
175    dispatch_getmethod(obj, name)
176}