use crate::builtins::common::spec::{
BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
ReductionNaN, ResidencyPolicy, ShapeRequirements,
};
use crate::builtins::introspection::class::class_name_for_value;
use crate::builtins::introspection::type_resolvers::isa_type;
use crate::{build_runtime_error, BuiltinResult};
use runmat_accelerate_api::handle_is_logical;
use runmat_builtins::{get_class, Value};
use runmat_macros::runtime_builtin;
#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::introspection::isa")]
pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
name: "isa",
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 returns host logical scalars; no GPU kernels or gathers are required.",
};
#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::introspection::isa")]
pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
name: "isa",
shape: ShapeRequirements::Any,
constant_strategy: ConstantStrategy::InlineLiteral,
elementwise: None,
reduction: None,
emits_nan: false,
notes:
"Not eligible for fusion planning; isa executes on the host and produces a logical scalar.",
};
#[runtime_builtin(
name = "isa",
category = "introspection",
summary = "Test whether a value belongs to a specified MATLAB class or abstract category.",
keywords = "isa,type checking,class comparison,numeric category,gpuArray",
accel = "metadata",
type_resolver(isa_type),
builtin_path = "crate::builtins::introspection::isa"
)]
fn isa_builtin(value: Value, class_designator: Value) -> crate::BuiltinResult<Value> {
let type_name = parse_type_name(&class_designator)?;
let result = value_is_a(&value, &type_name);
Ok(Value::Bool(result))
}
fn parse_type_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())
} else {
Err(
build_runtime_error("isa: TYPE must be a string scalar or character vector")
.with_builtin("isa")
.build()
.into(),
)
}
}
Value::CharArray(ca) => {
if ca.rows == 1 {
Ok(ca.data.iter().collect())
} else {
Err(
build_runtime_error("isa: TYPE must be a string scalar or character vector")
.with_builtin("isa")
.build()
.into(),
)
}
}
_ => Err(
build_runtime_error("isa: TYPE must be a string scalar or character vector")
.with_builtin("isa")
.build()
.into(),
),
}
}
fn value_is_a(value: &Value, requested: &str) -> bool {
let trimmed = requested.trim();
if trimmed.is_empty() {
return false;
}
let requested_lower = trimmed.to_ascii_lowercase();
match requested_lower.as_str() {
"numeric" => is_numeric(value),
"float" => is_float(value),
"integer" => is_integer(value),
"logical" => is_logical(value),
"char" => matches!(value, Value::CharArray(_)),
"string" => matches!(value, Value::String(_) | Value::StringArray(_)),
"cell" => matches!(value, Value::Cell(_)),
"struct" => matches!(value, Value::Struct(_)),
"function_handle" => matches!(value, Value::FunctionHandle(_) | Value::Closure(_)),
"gpuarray" => matches!(value, Value::GpuTensor(_)),
"listener" | "event.listener" => matches!(value, Value::Listener(_)),
"meta.class" => matches!(value, Value::ClassRef(_)),
"mexception" => matches!(value, Value::MException(_)),
"handle" => is_handle_like(value),
_ => {
let actual = class_name_for_value(value);
if actual.eq_ignore_ascii_case(trimmed) {
return true;
}
match value {
Value::Object(obj) => class_inherits(&obj.class_name, &requested_lower),
Value::HandleObject(handle) => {
!handle.class_name.is_empty()
&& class_inherits(&handle.class_name, &requested_lower)
}
_ => false,
}
}
}
}
fn is_numeric(value: &Value) -> bool {
match value {
Value::Num(_)
| Value::Tensor(_)
| Value::ComplexTensor(_)
| Value::Complex(_, _)
| Value::Int(_) => true,
Value::GpuTensor(handle) => !handle_is_logical(handle),
_ => false,
}
}
fn is_float(value: &Value) -> bool {
match value {
Value::Num(_) | Value::Tensor(_) | Value::ComplexTensor(_) | Value::Complex(_, _) => true,
Value::GpuTensor(handle) => !handle_is_logical(handle),
_ => false,
}
}
fn is_integer(value: &Value) -> bool {
matches!(value, Value::Int(_))
}
fn is_logical(value: &Value) -> bool {
match value {
Value::Bool(_) | Value::LogicalArray(_) => true,
Value::GpuTensor(handle) => handle_is_logical(handle),
_ => false,
}
}
fn is_handle_like(value: &Value) -> bool {
match value {
Value::HandleObject(_) | Value::Listener(_) => true,
Value::Object(obj) => class_inherits(&obj.class_name, "handle"),
_ => false,
}
}
fn class_inherits(class_name: &str, requested_lower: &str) -> bool {
if class_name.eq_ignore_ascii_case(requested_lower) {
return true;
}
let mut cursor = Some(class_name.to_string());
while let Some(name) = cursor {
if name.eq_ignore_ascii_case(requested_lower) {
return true;
}
if let Some(def) = get_class(&name) {
cursor = def.parent.clone();
} else {
break;
}
}
false
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::builtins::common::{gpu_helpers, test_support};
use runmat_accelerate_api::HostTensorView;
use runmat_builtins::{
CellArray, CharArray, ClassDef, HandleRef, IntValue, Listener, LogicalArray,
ObjectInstance, StringArray, StructValue, Tensor,
};
use runmat_gc_api::GcPtr;
use std::collections::HashMap;
fn error_message(err: crate::RuntimeError) -> String {
err.message().to_string()
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn isa_reports_expected_results_for_doubles() {
let double_result = isa_builtin(Value::Num(42.0), Value::from("double")).expect("isa");
assert_eq!(double_result, Value::Bool(true));
let numeric_result = isa_builtin(Value::Num(42.0), Value::from("numeric")).expect("isa");
assert_eq!(numeric_result, Value::Bool(true));
let integer_result = isa_builtin(Value::Num(42.0), Value::from("integer")).expect("isa");
assert_eq!(integer_result, Value::Bool(false));
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn isa_integer_category_matches_int_values() {
let value = Value::Int(IntValue::I16(12));
let int_result = isa_builtin(value.clone(), Value::from("integer")).expect("isa");
assert_eq!(int_result, Value::Bool(true));
let float_result = isa_builtin(value, Value::from("float")).expect("isa");
assert_eq!(float_result, Value::Bool(false));
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn isa_handles_logical_and_char_types() {
let logical = Value::LogicalArray(LogicalArray::new(vec![1], vec![1]).unwrap());
assert_eq!(
isa_builtin(logical, Value::from("logical")).expect("isa"),
Value::Bool(true)
);
let char_array = Value::CharArray(CharArray::new_row("RunMat"));
assert_eq!(
isa_builtin(char_array, Value::from("char")).expect("isa"),
Value::Bool(true)
);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn isa_string_and_struct_detection() {
let string_scalar = Value::String("runmat".into());
assert_eq!(
isa_builtin(string_scalar, Value::from("string")).expect("isa"),
Value::Bool(true)
);
let mut st = StructValue::new();
st.fields.insert("field".into(), Value::Num(1.0));
assert_eq!(
isa_builtin(Value::Struct(st), Value::from("struct")).expect("isa"),
Value::Bool(true)
);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn isa_cell_and_function_handle() {
let cell = Value::Cell(CellArray::new(vec![Value::Num(1.0)], 1, 1).unwrap());
assert_eq!(
isa_builtin(cell, Value::from("cell")).expect("isa"),
Value::Bool(true)
);
let func = Value::FunctionHandle("sin".into());
assert_eq!(
isa_builtin(func, Value::from("function_handle")).expect("isa"),
Value::Bool(true)
);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn isa_gpu_arrays_treat_metadata_correctly() {
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");
let gpu_value = Value::GpuTensor(handle);
let numeric = isa_builtin(gpu_value.clone(), Value::from("numeric")).expect("isa");
assert_eq!(numeric, Value::Bool(true));
let double = isa_builtin(gpu_value, Value::from("double")).expect("isa");
assert_eq!(double, Value::Bool(false));
});
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn isa_gpu_logical_handles_match_categories() {
test_support::with_test_provider(|provider| {
let tensor = Tensor::new(vec![1.0, 0.0, 1.0, 0.0], vec![2, 2]).unwrap();
let view = HostTensorView {
data: &tensor.data,
shape: &tensor.shape,
};
let handle = provider.upload(&view).expect("upload");
let logical_value = gpu_helpers::logical_gpu_value(handle.clone());
let logical = isa_builtin(logical_value.clone(), Value::from("logical")).expect("isa");
assert_eq!(logical, Value::Bool(true));
let numeric =
isa_builtin(logical_value, Value::from("numeric")).expect("isa numeric false");
assert_eq!(numeric, Value::Bool(false));
});
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn isa_handle_aliases_and_inheritance() {
let handle = HandleRef {
class_name: "TestHandle".into(),
target: GcPtr::null(),
valid: true,
};
assert_eq!(
isa_builtin(Value::HandleObject(handle), Value::from("handle")).expect("isa"),
Value::Bool(true)
);
let class_name = "pkg.TestHandle";
let def = ClassDef {
name: class_name.into(),
parent: Some("handle".into()),
properties: HashMap::new(),
methods: HashMap::new(),
};
runmat_builtins::register_class(def);
let obj = Value::Object(ObjectInstance::new(class_name.into()));
let handle_result = isa_builtin(obj.clone(), Value::from("handle")).expect("isa");
assert_eq!(handle_result, Value::Bool(true));
let exact = isa_builtin(obj, Value::from(class_name)).expect("isa");
assert_eq!(exact, Value::Bool(true));
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn isa_listener_alias_matches() {
let listener = Listener {
id: 1,
target: GcPtr::null(),
event_name: "Changed".into(),
callback: GcPtr::null(),
enabled: true,
valid: true,
};
let value = Value::Listener(listener);
assert_eq!(
isa_builtin(value.clone(), Value::from("listener")).expect("isa"),
Value::Bool(true)
);
assert_eq!(
isa_builtin(value.clone(), Value::from("event.listener")).expect("isa"),
Value::Bool(true)
);
assert_eq!(
isa_builtin(value, Value::from("handle")).expect("isa"),
Value::Bool(true)
);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn isa_meta_class_detection() {
let meta = Value::ClassRef("Point".into());
assert_eq!(
isa_builtin(meta, Value::from("meta.class")).expect("isa"),
Value::Bool(true)
);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn isa_errors_on_invalid_type_designator() {
let type_array = Value::StringArray(
StringArray::new(vec!["double".into(), "single".into()], vec![1, 2]).unwrap(),
);
let err = isa_builtin(Value::Num(1.0), type_array).unwrap_err();
let message = error_message(err);
assert_eq!(
message,
"isa: TYPE must be a string scalar or character vector"
);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
#[cfg(feature = "wgpu")]
fn isa_gpuarray_with_wgpu_provider_matches_numeric_category() {
use runmat_accelerate::backend::wgpu::provider::ensure_wgpu_provider;
use runmat_accelerate_api::AccelProvider;
let provider = match ensure_wgpu_provider() {
Ok(Some(p)) => p,
_ => return,
};
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("wgpu upload");
let value = Value::GpuTensor(handle);
let numeric = isa_builtin(value.clone(), Value::from("numeric")).expect("isa numeric");
assert_eq!(numeric, Value::Bool(true));
let dbl = isa_builtin(value, Value::from("double")).expect("isa double");
assert_eq!(dbl, Value::Bool(false));
}
}