use crate::builtins::common::spec::{
BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
ReductionNaN, ResidencyPolicy, ShapeRequirements,
};
use crate::builtins::introspection::type_resolvers::ismethod_type;
use crate::{build_runtime_error, BuiltinResult, RuntimeError};
use runmat_builtins::{
lookup_method, Access, BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor,
BuiltinOutputMode, BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType,
BuiltinSignatureDescriptor, Listener, MException, Value,
};
use runmat_macros::runtime_builtin;
#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::introspection::ismethod")]
pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
name: "ismethod",
op_kind: GpuOpKind::Custom("metadata"),
supported_precisions: &[],
broadcast: BroadcastSemantics::None,
provider_hooks: &[],
constant_strategy: ConstantStrategy::InlineLiteral,
residency: ResidencyPolicy::InheritInputs,
nan_mode: ReductionNaN::Include,
two_pass_threshold: None,
workgroup_size: None,
accepts_nan_mode: false,
notes: "Metadata predicate that inspects host class registry entries and returns a host logical scalar; no GPU kernels or gathers are required.",
};
#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::introspection::ismethod")]
pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
name: "ismethod",
shape: ShapeRequirements::Any,
constant_strategy: ConstantStrategy::InlineLiteral,
elementwise: None,
reduction: None,
emits_nan: false,
notes: "Not eligible for fusion planning; ismethod executes on the host and produces a logical scalar.",
};
const ISMETHOD_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
name: "tf",
ty: BuiltinParamType::LogicalArray,
arity: BuiltinParamArity::Required,
default: None,
description: "True when the object has the requested public method.",
}];
const ISMETHOD_INPUTS: [BuiltinParamDescriptor; 2] = [
BuiltinParamDescriptor {
name: "obj",
ty: BuiltinParamType::Any,
arity: BuiltinParamArity::Required,
default: None,
description: "Object, class name, class reference, or gpuArray value to inspect.",
},
BuiltinParamDescriptor {
name: "method_name",
ty: BuiltinParamType::StringScalar,
arity: BuiltinParamArity::Required,
default: None,
description: "Method name as a string scalar or row character vector.",
},
];
const ISMETHOD_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
label: "tf = ismethod(obj, method_name)",
inputs: &ISMETHOD_INPUTS,
outputs: &ISMETHOD_OUTPUT,
}];
const BUILTIN_NAME: &str = "ismethod";
const ISMETHOD_ERROR_NAME_INVALID: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
code: "RM.ISMETHOD.NAME_INVALID",
identifier: Some("RunMat:IsMethodNameInvalid"),
when: "The method-name argument is not a string scalar or row character vector.",
message: "ismethod: method name must be a string scalar or character vector",
};
const ISMETHOD_ERRORS: [BuiltinErrorDescriptor; 1] = [ISMETHOD_ERROR_NAME_INVALID];
pub const ISMETHOD_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
signatures: &ISMETHOD_SIGNATURES,
output_mode: BuiltinOutputMode::Fixed,
completion_policy: BuiltinCompletionPolicy::Public,
errors: &ISMETHOD_ERRORS,
};
#[runtime_builtin(
name = "ismethod",
category = "introspection",
summary = "Test whether an object has a public method.",
keywords = "ismethod,method,classdef,object,handle,inheritance",
accel = "metadata",
type_resolver(ismethod_type),
descriptor(crate::builtins::introspection::ismethod::ISMETHOD_DESCRIPTOR),
builtin_path = "crate::builtins::introspection::ismethod"
)]
fn ismethod_builtin(receiver: Value, method_designator: Value) -> crate::BuiltinResult<Value> {
let method_name = parse_method_name(&method_designator)?;
Ok(Value::Bool(value_has_public_method(
&receiver,
&method_name,
)))
}
fn parse_method_name(value: &Value) -> BuiltinResult<String> {
match value {
Value::String(s) => Ok(s.clone()),
Value::StringArray(sa) if sa.rows == 1 && sa.cols == 1 && !sa.data.is_empty() => {
Ok(sa.data[0].clone())
}
Value::CharArray(ca) if ca.rows <= 1 => Ok(ca.data.iter().collect::<String>()),
_ => Err(ismethod_error(&ISMETHOD_ERROR_NAME_INVALID)),
}
}
fn ismethod_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
let mut builder = build_runtime_error(error.message).with_builtin(BUILTIN_NAME);
if let Some(identifier) = error.identifier {
builder = builder.with_identifier(identifier);
}
builder.build()
}
fn value_has_public_method(receiver: &Value, method_name: &str) -> bool {
if method_name.is_empty() {
return false;
}
let class_name = match receiver_class_name(receiver) {
Some(class_name) if !class_name.is_empty() => class_name,
_ => return false,
};
lookup_method(&class_name, method_name)
.is_some_and(|(method, _owner)| matches!(method.access, Access::Public))
}
fn receiver_class_name(receiver: &Value) -> Option<String> {
match receiver {
Value::Object(object) => Some(object.class_name.clone()),
Value::HandleObject(handle) => {
if handle.class_name.is_empty() {
Some("handle".to_string())
} else {
Some(handle.class_name.clone())
}
}
Value::GpuTensor(_) => Some("gpuArray".to_string()),
Value::Listener(Listener { .. }) => Some("event.listener".to_string()),
Value::ClassRef(class_name) => Some(class_name.clone()),
Value::MException(MException { .. }) => Some("MException".to_string()),
Value::String(class_name) => Some(class_name.clone()),
Value::StringArray(sa) if sa.rows == 1 && sa.cols == 1 && !sa.data.is_empty() => {
Some(sa.data[0].clone())
}
Value::CharArray(ca) if ca.rows <= 1 => Some(ca.data.iter().collect::<String>()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::builtins::common::test_support;
use runmat_accelerate_api::HostTensorView;
use runmat_builtins::{
register_class, Access, CharArray, ClassDef, HandleRef, MethodDef, ObjectInstance,
StringArray, Tensor,
};
use std::collections::HashMap;
fn unique_class_name(label: &str) -> String {
format!(
"Ismethod{}{}",
label,
std::thread::current().name().unwrap_or("thread")
)
}
fn method(name: &str, access: Access, is_static: bool) -> MethodDef {
MethodDef {
name: name.to_string(),
is_static,
is_abstract: false,
is_sealed: false,
access,
function_name: format!("{name}_impl"),
implicit_class_argument: None,
}
}
fn register_pair(label: &str) -> (String, String) {
let parent = unique_class_name(&format!("{label}Parent"));
let child = unique_class_name(&format!("{label}Child"));
let mut parent_methods = HashMap::new();
parent_methods.insert(
"inherited".to_string(),
method("inherited", Access::Public, false),
);
parent_methods.insert(
"hidden".to_string(),
method("hidden", Access::Private, false),
);
register_class(ClassDef {
name: parent.clone(),
parent: None,
properties: HashMap::new(),
methods: parent_methods,
});
let mut child_methods = HashMap::new();
child_methods.insert("run".to_string(), method("run", Access::Public, false));
child_methods.insert("make".to_string(), method("make", Access::Public, true));
child_methods.insert(
"secret".to_string(),
method("secret", Access::Protected, false),
);
register_class(ClassDef {
name: child.clone(),
parent: Some(parent.clone()),
properties: HashMap::new(),
methods: child_methods,
});
(parent, child)
}
fn object(class_name: &str) -> Value {
Value::Object(ObjectInstance::new(class_name.to_string()))
}
fn handle_object(class_name: &str) -> Value {
let target =
runmat_gc::gc_allocate(Value::Object(ObjectInstance::new(class_name.to_string())))
.expect("gc allocation");
Value::HandleObject(HandleRef {
class_name: class_name.to_string(),
target,
valid: true,
})
}
fn call(receiver: Value, method_name: Value) -> bool {
let Value::Bool(result) = ismethod_builtin(receiver, method_name).expect("ismethod") else {
panic!("ismethod should return bool");
};
result
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn ismethod_finds_public_instance_static_and_inherited_methods() {
let (_parent, child) = register_pair("Public");
let receiver = object(&child);
assert!(call(receiver.clone(), Value::String("run".to_string())));
assert!(call(receiver.clone(), Value::String("make".to_string())));
assert!(call(receiver, Value::String("inherited".to_string())));
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn ismethod_respects_public_visibility() {
let (_parent, child) = register_pair("Access");
let receiver = object(&child);
assert!(!call(receiver.clone(), Value::String("secret".to_string())));
assert!(!call(receiver, Value::String("hidden".to_string())));
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn ismethod_supports_handle_objects_and_scalar_text_forms() {
let (_parent, child) = register_pair("Handle");
let receiver = handle_object(&child);
assert!(call(
receiver.clone(),
Value::CharArray(CharArray::new_row("inherited"))
));
assert!(call(
receiver,
Value::StringArray(
StringArray::new(vec!["run".to_string()], vec![1, 1]).expect("string scalar")
)
));
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn ismethod_supports_class_name_text_and_class_refs() {
let (_parent, child) = register_pair("ClassName");
assert!(call(
Value::String(child.clone()),
Value::String("run".to_string())
));
assert!(call(
Value::CharArray(CharArray::new_row(&child)),
Value::String("make".to_string())
));
assert!(call(
Value::StringArray(
StringArray::new(vec![child.clone()], vec![1, 1]).expect("class scalar")
),
Value::String("inherited".to_string())
));
assert!(call(
Value::ClassRef(child),
Value::String("run".to_string())
));
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn ismethod_uses_gpuarray_class_metadata_without_gather() {
let mut methods = HashMap::new();
methods.insert(
"__runmat_ismethod_marker".to_string(),
method("__runmat_ismethod_marker", Access::Public, false),
);
register_class(ClassDef {
name: "gpuArray".to_string(),
parent: None,
properties: HashMap::new(),
methods,
});
test_support::with_test_provider(|provider| {
let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]).unwrap();
let view = HostTensorView {
data: &tensor.data,
shape: &tensor.shape,
};
let handle = provider.upload(&view).expect("upload");
assert!(call(
Value::GpuTensor(handle),
Value::String("__runmat_ismethod_marker".to_string())
));
});
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn ismethod_returns_false_for_missing_empty_and_nonobject_values() {
let (_parent, child) = register_pair("Negative");
assert!(!call(object(&child), Value::String("missing".to_string())));
assert!(!call(object(&child), Value::String(String::new())));
assert!(!call(object(&child), Value::String(" run ".to_string())));
assert!(!call(Value::Num(1.0), Value::String("run".to_string())));
assert!(!call(
Value::String("MissingClassForIsmethod".to_string()),
Value::String("run".to_string())
));
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn ismethod_rejects_nonscalar_method_name() {
let (_parent, child) = register_pair("Invalid");
let err = ismethod_builtin(
object(&child),
Value::StringArray(
StringArray::new(vec!["run".to_string(), "make".to_string()], vec![1, 2])
.expect("string array"),
),
)
.expect_err("non-scalar string array should fail");
assert_eq!(
err.identifier.as_deref(),
Some("RunMat:IsMethodNameInvalid")
);
}
}