Skip to main content

runmat_core/
value_metadata.rs

1use runmat_builtins::{LogicalArray, SparseTensor, Value};
2
3/// MATLAB-style class name for a runtime value.
4pub fn matlab_class_name(value: &Value) -> String {
5    match value {
6        Value::Num(_) | Value::ComplexTensor(_) | Value::Complex(_, _) => "double".to_string(),
7        Value::Tensor(tensor) => tensor.dtype.class_name().to_string(),
8        Value::SparseTensor(_) => "double".to_string(),
9        Value::Int(iv) => iv.class_name().to_string(),
10        Value::Bool(_) | Value::LogicalArray(_) => "logical".to_string(),
11        Value::String(_) | Value::StringArray(_) => "string".to_string(),
12        Value::CharArray(_) => "char".to_string(),
13        Value::Symbolic(_) => "sym".to_string(),
14        Value::Cell(_) => "cell".to_string(),
15        Value::Struct(_) => "struct".to_string(),
16        Value::GpuTensor(_) => "gpuArray".to_string(),
17        Value::FunctionHandle(_)
18        | Value::ExternalFunctionHandle(_)
19        | Value::MethodFunctionHandle(_)
20        | Value::BoundFunctionHandle { .. }
21        | Value::Closure(_) => "function_handle".to_string(),
22        Value::HandleObject(handle) => {
23            if handle.class_name.is_empty() {
24                "handle".to_string()
25            } else {
26                handle.class_name.clone()
27            }
28        }
29        Value::Listener(_) => "event.listener".to_string(),
30        // Internal destructuring helper; shouldn't surface in user-facing values,
31        // but handle it defensively for completeness.
32        Value::OutputList(_) => "OutputList".to_string(),
33        Value::Object(obj) => obj.class_name.clone(),
34        Value::ClassRef(_) => "meta.class".to_string(),
35        Value::MException(_) => "MException".to_string(),
36    }
37}
38
39/// Returns the MATLAB-style shape for the provided value when applicable.
40pub fn value_shape(value: &Value) -> Option<Vec<usize>> {
41    match value {
42        Value::Num(_)
43        | Value::Int(_)
44        | Value::Bool(_)
45        | Value::Complex(_, _)
46        | Value::Symbolic(_) => Some(vec![1, 1]),
47        Value::LogicalArray(arr) => Some(arr.shape.clone()),
48        Value::StringArray(sa) => Some(sa.shape.clone()),
49        Value::String(s) => Some(vec![1, s.chars().count()]),
50        Value::CharArray(ca) => Some(vec![ca.rows, ca.cols]),
51        Value::Tensor(t) => Some(t.shape.clone()),
52        Value::SparseTensor(s) => Some(vec![s.rows, s.cols]),
53        Value::ComplexTensor(t) => Some(t.shape.clone()),
54        Value::Cell(ca) => Some(ca.shape.clone()),
55        Value::GpuTensor(handle) => Some(handle.shape.clone()),
56        Value::Object(obj) if obj.is_class("datetime") => match obj.properties.get("__serial") {
57            Some(Value::Tensor(tensor)) => Some(tensor.shape.clone()),
58            Some(Value::Num(_)) => Some(vec![1, 1]),
59            _ => None,
60        },
61        _ => None,
62    }
63}
64
65/// Returns a MATLAB dtype label for numeric values when available.
66pub fn numeric_dtype_label(value: &Value) -> Option<&'static str> {
67    match value {
68        Value::Num(_) | Value::Complex(_, _) => Some("double"),
69        Value::Tensor(t) => Some(t.dtype.class_name()),
70        Value::LogicalArray(_) => Some("logical"),
71        Value::Int(iv) => Some(iv.class_name()),
72        _ => None,
73    }
74}
75
76/// Rough estimate of the in-memory footprint for the provided value, in bytes.
77pub fn approximate_size_bytes(value: &Value) -> Option<u64> {
78    Some(match value {
79        Value::Num(_) | Value::Int(_) | Value::Complex(_, _) => 8,
80        Value::Bool(_) => 1,
81        Value::LogicalArray(arr) => arr.data.len() as u64,
82        Value::Tensor(t) => (t.data.len() * 8) as u64,
83        Value::SparseTensor(s) => sparse_tensor_memory_bytes(s),
84        Value::ComplexTensor(t) => (t.data.len() * 16) as u64,
85        Value::String(s) => s.len() as u64,
86        Value::StringArray(sa) => sa.data.iter().map(|s| s.len() as u64).sum(),
87        Value::CharArray(ca) => (ca.rows * ca.cols) as u64,
88        _ => return None,
89    })
90}
91
92/// Rough estimate of the sparse tensor storage footprint, in bytes.
93pub fn sparse_tensor_memory_bytes(sparse: &SparseTensor) -> u64 {
94    sparse_tensor_memory_bytes_from_lengths(
95        sparse.values.len(),
96        sparse.row_indices.len(),
97        sparse.col_ptrs.len(),
98    )
99}
100
101fn sparse_tensor_memory_bytes_from_lengths(
102    values_len: usize,
103    row_indices_len: usize,
104    col_ptrs_len: usize,
105) -> u64 {
106    (values_len as u64)
107        .saturating_mul(std::mem::size_of::<f64>() as u64)
108        .saturating_add(
109            (row_indices_len as u64).saturating_mul(std::mem::size_of::<usize>() as u64),
110        )
111        .saturating_add((col_ptrs_len as u64).saturating_mul(std::mem::size_of::<usize>() as u64))
112}
113
114/// Produce a numeric preview (up to `limit` elements) for scalars and dense arrays.
115pub fn preview_numeric_values(value: &Value, limit: usize) -> Option<(Vec<f64>, bool)> {
116    match value {
117        Value::Num(n) => Some((vec![*n], false)),
118        Value::Int(iv) => Some((vec![iv.to_f64()], false)),
119        Value::Bool(flag) => Some((vec![if *flag { 1.0 } else { 0.0 }], false)),
120        Value::Tensor(t) => Some(preview_f64_slice(&t.data, limit)),
121        Value::SparseTensor(s) => Some(preview_sparse_tensor(s, limit)),
122        Value::LogicalArray(arr) => Some(preview_logical_slice(arr, limit)),
123        Value::StringArray(_) | Value::String(_) | Value::CharArray(_) => None,
124        Value::ComplexTensor(_) | Value::Complex(_, _) => None,
125        Value::Cell(_)
126        | Value::Symbolic(_)
127        | Value::Struct(_)
128        | Value::Object(_)
129        | Value::HandleObject(_)
130        | Value::Listener(_)
131        | Value::OutputList(_)
132        | Value::FunctionHandle(_)
133        | Value::ExternalFunctionHandle(_)
134        | Value::MethodFunctionHandle(_)
135        | Value::BoundFunctionHandle { .. }
136        | Value::Closure(_)
137        | Value::ClassRef(_)
138        | Value::MException(_)
139        | Value::GpuTensor(_) => None,
140    }
141}
142
143fn preview_f64_slice(data: &[f64], limit: usize) -> (Vec<f64>, bool) {
144    if data.len() > limit {
145        (data[..limit].to_vec(), true)
146    } else {
147        (data.to_vec(), false)
148    }
149}
150
151fn preview_sparse_tensor(sparse: &SparseTensor, limit: usize) -> (Vec<f64>, bool) {
152    let total_len = sparse.rows.saturating_mul(sparse.cols);
153    let preview_len = total_len.min(limit);
154    let mut preview = Vec::with_capacity(preview_len);
155    if sparse.rows == 0 {
156        return (preview, false);
157    }
158    for linear_index in 0..preview_len {
159        let row = linear_index % sparse.rows;
160        let col = linear_index / sparse.rows;
161        preview.push(sparse.get(row, col).unwrap_or(0.0));
162    }
163    (preview, total_len > limit)
164}
165
166fn preview_logical_slice(arr: &LogicalArray, limit: usize) -> (Vec<f64>, bool) {
167    let truncated = arr.data.len() > limit;
168    let mut preview = Vec::with_capacity(arr.data.len().min(limit));
169    for value in arr.data.iter().take(limit) {
170        preview.push(if *value == 0 { 0.0 } else { 1.0 });
171    }
172    (preview, truncated)
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use runmat_builtins::{NumericDType, ObjectInstance, Tensor};
179
180    #[test]
181    fn approximate_size_bytes_uses_f64_width_for_integer_dtypes() {
182        // Tensor.data is always Vec<f64> (8 bytes/element) regardless of dtype.
183        let u8_tensor = Tensor::new_with_dtype(vec![1.0, 2.0, 3.0], vec![3, 1], NumericDType::U8)
184            .expect("tensor");
185        let u16_tensor = Tensor::new_with_dtype(vec![1.0, 2.0, 3.0], vec![3, 1], NumericDType::U16)
186            .expect("tensor");
187        let f32_tensor = Tensor::new_with_dtype(vec![1.0, 2.0, 3.0], vec![3, 1], NumericDType::F32)
188            .expect("tensor");
189
190        assert_eq!(approximate_size_bytes(&Value::Tensor(u8_tensor)), Some(24));
191        assert_eq!(approximate_size_bytes(&Value::Tensor(u16_tensor)), Some(24));
192        assert_eq!(approximate_size_bytes(&Value::Tensor(f32_tensor)), Some(24));
193    }
194
195    #[test]
196    fn sparse_tensor_memory_bytes_uses_saturating_arithmetic() {
197        let sparse =
198            SparseTensor::new(3, 2, vec![0, 1, 2], vec![0, 2], vec![4.0, 5.0]).expect("sparse");
199        let expected = (2 * std::mem::size_of::<f64>())
200            + (2 * std::mem::size_of::<usize>())
201            + (3 * std::mem::size_of::<usize>());
202
203        assert_eq!(sparse_tensor_memory_bytes(&sparse), expected as u64);
204        assert_eq!(
205            sparse_tensor_memory_bytes_from_lengths(usize::MAX, usize::MAX, usize::MAX),
206            u64::MAX
207        );
208    }
209
210    #[test]
211    fn datetime_object_shape_comes_from_internal_serial_tensor() {
212        let mut object = ObjectInstance::new("datetime".to_string());
213        object.properties.insert(
214            "__serial".to_string(),
215            Value::Tensor(Tensor::new(vec![739351.0, 739352.0], vec![2, 1]).expect("tensor")),
216        );
217
218        assert_eq!(value_shape(&Value::Object(object)), Some(vec![2, 1]));
219    }
220
221    #[test]
222    fn sparse_preview_uses_logical_column_major_values() {
223        let sparse = SparseTensor::new(3, 3, vec![0, 1, 1, 3], vec![1, 0, 2], vec![4.0, 5.0, 6.0])
224            .expect("sparse");
225
226        assert_eq!(
227            preview_numeric_values(&Value::SparseTensor(sparse), 9),
228            Some((vec![0.0, 4.0, 0.0, 0.0, 0.0, 0.0, 5.0, 0.0, 6.0], false))
229        );
230    }
231
232    #[test]
233    fn sparse_preview_truncates_by_logical_element_count() {
234        let sparse = SparseTensor::zeros(1000, 1000);
235
236        assert_eq!(
237            preview_numeric_values(&Value::SparseTensor(sparse), 3),
238            Some((vec![0.0, 0.0, 0.0], true))
239        );
240    }
241}