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::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};
12#[cfg(feature = "doc_export")]
13use crate::register_builtin_doc_text;
14use crate::{register_builtin_fusion_spec, register_builtin_gpu_spec};
15
16#[cfg(feature = "doc_export")]
17pub const DOC_MD: &str = r#"---
18title: "isnumeric"
19category: "logical/tests"
20keywords: ["isnumeric", "numeric type", "type predicate", "gpuArray isnumeric", "MATLAB isnumeric"]
21summary: "Return true when a value is stored as numeric data (real or complex)."
22references: []
23gpu_support:
24  elementwise: false
25  reduction: false
26  precisions: ["f32", "f64"]
27  broadcasting: "none"
28  notes: "Queries device metadata; falls back to runtime residency tracking when provider hooks are absent."
29fusion:
30  elementwise: false
31  reduction: false
32  max_inputs: 1
33  constants: "inline"
34requires_feature: null
35tested:
36  unit: "builtins::logical::tests::isnumeric::tests"
37  integration: "builtins::logical::tests::isnumeric::tests::gpu_numeric_and_logical_handles"
38  gpu: "builtins::logical::tests::isnumeric::tests::isnumeric_wgpu_handles_respect_metadata"
39  doc: "builtins::logical::tests::isnumeric::tests::doc_examples_present"
40---
41
42# What does the `isnumeric` function do in MATLAB / RunMat?
43`tf = isnumeric(x)` returns a logical scalar that is `true` when `x` stores numeric data and
44`false` otherwise. Numeric data includes doubles, singles, integers, and complex numbers, as
45well as dense numeric arrays that live on the CPU or GPU.
46
47## How does the `isnumeric` function behave in MATLAB / RunMat?
48- Every built-in numeric class (`double`, `single`, signed/unsigned integer types) returns
49  `true`, including complex scalars.
50- Real and complex numeric arrays return `true` regardless of dimensionality or residency on
51  the CPU or GPU.
52- `gpuArray` values rely on provider metadata: numeric handles return `true`, while logical
53  masks constructed on the GPU return `false`.
54- Logical values, characters, strings, tables, cell arrays, structs, objects, and function
55  handles return `false`.
56- The result is always a logical scalar.
57
58## Examples of using the `isnumeric` function in MATLAB / RunMat
59
60### Checking if a scalar double is numeric
61
62```matlab
63tf = isnumeric(42);
64```
65
66Expected output:
67
68```matlab
69tf =
70     1
71```
72
73### Detecting numeric matrices
74
75```matlab
76A = [1 2 3; 4 5 6];
77tf = isnumeric(A);
78```
79
80Expected output:
81
82```matlab
83tf =
84     1
85```
86
87### Testing complex numbers for numeric storage
88
89```matlab
90z = 1 + 2i;
91tf = isnumeric(z);
92```
93
94Expected output:
95
96```matlab
97tf =
98     1
99```
100
101### Logical arrays are not numeric
102
103```matlab
104mask = logical([1 0 1]);
105tf = isnumeric(mask);
106```
107
108Expected output:
109
110```matlab
111tf =
112     0
113```
114
115### Character vectors and strings return false
116
117```matlab
118chars = ['R' 'u' 'n'];
119name = "RunMat";
120tf_chars = isnumeric(chars);
121tf_string = isnumeric(name);
122```
123
124Expected output:
125
126```matlab
127tf_chars =
128     0
129
130tf_string =
131     0
132```
133
134### Evaluating `gpuArray` inputs
135
136```matlab
137G = gpuArray(rand(4));
138mask = G > 0.5;
139tf_numeric = isnumeric(G);
140tf_mask = isnumeric(mask);
141```
142
143Expected output:
144
145```matlab
146tf_numeric =
147     1
148
149tf_mask =
150     0
151```
152
153## `isnumeric` Function GPU Execution Behaviour
154When RunMat Accelerate is active, `isnumeric` first checks provider metadata via the
155`logical_islogical` hook to determine whether a `gpuArray` handle was created as a logical
156mask. Providers that supply the hook can answer the query without copying data back to the
157host. When the hook is absent, RunMat consults its residency metadata and only gathers the
158value to host memory when the residency tag is missing, ensuring the builtin always succeeds.
159
160## GPU residency in RunMat (Do I need `gpuArray`?)
161You generally do **not** need to call `gpuArray` manually. RunMat's auto-offload planner keeps
162numeric tensors on the GPU across fused expressions whenever that improves performance.
163Explicit `gpuArray` and `gather` calls remain available for compatibility with MATLAB scripts
164that manage residency themselves.
165
166## FAQ
167
168### Does `isnumeric` ever return an array?
169No. The builtin always returns a logical scalar, even when the input is an array.
170
171### Are complex tensors considered numeric?
172Yes. Real and complex tensors both return `true`, matching MATLAB semantics.
173
174### Does `isnumeric` gather GPU data back to the host?
175Only when residency metadata is unavailable. Providers that expose type metadata let RunMat
176answer the query without host↔device transfers.
177
178### Do logical masks return `true`?
179No. Logical scalars and logical arrays return `false`. Use `islogical` if you need to detect
180logical storage explicitly.
181
182### What about character vectors or string arrays?
183They return `false`, just like in MATLAB. Characters and strings are text types rather than
184numeric arrays.
185
186### Do cell arrays or structs ever count as numeric?
187No. Containers and objects always return `false`.
188
189### Is there a difference between CPU and GPU numeric arrays?
190No. Both host and device numeric arrays return `true`; only logical GPU handles report `false`.
191
192## See Also
193[islogical](./islogical), [isreal](./isreal), [isfinite](./isfinite), [gpuArray](../../acceleration/gpu/gpuArray), [gather](../../acceleration/gpu/gather)
194
195## Source & Feedback
196- The full source code for the implementation of the `isnumeric` function is available at: [`crates/runmat-runtime/src/builtins/logical/tests/isnumeric.rs`](https://github.com/runmat-org/runmat/blob/main/crates/runmat-runtime/src/builtins/logical/tests/isnumeric.rs)
197- Found a bug or behavioural difference? Please [open an issue](https://github.com/runmat-org/runmat/issues/new/choose) with details and a minimal repro.
198"#;
199
200pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
201    name: "isnumeric",
202    op_kind: GpuOpKind::Custom("metadata"),
203    supported_precisions: &[ScalarType::F32, ScalarType::F64],
204    broadcast: BroadcastSemantics::None,
205    provider_hooks: &[ProviderHook::Custom("logical_islogical")],
206    constant_strategy: ConstantStrategy::InlineLiteral,
207    residency: ResidencyPolicy::GatherImmediately,
208    nan_mode: ReductionNaN::Include,
209    two_pass_threshold: None,
210    workgroup_size: None,
211    accepts_nan_mode: false,
212    notes:
213        "Uses provider metadata to distinguish logical gpuArrays from numeric ones; otherwise falls back to runtime residency tracking.",
214};
215
216register_builtin_gpu_spec!(GPU_SPEC);
217
218pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
219    name: "isnumeric",
220    shape: ShapeRequirements::Any,
221    constant_strategy: ConstantStrategy::InlineLiteral,
222    elementwise: None,
223    reduction: None,
224    emits_nan: false,
225    notes: "Type check executed outside fusion; planners treat it as a scalar metadata query.",
226};
227
228register_builtin_fusion_spec!(FUSION_SPEC);
229
230#[cfg(feature = "doc_export")]
231register_builtin_doc_text!("isnumeric", DOC_MD);
232
233#[runtime_builtin(
234    name = "isnumeric",
235    category = "logical/tests",
236    summary = "Return true when a value is stored as numeric data.",
237    keywords = "isnumeric,numeric,type,gpu",
238    accel = "metadata"
239)]
240fn isnumeric_builtin(value: Value) -> Result<Value, String> {
241    match value {
242        Value::GpuTensor(handle) => isnumeric_gpu(handle),
243        other => Ok(Value::Bool(isnumeric_value(&other))),
244    }
245}
246
247fn isnumeric_gpu(handle: GpuTensorHandle) -> Result<Value, String> {
248    if let Some(provider) = runmat_accelerate_api::provider() {
249        if let Ok(flag) = provider.logical_islogical(&handle) {
250            return Ok(Value::Bool(!flag));
251        }
252    }
253
254    if runmat_accelerate_api::handle_is_logical(&handle) {
255        return Ok(Value::Bool(false));
256    }
257
258    // Fall back to gathering only when metadata is unavailable.
259    let gpu_value = Value::GpuTensor(handle.clone());
260    if let Ok(gathered) = gpu_helpers::gather_value(&gpu_value) {
261        return isnumeric_host(gathered);
262    }
263
264    Ok(Value::Bool(true))
265}
266
267fn isnumeric_host(value: Value) -> Result<Value, String> {
268    if matches!(value, Value::GpuTensor(_)) {
269        return Err("isnumeric: internal error, GPU value reached host path".to_string());
270    }
271    Ok(Value::Bool(isnumeric_value(&value)))
272}
273
274fn isnumeric_value(value: &Value) -> bool {
275    matches!(
276        value,
277        Value::Num(_)
278            | Value::Int(_)
279            | Value::Complex(_, _)
280            | Value::Tensor(_)
281            | Value::ComplexTensor(_)
282    )
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    use crate::builtins::common::test_support;
289    use runmat_accelerate_api::HostTensorView;
290    use runmat_builtins::{
291        CellArray, CharArray, Closure, ComplexTensor, HandleRef, IntValue, Listener, LogicalArray,
292        MException, ObjectInstance, StringArray, StructValue, Tensor,
293    };
294    use runmat_gc_api::GcPtr;
295
296    #[test]
297    fn numeric_scalars_return_true() {
298        assert_eq!(
299            isnumeric_builtin(Value::Num(3.5)).unwrap(),
300            Value::Bool(true)
301        );
302        assert_eq!(
303            isnumeric_builtin(Value::Int(IntValue::I16(7))).unwrap(),
304            Value::Bool(true)
305        );
306        assert_eq!(
307            isnumeric_builtin(Value::Complex(1.0, -2.0)).unwrap(),
308            Value::Bool(true)
309        );
310    }
311
312    #[test]
313    fn numeric_tensors_return_true() {
314        let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
315        assert_eq!(
316            isnumeric_builtin(Value::Tensor(tensor)).unwrap(),
317            Value::Bool(true)
318        );
319
320        let complex = ComplexTensor::new(vec![(1.0, 2.0), (3.0, 4.0)], vec![2, 1]).unwrap();
321        assert_eq!(
322            isnumeric_builtin(Value::ComplexTensor(complex)).unwrap(),
323            Value::Bool(true)
324        );
325    }
326
327    #[test]
328    fn non_numeric_types_return_false() {
329        assert_eq!(
330            isnumeric_builtin(Value::Bool(true)).unwrap(),
331            Value::Bool(false)
332        );
333
334        let logical = LogicalArray::new(vec![1, 0], vec![2, 1]).unwrap();
335        assert_eq!(
336            isnumeric_builtin(Value::LogicalArray(logical)).unwrap(),
337            Value::Bool(false)
338        );
339
340        let chars = CharArray::new("rm".chars().collect(), 1, 2).unwrap();
341        assert_eq!(
342            isnumeric_builtin(Value::CharArray(chars)).unwrap(),
343            Value::Bool(false)
344        );
345
346        assert_eq!(
347            isnumeric_builtin(Value::String("runmat".into())).unwrap(),
348            Value::Bool(false)
349        );
350        assert_eq!(
351            isnumeric_builtin(Value::Struct(StructValue::new())).unwrap(),
352            Value::Bool(false)
353        );
354        let string_array =
355            StringArray::new(vec!["foo".into(), "bar".into()], vec![1, 2]).expect("string array");
356        assert_eq!(
357            isnumeric_builtin(Value::StringArray(string_array)).unwrap(),
358            Value::Bool(false)
359        );
360        let cell =
361            CellArray::new(vec![Value::Num(1.0), Value::Bool(false)], 1, 2).expect("cell array");
362        assert_eq!(
363            isnumeric_builtin(Value::Cell(cell)).unwrap(),
364            Value::Bool(false)
365        );
366        let object = ObjectInstance::new("runmat.MockObject".into());
367        assert_eq!(
368            isnumeric_builtin(Value::Object(object)).unwrap(),
369            Value::Bool(false)
370        );
371        assert_eq!(
372            isnumeric_builtin(Value::FunctionHandle("runmat_fun".into())).unwrap(),
373            Value::Bool(false)
374        );
375        let closure = Closure {
376            function_name: "anon".into(),
377            captures: vec![Value::Num(1.0)],
378        };
379        assert_eq!(
380            isnumeric_builtin(Value::Closure(closure)).unwrap(),
381            Value::Bool(false)
382        );
383        let handle = HandleRef {
384            class_name: "runmat.Handle".into(),
385            target: GcPtr::null(),
386            valid: true,
387        };
388        assert_eq!(
389            isnumeric_builtin(Value::HandleObject(handle)).unwrap(),
390            Value::Bool(false)
391        );
392        let listener = Listener {
393            id: 1,
394            target: GcPtr::null(),
395            event_name: "changed".into(),
396            callback: GcPtr::null(),
397            enabled: true,
398            valid: true,
399        };
400        assert_eq!(
401            isnumeric_builtin(Value::Listener(listener)).unwrap(),
402            Value::Bool(false)
403        );
404        assert_eq!(
405            isnumeric_builtin(Value::ClassRef("pkg.Class".into())).unwrap(),
406            Value::Bool(false)
407        );
408        let mex = MException::new("MATLAB:mock".into(), "message".into());
409        assert_eq!(
410            isnumeric_builtin(Value::MException(mex)).unwrap(),
411            Value::Bool(false)
412        );
413    }
414
415    #[test]
416    fn gpu_numeric_and_logical_handles() {
417        test_support::with_test_provider(|provider| {
418            let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
419            let view = HostTensorView {
420                data: &tensor.data,
421                shape: &tensor.shape,
422            };
423            let numeric_handle = provider.upload(&view).expect("upload");
424            let numeric = isnumeric_builtin(Value::GpuTensor(numeric_handle.clone())).unwrap();
425            assert_eq!(numeric, Value::Bool(true));
426
427            let logical_value = gpu_helpers::logical_gpu_value(numeric_handle.clone());
428            let logical = isnumeric_builtin(logical_value).unwrap();
429            assert_eq!(logical, Value::Bool(false));
430
431            runmat_accelerate_api::clear_handle_logical(&numeric_handle);
432            provider.free(&numeric_handle).ok();
433        });
434    }
435
436    #[test]
437    #[cfg(feature = "wgpu")]
438    fn isnumeric_wgpu_handles_respect_metadata() {
439        let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
440            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
441        );
442        let provider = runmat_accelerate_api::provider().expect("wgpu provider");
443
444        let data = vec![1.0, 2.0, 3.0, 4.0];
445        let shape = vec![4, 1];
446        let view = HostTensorView {
447            data: &data,
448            shape: &shape,
449        };
450        let handle = provider.upload(&view).expect("upload");
451        let numeric = isnumeric_builtin(Value::GpuTensor(handle.clone())).unwrap();
452        assert_eq!(numeric, Value::Bool(true));
453
454        let logical_value = gpu_helpers::logical_gpu_value(handle.clone());
455        let logical = isnumeric_builtin(logical_value).unwrap();
456        assert_eq!(logical, Value::Bool(false));
457
458        runmat_accelerate_api::clear_handle_logical(&handle);
459        provider.free(&handle).ok();
460    }
461
462    #[test]
463    #[cfg(feature = "doc_export")]
464    fn doc_examples_present() {
465        let blocks = test_support::doc_examples(DOC_MD);
466        assert!(!blocks.is_empty());
467    }
468}