Skip to main content

runmat_runtime/builtins/logical/tests/
isnumeric.rs

1//! MATLAB-compatible `isnumeric` builtin with GPU-aware semantics for RunMat.
2
3use runmat_accelerate_api::GpuTensorHandle;
4use runmat_builtins::{ResolveContext, Type, Value};
5use runmat_macros::runtime_builtin;
6
7use crate::builtins::common::gpu_helpers;
8use crate::builtins::common::spec::{
9    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
10    ProviderHook, ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
11};
12use crate::{build_runtime_error, BuiltinResult, RuntimeError};
13
14#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::logical::tests::isnumeric")]
15pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
16    name: "isnumeric",
17    op_kind: GpuOpKind::Custom("metadata"),
18    supported_precisions: &[ScalarType::F32, ScalarType::F64],
19    broadcast: BroadcastSemantics::None,
20    provider_hooks: &[ProviderHook::Custom("logical_islogical")],
21    constant_strategy: ConstantStrategy::InlineLiteral,
22    residency: ResidencyPolicy::GatherImmediately,
23    nan_mode: ReductionNaN::Include,
24    two_pass_threshold: None,
25    workgroup_size: None,
26    accepts_nan_mode: false,
27    notes:
28        "Uses provider metadata to distinguish logical gpuArrays from numeric ones; otherwise falls back to runtime residency tracking.",
29};
30
31#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::logical::tests::isnumeric")]
32pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
33    name: "isnumeric",
34    shape: ShapeRequirements::Any,
35    constant_strategy: ConstantStrategy::InlineLiteral,
36    elementwise: None,
37    reduction: None,
38    emits_nan: false,
39    notes: "Type check executed outside fusion; planners treat it as a scalar metadata query.",
40};
41
42const BUILTIN_NAME: &str = "isnumeric";
43const IDENTIFIER_INTERNAL: &str = "RunMat:isnumeric:InternalError";
44
45#[runtime_builtin(
46    name = "isnumeric",
47    category = "logical/tests",
48    summary = "Return true when a value is stored as numeric data.",
49    keywords = "isnumeric,numeric,type,gpu",
50    accel = "metadata",
51    type_resolver(bool_scalar_type),
52    builtin_path = "crate::builtins::logical::tests::isnumeric"
53)]
54async fn isnumeric_builtin(value: Value) -> BuiltinResult<Value> {
55    match value {
56        Value::GpuTensor(handle) => isnumeric_gpu(handle).await,
57        other => Ok(Value::Bool(isnumeric_value(&other))),
58    }
59}
60
61fn bool_scalar_type(_: &[Type], _context: &ResolveContext) -> Type {
62    Type::Bool
63}
64
65async fn isnumeric_gpu(handle: GpuTensorHandle) -> BuiltinResult<Value> {
66    if let Some(provider) = runmat_accelerate_api::provider() {
67        if let Ok(flag) = provider.logical_islogical(&handle) {
68            return Ok(Value::Bool(!flag));
69        }
70    }
71
72    if runmat_accelerate_api::handle_is_logical(&handle) {
73        return Ok(Value::Bool(false));
74    }
75
76    // Fall back to gathering only when metadata is unavailable.
77    let gpu_value = Value::GpuTensor(handle.clone());
78    let gathered = gpu_helpers::gather_value_async(&gpu_value)
79        .await
80        .map_err(|err| internal_error(format!("isnumeric: {err}")))?;
81    isnumeric_host(gathered)
82}
83
84fn isnumeric_host(value: Value) -> BuiltinResult<Value> {
85    if matches!(value, Value::GpuTensor(_)) {
86        return Err(internal_error(
87            "isnumeric: internal error, GPU value reached host path",
88        ));
89    }
90    Ok(Value::Bool(isnumeric_value(&value)))
91}
92
93fn isnumeric_value(value: &Value) -> bool {
94    matches!(
95        value,
96        Value::Num(_)
97            | Value::Int(_)
98            | Value::Complex(_, _)
99            | Value::Tensor(_)
100            | Value::ComplexTensor(_)
101    )
102}
103
104fn internal_error(message: impl Into<String>) -> RuntimeError {
105    build_runtime_error(message)
106        .with_identifier(IDENTIFIER_INTERNAL)
107        .with_builtin(BUILTIN_NAME)
108        .build()
109}
110
111#[cfg(test)]
112pub(crate) mod tests {
113    use super::*;
114    use crate::builtins::common::test_support;
115    use futures::executor::block_on;
116    use runmat_accelerate_api::HostTensorView;
117    use runmat_builtins::{
118        CellArray, CharArray, Closure, ComplexTensor, HandleRef, IntValue, Listener, LogicalArray,
119        MException, ObjectInstance, ResolveContext, StringArray, StructValue, Tensor, Type,
120    };
121    use runmat_gc_api::GcPtr;
122
123    fn run_isnumeric(value: Value) -> BuiltinResult<Value> {
124        block_on(super::isnumeric_builtin(value))
125    }
126
127    #[test]
128    fn isnumeric_type_returns_bool() {
129        assert_eq!(
130            bool_scalar_type(&[Type::Num], &ResolveContext::new(Vec::new())),
131            Type::Bool
132        );
133    }
134
135    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
136    #[test]
137    fn numeric_scalars_return_true() {
138        assert_eq!(run_isnumeric(Value::Num(3.5)).unwrap(), Value::Bool(true));
139        assert_eq!(
140            run_isnumeric(Value::Int(IntValue::I16(7))).unwrap(),
141            Value::Bool(true)
142        );
143        assert_eq!(
144            run_isnumeric(Value::Complex(1.0, -2.0)).unwrap(),
145            Value::Bool(true)
146        );
147    }
148
149    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
150    #[test]
151    fn numeric_tensors_return_true() {
152        let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
153        assert_eq!(
154            run_isnumeric(Value::Tensor(tensor)).unwrap(),
155            Value::Bool(true)
156        );
157
158        let complex = ComplexTensor::new(vec![(1.0, 2.0), (3.0, 4.0)], vec![2, 1]).unwrap();
159        assert_eq!(
160            run_isnumeric(Value::ComplexTensor(complex)).unwrap(),
161            Value::Bool(true)
162        );
163    }
164
165    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
166    #[test]
167    fn non_numeric_types_return_false() {
168        assert_eq!(
169            run_isnumeric(Value::Bool(true)).unwrap(),
170            Value::Bool(false)
171        );
172
173        let logical = LogicalArray::new(vec![1, 0], vec![2, 1]).unwrap();
174        assert_eq!(
175            run_isnumeric(Value::LogicalArray(logical)).unwrap(),
176            Value::Bool(false)
177        );
178
179        let chars = CharArray::new("rm".chars().collect(), 1, 2).unwrap();
180        assert_eq!(
181            run_isnumeric(Value::CharArray(chars)).unwrap(),
182            Value::Bool(false)
183        );
184
185        assert_eq!(
186            run_isnumeric(Value::String("runmat".into())).unwrap(),
187            Value::Bool(false)
188        );
189        assert_eq!(
190            run_isnumeric(Value::Struct(StructValue::new())).unwrap(),
191            Value::Bool(false)
192        );
193        let string_array =
194            StringArray::new(vec!["foo".into(), "bar".into()], vec![1, 2]).expect("string array");
195        assert_eq!(
196            run_isnumeric(Value::StringArray(string_array)).unwrap(),
197            Value::Bool(false)
198        );
199        let cell =
200            CellArray::new(vec![Value::Num(1.0), Value::Bool(false)], 1, 2).expect("cell array");
201        assert_eq!(
202            run_isnumeric(Value::Cell(cell)).unwrap(),
203            Value::Bool(false)
204        );
205        let object = ObjectInstance::new("runmat.MockObject".into());
206        assert_eq!(
207            run_isnumeric(Value::Object(object)).unwrap(),
208            Value::Bool(false)
209        );
210        assert_eq!(
211            run_isnumeric(Value::FunctionHandle("runmat_fun".into())).unwrap(),
212            Value::Bool(false)
213        );
214        let closure = Closure {
215            function_name: "anon".into(),
216            captures: vec![Value::Num(1.0)],
217        };
218        assert_eq!(
219            run_isnumeric(Value::Closure(closure)).unwrap(),
220            Value::Bool(false)
221        );
222        let handle = HandleRef {
223            class_name: "runmat.Handle".into(),
224            target: GcPtr::null(),
225            valid: true,
226        };
227        assert_eq!(
228            run_isnumeric(Value::HandleObject(handle)).unwrap(),
229            Value::Bool(false)
230        );
231        let listener = Listener {
232            id: 1,
233            target: GcPtr::null(),
234            event_name: "changed".into(),
235            callback: GcPtr::null(),
236            enabled: true,
237            valid: true,
238        };
239        assert_eq!(
240            run_isnumeric(Value::Listener(listener)).unwrap(),
241            Value::Bool(false)
242        );
243        assert_eq!(
244            run_isnumeric(Value::ClassRef("pkg.Class".into())).unwrap(),
245            Value::Bool(false)
246        );
247        let mex = MException::new("RunMat:mock".into(), "message".into());
248        assert_eq!(
249            run_isnumeric(Value::MException(mex)).unwrap(),
250            Value::Bool(false)
251        );
252    }
253
254    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
255    #[test]
256    fn gpu_numeric_and_logical_handles() {
257        test_support::with_test_provider(|provider| {
258            let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
259            let view = HostTensorView {
260                data: &tensor.data,
261                shape: &tensor.shape,
262            };
263            let numeric_handle = provider.upload(&view).expect("upload");
264            let numeric = run_isnumeric(Value::GpuTensor(numeric_handle.clone())).unwrap();
265            assert_eq!(numeric, Value::Bool(true));
266
267            let logical_value = gpu_helpers::logical_gpu_value(numeric_handle.clone());
268            let logical = run_isnumeric(logical_value).unwrap();
269            assert_eq!(logical, Value::Bool(false));
270
271            runmat_accelerate_api::clear_handle_logical(&numeric_handle);
272            provider.free(&numeric_handle).ok();
273        });
274    }
275
276    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
277    #[test]
278    #[cfg(feature = "wgpu")]
279    fn isnumeric_wgpu_handles_respect_metadata() {
280        let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
281            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
282        );
283        let provider = runmat_accelerate_api::provider().expect("wgpu provider");
284
285        let data = vec![1.0, 2.0, 3.0, 4.0];
286        let shape = vec![4, 1];
287        let view = HostTensorView {
288            data: &data,
289            shape: &shape,
290        };
291        let handle = provider.upload(&view).expect("upload");
292        let numeric = run_isnumeric(Value::GpuTensor(handle.clone())).unwrap();
293        assert_eq!(numeric, Value::Bool(true));
294
295        let logical_value = gpu_helpers::logical_gpu_value(handle.clone());
296        let logical = run_isnumeric(logical_value).unwrap();
297        assert_eq!(logical, Value::Bool(false));
298
299        runmat_accelerate_api::clear_handle_logical(&handle);
300        provider.free(&handle).ok();
301    }
302}