runmat_runtime/
introspection.rs

1use runmat_builtins::{Tensor, Value};
2use runmat_macros::runtime_builtin;
3
4fn tensor_from_dims(dims: Vec<usize>) -> Result<Value, String> {
5    // Return as a 1xN row vector (double)
6    let len = dims.len();
7    let data: Vec<f64> = dims.into_iter().map(|d| d as f64).collect();
8    Ok(Value::Tensor(Tensor::new_2d(data, 1, len)?))
9}
10
11fn dims_of_value(v: &Value) -> Vec<usize> {
12    match v {
13        Value::Tensor(t) => {
14            if t.shape.is_empty() {
15                vec![1, 1]
16            } else if t.shape.len() == 1 {
17                vec![1, t.shape[0]]
18            } else {
19                t.shape.clone()
20            }
21        }
22        Value::LogicalArray(la) => {
23            if la.shape.is_empty() {
24                vec![1, 1]
25            } else if la.shape.len() == 1 {
26                vec![1, la.shape[0]]
27            } else {
28                la.shape.clone()
29            }
30        }
31        Value::StringArray(sa) => {
32            if sa.shape.is_empty() {
33                vec![1, 1]
34            } else if sa.shape.len() == 1 {
35                vec![1, sa.shape[0]]
36            } else {
37                sa.shape.clone()
38            }
39        }
40        Value::CharArray(ca) => vec![ca.rows, ca.cols],
41        // Scalars and other values treated as 1x1
42        _ => vec![1, 1],
43    }
44}
45
46fn numel_of_value(v: &Value) -> usize {
47    match v {
48        Value::Tensor(t) => t.data.len(),
49        Value::LogicalArray(la) => la.data.len(),
50        Value::StringArray(sa) => sa.data.len(),
51        Value::CharArray(ca) => ca.rows * ca.cols,
52        Value::Cell(ca) => ca.data.len(),
53        // Scalars and objects
54        _ => 1,
55    }
56}
57
58fn ndims_of_value(v: &Value) -> usize {
59    let dims = dims_of_value(v);
60    // The MATLAB language returns at least 2 for arrays/scalars
61    if dims.len() < 2 {
62        2
63    } else {
64        dims.len()
65    }
66}
67
68#[runtime_builtin(name = "size")]
69fn size_builtin(a: Value, rest: Vec<Value>) -> Result<Value, String> {
70    if rest.is_empty() {
71        let dims = dims_of_value(&a);
72        return tensor_from_dims(dims);
73    }
74    let mut dims = dims_of_value(&a);
75    let dim: f64 = (&rest[0]).try_into()?;
76    let d = if dim < 1.0 { 1usize } else { dim as usize };
77    if dims.len() < 2 {
78        dims.resize(2, 1);
79    }
80    let out = if d == 0 {
81        1.0
82    } else if d <= dims.len() {
83        dims[d - 1] as f64
84    } else {
85        1.0
86    };
87    Ok(Value::Num(out))
88}
89
90#[runtime_builtin(name = "numel")]
91fn numel_builtin(a: Value) -> Result<f64, String> {
92    Ok(numel_of_value(&a) as f64)
93}
94
95#[runtime_builtin(name = "ndims")]
96fn ndims_builtin(a: Value) -> Result<f64, String> {
97    Ok(ndims_of_value(&a) as f64)
98}
99
100#[runtime_builtin(name = "isempty")]
101fn isempty_builtin(a: Value) -> Result<bool, String> {
102    Ok(match a {
103        Value::Tensor(t) => t.data.is_empty() || t.rows == 0 || t.cols == 0,
104        Value::LogicalArray(la) => la.data.is_empty() || la.shape.contains(&0),
105        Value::StringArray(sa) => sa.data.is_empty() || sa.rows == 0 || sa.cols == 0,
106        Value::CharArray(ca) => ca.rows == 0 || ca.cols == 0,
107        Value::Cell(ca) => ca.data.is_empty() || ca.rows == 0 || ca.cols == 0,
108        _ => false,
109    })
110}
111
112#[runtime_builtin(name = "islogical")]
113fn islogical_builtin(a: Value) -> Result<bool, String> {
114    Ok(matches!(a, Value::Bool(_) | Value::LogicalArray(_)))
115}
116
117#[runtime_builtin(name = "isnumeric")]
118fn isnumeric_builtin(a: Value) -> Result<bool, String> {
119    Ok(matches!(
120        a,
121        Value::Num(_)
122            | Value::Complex(_, _)
123            | Value::Int(_)
124            | Value::Tensor(_)
125            | Value::ComplexTensor(_)
126    ))
127}
128
129#[runtime_builtin(name = "ischar")]
130fn ischar_builtin(a: Value) -> Result<bool, String> {
131    Ok(matches!(a, Value::CharArray(_)))
132}
133
134#[runtime_builtin(name = "isstring")]
135fn isstring_builtin(a: Value) -> Result<bool, String> {
136    Ok(matches!(a, Value::String(_) | Value::StringArray(_)))
137}
138
139#[runtime_builtin(name = "class")]
140fn class_builtin(a: Value) -> Result<String, String> {
141    let s = match &a {
142        Value::Num(_) | Value::Tensor(_) => "double",
143        Value::ComplexTensor(_) => "double",
144        Value::Complex(_, _) => "double", // The MATLAB language reports 'double' though value is complex
145        Value::Int(iv) => iv.class_name(),
146        Value::Bool(_) | Value::LogicalArray(_) => "logical",
147        Value::String(_) | Value::StringArray(_) => "string",
148        Value::CharArray(_) => "char",
149        Value::Cell(_) => "cell",
150        Value::Struct(_) => "struct",
151        Value::GpuTensor(_) => "gpuArray",
152        Value::FunctionHandle(_) | Value::Closure(_) => "function_handle",
153        Value::HandleObject(_) => "handle",
154        Value::Listener(_) => "listener",
155        Value::Object(o) => o.class_name.as_str(),
156        Value::ClassRef(_) => "meta.class",
157        Value::MException(_) => "MException",
158    };
159    Ok(s.to_string())
160}
161
162#[runtime_builtin(name = "isa")]
163fn isa_builtin(a: Value, type_name: String) -> Result<bool, String> {
164    let t = type_name.to_lowercase();
165    let is = match &a {
166        Value::Num(_) | Value::Tensor(_) => t == "double" || t == "numeric",
167        Value::ComplexTensor(_) => t == "double" || t == "numeric",
168        Value::Complex(_, _) => t == "double" || t == "numeric",
169        Value::Int(iv) => t == iv.class_name() || t == "numeric",
170        Value::Bool(_) | Value::LogicalArray(_) => t == "logical",
171        Value::String(_) | Value::StringArray(_) => t == "string",
172        Value::CharArray(_) => t == "char",
173        Value::Cell(_) => t == "cell",
174        Value::Struct(_) => t == "struct",
175        Value::GpuTensor(_) => t == "gpuarray",
176        Value::FunctionHandle(_) | Value::Closure(_) => t == "function_handle",
177        Value::HandleObject(_) => t == "handle",
178        Value::Listener(_) => t == "listener",
179        Value::Object(o) => t == o.class_name.to_lowercase(),
180        Value::ClassRef(_) => t == "meta.class",
181        Value::MException(_) => t == "mexception",
182    };
183    Ok(is)
184}
185
186// ---------------------------
187// Numeric predicates
188// ---------------------------
189
190fn map_tensor_to_mask<F: Fn(f64) -> bool>(t: &Tensor, pred: F) -> Result<Value, String> {
191    let data: Vec<u8> = t
192        .data
193        .iter()
194        .map(|&x| if pred(x) { 1u8 } else { 0u8 })
195        .collect();
196    let out = runmat_builtins::LogicalArray::new(data, t.shape.clone())?;
197    Ok(Value::LogicalArray(out))
198}
199
200#[runtime_builtin(name = "isnan")]
201fn isnan_builtin(a: Value) -> Result<Value, String> {
202    Ok(match a {
203        Value::Num(x) => Value::Bool(x.is_nan()),
204        Value::Int(_) => Value::Bool(false),
205        Value::Tensor(t) => return map_tensor_to_mask(&t, |x| x.is_nan()),
206        Value::LogicalArray(la) => Value::LogicalArray(la),
207        _ => Value::Bool(false),
208    })
209}
210
211#[runtime_builtin(name = "isfinite")]
212fn isfinite_builtin(a: Value) -> Result<Value, String> {
213    Ok(match a {
214        Value::Num(x) => Value::Bool(x.is_finite()),
215        Value::Int(_) => Value::Bool(true),
216        Value::Tensor(t) => return map_tensor_to_mask(&t, |x| x.is_finite()),
217        Value::LogicalArray(la) => Value::LogicalArray(la),
218        _ => Value::Bool(false),
219    })
220}
221
222#[runtime_builtin(name = "isinf")]
223fn isinf_builtin(a: Value) -> Result<Value, String> {
224    Ok(match a {
225        Value::Num(x) => Value::Bool(x.is_infinite()),
226        Value::Int(_) => Value::Bool(false),
227        Value::Tensor(t) => return map_tensor_to_mask(&t, |x| x.is_infinite()),
228        Value::LogicalArray(la) => Value::LogicalArray(la),
229        _ => Value::Bool(false),
230    })
231}
232
233// ---------------------------
234// String predicates and ops
235// ---------------------------
236
237fn extract_scalar_string(v: &Value) -> Option<String> {
238    match v {
239        Value::String(s) => Some(s.clone()),
240        Value::CharArray(ca) => Some(ca.data.iter().collect()),
241        Value::StringArray(sa) => {
242            if sa.data.len() == 1 {
243                Some(sa.data[0].clone())
244            } else {
245                None
246            }
247        }
248        _ => None,
249    }
250}
251
252#[runtime_builtin(name = "strcmp")]
253fn strcmp_builtin(a: Value, b: Value) -> Result<bool, String> {
254    let sa = extract_scalar_string(&a)
255        .ok_or_else(|| "strcmp: expected string/char scalar inputs".to_string())?;
256    let sb = extract_scalar_string(&b)
257        .ok_or_else(|| "strcmp: expected string/char scalar inputs".to_string())?;
258    Ok(sa == sb)
259}
260
261#[runtime_builtin(name = "strcmpi")]
262fn strcmpi_builtin(a: Value, b: Value) -> Result<bool, String> {
263    let sa = extract_scalar_string(&a)
264        .ok_or_else(|| "strcmpi: expected string/char scalar inputs".to_string())?;
265    let sb = extract_scalar_string(&b)
266        .ok_or_else(|| "strcmpi: expected string/char scalar inputs".to_string())?;
267    Ok(sa.eq_ignore_ascii_case(&sb))
268}
269
270fn set_from_strings(v: &Value) -> Result<Vec<String>, String> {
271    match v {
272        Value::StringArray(sa) => Ok(sa.data.clone()),
273        Value::Cell(ca) => {
274            let mut out = Vec::new();
275            for p in &ca.data {
276                let elem = &**p;
277                if let Some(s) = extract_scalar_string(elem) {
278                    out.push(s);
279                }
280            }
281            Ok(out)
282        }
283        Value::String(s) => Ok(vec![s.clone()]),
284        Value::CharArray(ca) => Ok(vec![ca.data.iter().collect()]),
285        _ => Err("ismember: expected set to be string array or cell of strings".to_string()),
286    }
287}
288
289#[runtime_builtin(name = "ismember")]
290fn ismember_strings(a: Value, set: Value) -> Result<Value, String> {
291    let set_vec = set_from_strings(&set)?;
292    match a {
293        Value::String(s) => Ok(Value::Bool(set_vec.iter().any(|x| x == &s))),
294        Value::CharArray(ca) => {
295            let s: String = ca.data.iter().collect();
296            Ok(Value::Bool(set_vec.iter().any(|x| x == &s)))
297        }
298        Value::StringArray(sa) => {
299            let mask: Vec<f64> = sa
300                .data
301                .iter()
302                .map(|s| {
303                    if set_vec.iter().any(|x| x == s) {
304                        1.0
305                    } else {
306                        0.0
307                    }
308                })
309                .collect();
310            // Return a row vector 1xN
311            let out = Tensor::new_2d(mask, 1, sa.data.len())?;
312            Ok(Value::Tensor(out))
313        }
314        _ => Err("ismember: expected string or string array as first argument".to_string()),
315    }
316}