Skip to main content

runmat_runtime/execution/value_codec/
decode.rs

1use runmat_execution::value::{
2    CallableValue, DenseValue, ElementType, ExceptionValue, InlineValue, SparseValue, ValueLimits,
3    ValuePayload,
4};
5use runmat_value::{
6    CellArray, CharArray, Closure, ComplexTensor, IntValue, IntegerComplexStorage, IntegerStorage,
7    LogicalArray, MException, SparseTensor, StringArray, StructValue, Tensor, Value,
8};
9
10use super::ValueCodecError;
11
12pub fn decode_inline_value(payload: &ValuePayload) -> Result<Value, ValueCodecError> {
13    payload
14        .validate(ValueLimits::default())
15        .map_err(|error| ValueCodecError::invalid("$", error.to_string()))?;
16    decode(payload, "$")
17}
18
19fn decode(payload: &ValuePayload, path: &str) -> Result<Value, ValueCodecError> {
20    let ValuePayload::Inline(value) = payload else {
21        return Err(ValueCodecError::unsupported(
22            path,
23            "object references require an execution object-store decoder",
24        ));
25    };
26    match value.as_ref() {
27        InlineValue::Null => Err(ValueCodecError::unsupported(
28            path,
29            "MATLAB has no null runtime value",
30        )),
31        InlineValue::Logical(value) => Ok(Value::Bool(*value)),
32        InlineValue::F64Bits(value) => Ok(Value::Num(f64::from_bits(*value))),
33        InlineValue::I8(value) => Ok(Value::Int(IntValue::I8(*value))),
34        InlineValue::I16(value) => Ok(Value::Int(IntValue::I16(*value))),
35        InlineValue::I32(value) => Ok(Value::Int(IntValue::I32(*value))),
36        InlineValue::I64(value) => Ok(Value::Int(IntValue::I64(*value))),
37        InlineValue::U8(value) => Ok(Value::Int(IntValue::U8(*value))),
38        InlineValue::U16(value) => Ok(Value::Int(IntValue::U16(*value))),
39        InlineValue::U32(value) => Ok(Value::Int(IntValue::U32(*value))),
40        InlineValue::U64(value) => Ok(Value::Int(IntValue::U64(*value))),
41        InlineValue::ComplexF64Bits { real, imaginary } => Ok(Value::Complex(
42            f64::from_bits(*real),
43            f64::from_bits(*imaginary),
44        )),
45        InlineValue::String(value) => Ok(Value::String(value.clone())),
46        InlineValue::StringArray { shape, values } => Ok(Value::StringArray(
47            StringArray::new(values.clone(), usize_shape(shape, path)?)
48                .map_err(|error| ValueCodecError::invalid(path, error))?,
49        )),
50        InlineValue::Char { shape, code_points } => {
51            if shape.len() != 2 {
52                return Err(ValueCodecError::invalid(
53                    path,
54                    "char arrays require exactly two dimensions",
55                ));
56            }
57            let shape = usize_shape(shape, path)?;
58            let chars = code_points
59                .iter()
60                .map(|value| {
61                    char::from_u32(*value)
62                        .ok_or_else(|| ValueCodecError::invalid(path, "invalid Unicode scalar"))
63                })
64                .collect::<Result<_, _>>()?;
65            Ok(Value::CharArray(
66                CharArray::new(chars, shape[0], shape[1])
67                    .map_err(|error| ValueCodecError::invalid(path, error))?,
68            ))
69        }
70        InlineValue::Dense(value) => decode_dense(value, path),
71        InlineValue::Sparse(value) => decode_sparse(value, path),
72        InlineValue::Symbolic(_) => Err(ValueCodecError::unsupported(
73            path,
74            "the registered symbolic codec is unavailable in this runtime",
75        )),
76        InlineValue::Cell { shape, values } => Ok(Value::Cell(
77            CellArray::new_with_shape(
78                values
79                    .iter()
80                    .enumerate()
81                    .map(|(index, value)| decode(value, &format!("{path}[{index}]")))
82                    .collect::<Result<_, _>>()?,
83                usize_shape(shape, path)?,
84            )
85            .map_err(|error| ValueCodecError::invalid(path, error))?,
86        )),
87        InlineValue::Struct(fields) => {
88            let mut value = StructValue::new();
89            for field in fields {
90                value.insert(
91                    field.name.clone(),
92                    decode(&field.value, &format!("{path}.{}", field.name))?,
93                );
94            }
95            Ok(Value::Struct(value))
96        }
97        InlineValue::OutputList(values) => Ok(Value::OutputList(
98            values
99                .iter()
100                .enumerate()
101                .map(|(index, value)| decode(value, &format!("{path}[{index}]")))
102                .collect::<Result<_, _>>()?,
103        )),
104        InlineValue::Exception(value) => decode_exception(value, path),
105        InlineValue::Callable(value) => decode_callable(value, path),
106        InlineValue::ImmutableValueClass(_) => Err(ValueCodecError::unsupported(
107            path,
108            "the registered immutable value-class codec is unavailable in this runtime",
109        )),
110    }
111}
112
113fn decode_dense(value: &DenseValue, path: &str) -> Result<Value, ValueCodecError> {
114    let shape = usize_shape(&value.shape, path)?;
115    match value.element_type {
116        ElementType::Logical => Ok(Value::LogicalArray(
117            LogicalArray::new(value.little_endian_data.clone(), shape)
118                .map_err(|error| ValueCodecError::invalid(path, error))?,
119        )),
120        ElementType::F32 => Ok(Value::Tensor(
121            Tensor::from_f32(decode_f32(&value.little_endian_data, path)?, shape)
122                .map_err(|error| ValueCodecError::invalid(path, error))?,
123        )),
124        ElementType::F64 => Ok(Value::Tensor(
125            Tensor::new(decode_f64(&value.little_endian_data, path)?, shape)
126                .map_err(|error| ValueCodecError::invalid(path, error))?,
127        )),
128        ElementType::ComplexF64 => {
129            let words = decode_words::<8>(&value.little_endian_data, path)?;
130            let data = words
131                .chunks_exact(2)
132                .map(|pair| (f64::from_bits(pair[0]), f64::from_bits(pair[1])))
133                .collect();
134            Ok(Value::ComplexTensor(
135                ComplexTensor::new(data, shape)
136                    .map_err(|error| ValueCodecError::invalid(path, error))?,
137            ))
138        }
139        ElementType::ComplexF32 => {
140            let words = decode_arrays::<4>(&value.little_endian_data, path)?;
141            let data = words
142                .chunks_exact(2)
143                .map(|pair| {
144                    (
145                        f32::from_bits(u32::from_le_bytes(pair[0])),
146                        f32::from_bits(u32::from_le_bytes(pair[1])),
147                    )
148                })
149                .collect();
150            Ok(Value::ComplexTensor(
151                ComplexTensor::from_f32(data, shape)
152                    .map_err(|error| ValueCodecError::invalid(path, error))?,
153            ))
154        }
155        element_type if complex_integer_element_type(element_type).is_some() => {
156            let (scalar_type, width) = complex_integer_element_type(element_type)
157                .expect("guarded complex integer element type");
158            let pair_width = width * 2;
159            if !value.little_endian_data.len().is_multiple_of(pair_width) {
160                return Err(ValueCodecError::invalid(
161                    path,
162                    "complex integer byte length is not element aligned",
163                ));
164            }
165            let mut real = Vec::with_capacity(value.little_endian_data.len() / 2);
166            let mut imag = Vec::with_capacity(value.little_endian_data.len() / 2);
167            for pair in value.little_endian_data.chunks_exact(pair_width) {
168                real.extend_from_slice(&pair[..width]);
169                imag.extend_from_slice(&pair[width..]);
170            }
171            let storage = IntegerComplexStorage::new(
172                decode_integer_storage(scalar_type, &real, path)?,
173                decode_integer_storage(scalar_type, &imag, path)?,
174            )
175            .map_err(|error| ValueCodecError::invalid(path, error))?;
176            Ok(Value::ComplexTensor(
177                ComplexTensor::new_integer(storage, shape)
178                    .map_err(|error| ValueCodecError::invalid(path, error))?,
179            ))
180        }
181        element_type => Ok(Value::Tensor(
182            Tensor::new_integer(
183                decode_integer_storage(element_type, &value.little_endian_data, path)?,
184                shape,
185            )
186            .map_err(|error| ValueCodecError::invalid(path, error))?,
187        )),
188    }
189}
190
191fn decode_sparse(value: &SparseValue, path: &str) -> Result<Value, ValueCodecError> {
192    let rows = usize::try_from(value.rows)
193        .map_err(|_| ValueCodecError::invalid(path, "row count exceeds platform limits"))?;
194    let columns = usize::try_from(value.columns)
195        .map_err(|_| ValueCodecError::invalid(path, "column count exceeds platform limits"))?;
196    let column_offsets = usize_indices(&value.column_offsets, path)?;
197    let row_indices = usize_indices(&value.row_indices, path)?;
198    let sparse = match value.element_type {
199        ElementType::F64 => SparseTensor::new(
200            rows,
201            columns,
202            column_offsets,
203            row_indices,
204            decode_f64(&value.little_endian_data, path)?,
205        ),
206        ElementType::F32 => SparseTensor::new_f32(
207            rows,
208            columns,
209            column_offsets,
210            row_indices,
211            decode_f32(&value.little_endian_data, path)?,
212        ),
213        ElementType::Logical
214        | ElementType::ComplexF64
215        | ElementType::ComplexF32
216        | ElementType::ComplexI8
217        | ElementType::ComplexI16
218        | ElementType::ComplexI32
219        | ElementType::ComplexI64
220        | ElementType::ComplexU8
221        | ElementType::ComplexU16
222        | ElementType::ComplexU32
223        | ElementType::ComplexU64 => {
224            return Err(ValueCodecError::unsupported(
225                path,
226                "the runtime has no matching sparse value class",
227            ))
228        }
229        element_type => SparseTensor::new_integer(
230            rows,
231            columns,
232            column_offsets,
233            row_indices,
234            decode_integer_storage(element_type, &value.little_endian_data, path)?,
235        ),
236    }
237    .map_err(|error| ValueCodecError::invalid(path, error))?;
238    Ok(Value::SparseTensor(sparse))
239}
240
241fn complex_integer_element_type(element_type: ElementType) -> Option<(ElementType, usize)> {
242    Some(match element_type {
243        ElementType::ComplexI8 => (ElementType::I8, 1),
244        ElementType::ComplexI16 => (ElementType::I16, 2),
245        ElementType::ComplexI32 => (ElementType::I32, 4),
246        ElementType::ComplexI64 => (ElementType::I64, 8),
247        ElementType::ComplexU8 => (ElementType::U8, 1),
248        ElementType::ComplexU16 => (ElementType::U16, 2),
249        ElementType::ComplexU32 => (ElementType::U32, 4),
250        ElementType::ComplexU64 => (ElementType::U64, 8),
251        _ => return None,
252    })
253}
254
255fn decode_integer_storage(
256    element_type: ElementType,
257    bytes: &[u8],
258    path: &str,
259) -> Result<IntegerStorage, ValueCodecError> {
260    Ok(match element_type {
261        ElementType::I8 => IntegerStorage::I8(bytes.iter().map(|value| *value as i8).collect()),
262        ElementType::I16 => {
263            IntegerStorage::I16(decode_signed::<2, i16>(bytes, path, i16::from_le_bytes)?)
264        }
265        ElementType::I32 => {
266            IntegerStorage::I32(decode_signed::<4, i32>(bytes, path, i32::from_le_bytes)?)
267        }
268        ElementType::I64 => {
269            IntegerStorage::I64(decode_signed::<8, i64>(bytes, path, i64::from_le_bytes)?)
270        }
271        ElementType::U8 => IntegerStorage::U8(bytes.to_vec()),
272        ElementType::U16 => {
273            IntegerStorage::U16(decode_signed::<2, u16>(bytes, path, u16::from_le_bytes)?)
274        }
275        ElementType::U32 => {
276            IntegerStorage::U32(decode_signed::<4, u32>(bytes, path, u32::from_le_bytes)?)
277        }
278        ElementType::U64 => {
279            IntegerStorage::U64(decode_signed::<8, u64>(bytes, path, u64::from_le_bytes)?)
280        }
281        _ => {
282            return Err(ValueCodecError::invalid(
283                path,
284                "element type is not an integer",
285            ))
286        }
287    })
288}
289
290fn decode_signed<const N: usize, T>(
291    bytes: &[u8],
292    path: &str,
293    decode: fn([u8; N]) -> T,
294) -> Result<Vec<T>, ValueCodecError> {
295    decode_arrays(bytes, path)?
296        .into_iter()
297        .map(|word| Ok(decode(word)))
298        .collect()
299}
300
301fn decode_f32(bytes: &[u8], path: &str) -> Result<Vec<f32>, ValueCodecError> {
302    Ok(decode_arrays::<4>(bytes, path)?
303        .into_iter()
304        .map(|word| f32::from_bits(u32::from_le_bytes(word)))
305        .collect())
306}
307
308fn decode_f64(bytes: &[u8], path: &str) -> Result<Vec<f64>, ValueCodecError> {
309    Ok(decode_arrays::<8>(bytes, path)?
310        .into_iter()
311        .map(|word| f64::from_bits(u64::from_le_bytes(word)))
312        .collect())
313}
314
315fn decode_words<const N: usize>(bytes: &[u8], path: &str) -> Result<Vec<u64>, ValueCodecError> {
316    if N != 8 {
317        return Err(ValueCodecError::invalid(path, "invalid word width"));
318    }
319    Ok(decode_arrays::<N>(bytes, path)?
320        .into_iter()
321        .map(|word| {
322            let mut full = [0_u8; 8];
323            full.copy_from_slice(&word);
324            u64::from_le_bytes(full)
325        })
326        .collect())
327}
328
329fn decode_arrays<const N: usize>(
330    bytes: &[u8],
331    path: &str,
332) -> Result<Vec<[u8; N]>, ValueCodecError> {
333    if !bytes.len().is_multiple_of(N) {
334        return Err(ValueCodecError::invalid(
335            path,
336            "encoded data is not aligned to its element width",
337        ));
338    }
339    Ok(bytes
340        .chunks_exact(N)
341        .map(|chunk| chunk.try_into().expect("chunks have exact width"))
342        .collect())
343}
344
345fn decode_exception(value: &ExceptionValue, path: &str) -> Result<Value, ValueCodecError> {
346    if !value.causes.is_empty() {
347        return Err(ValueCodecError::unsupported(
348            path,
349            "nested exception causes are not represented by the current runtime MException",
350        ));
351    }
352    Ok(Value::MException(MException {
353        identifier: value.identifier.clone(),
354        message: value.message.clone(),
355        stack: value.stack.clone(),
356    }))
357}
358
359fn decode_callable(value: &CallableValue, path: &str) -> Result<Value, ValueCodecError> {
360    value
361        .validate_identity()
362        .map_err(|error| ValueCodecError::invalid(path, error.to_string()))?;
363    let captures = value
364        .captures
365        .iter()
366        .enumerate()
367        .map(|(index, value)| decode(value, &format!("{path}.capture[{index}]")))
368        .collect::<Result<Vec<_>, _>>()?;
369    let bound_function = value
370        .owner_identity
371        .strip_prefix("bound:")
372        .map(|value| {
373            value
374                .parse()
375                .map_err(|_| ValueCodecError::invalid(path, "invalid bound callable identity"))
376        })
377        .transpose()?;
378    if !captures.is_empty() {
379        return Ok(Value::Closure(Closure {
380            function_name: value.qualified_name.clone(),
381            bound_function,
382            captures,
383        }));
384    }
385    match value.owner_identity.as_str() {
386        "workspace" => Ok(Value::FunctionHandle(value.qualified_name.clone())),
387        "external" => Ok(Value::ExternalFunctionHandle(value.qualified_name.clone())),
388        "method" => Ok(Value::MethodFunctionHandle(value.qualified_name.clone())),
389        _ if bound_function.is_some() => Ok(Value::BoundFunctionHandle {
390            name: value.qualified_name.clone(),
391            function: bound_function.expect("checked above"),
392        }),
393        _ => Err(ValueCodecError::unsupported(
394            path,
395            "callable owner is not available in this runtime",
396        )),
397    }
398}
399
400fn usize_shape(shape: &[u64], path: &str) -> Result<Vec<usize>, ValueCodecError> {
401    usize_indices(shape, path)
402}
403
404fn usize_indices(values: &[u64], path: &str) -> Result<Vec<usize>, ValueCodecError> {
405    values
406        .iter()
407        .map(|value| {
408            usize::try_from(*value)
409                .map_err(|_| ValueCodecError::invalid(path, "value exceeds platform limits"))
410        })
411        .collect()
412}