Skip to main content

wolfram_serialize/
numeric_in.rs

1//! Flexible numeric-input helpers — accept any of NumericArray, PackedArray,
2//! or ByteArray on the wire and widen the element type into the caller's
3//! target `T`. The widening rules are lossless: a source type is accepted
4//! only when every value of its domain is exactly representable in the target.
5//!
6//! Used by the hand-written `Vec<T>` impls in [`crate::from_wolfram`] and by
7//! the field-extract code emitted by the `FromWolfram` derive macro
8//! (`VecOfNumeric` and `NumericTensor` field kinds).
9//!
10//! `ByteArray` on the wire is treated as a 1-D `NumericArray<Integer8>` before
11//! the widening rules apply.
12
13use std::convert::TryInto;
14
15use crate::complex::{Complex32, Complex64};
16use crate::constants::ExpressionEnum;
17use crate::constants::NumericArrayEnum as DT;
18use crate::reader::Reader;
19use crate::wxf::reader::WxfReader;
20use crate::Error;
21
22/// Sealed trait implemented for each numeric primitive that the WXF derive /
23/// hand-impl path can read into. Each impl knows its target [`DT`] and how to
24/// widen from any compatible source [`DT`].
25pub trait NumericTarget: Sized + Copy + 'static {
26    /// The wire type this target maps to on the canonical (no-widening) path.
27    const TARGET: DT;
28    /// Build a `Vec<Self>` from a source data-type tag plus raw little-endian
29    /// bytes. Returns `Err(message)` when the source can't widen losslessly
30    /// into `Self` (truncation, signedness change, precision loss).
31    fn widen_from(src: DT, bytes: &[u8]) -> Result<Vec<Self>, String>;
32}
33
34//==============================================================================
35// Public read helpers — generic over the reader, tag-aware (peek-free)
36//==============================================================================
37
38/// Read the next value as a flat `Vec<T>`. Accepts `NumericArray`,
39/// `PackedArray` (any rank — multi-dim flattens row-major), or `ByteArray`
40/// (treated as a 1-D `NumericArray<Integer8>`).
41pub fn read_vec<'de, T: NumericTarget, R: Reader<'de>>(
42    r: &mut WxfReader<R>,
43    path: &str,
44) -> Result<Vec<T>, Error> {
45    let tok = r.read_expr_token()?;
46    read_vec_with_tag::<T, R>(r, tok, path)
47}
48
49/// [`read_vec`] given an already-consumed expression token.
50pub fn read_vec_with_tag<'de, T: NumericTarget, R: Reader<'de>>(
51    r: &mut WxfReader<R>,
52    tok: ExpressionEnum,
53    path: &str,
54) -> Result<Vec<T>, Error> {
55    match tok {
56        ExpressionEnum::NumericArray | ExpressionEnum::PackedArray => {
57            let dt = r.read_numeric_type()?;
58            let rank = r.read_varint()? as usize;
59            let mut count = 1usize;
60            for _ in 0..rank {
61                count *= r.read_varint()? as usize;
62            }
63            let bytes = r.read_bytes(count * dt.size_in_bytes())?;
64            T::widen_from(dt, bytes)
65                .map_err(|m| err(path, "compatible numeric source", m))
66        },
67        ExpressionEnum::ByteArray => {
68            let len = r.read_varint()? as usize;
69            let bytes = r.read_bytes(len)?;
70            T::widen_from(DT::Integer8, bytes)
71                .map_err(|m| err(path, "compatible numeric source", m))
72        },
73        other => Err(err(
74            path,
75            "NumericArray, PackedArray, or ByteArray",
76            other.name().to_string(),
77        )),
78    }
79}
80
81/// Like [`read_vec`] but errors if the resulting buffer length doesn't equal `n`.
82pub fn read_fixed<'de, T: NumericTarget, R: Reader<'de>>(
83    r: &mut WxfReader<R>,
84    path: &str,
85    n: usize,
86) -> Result<Vec<T>, Error> {
87    let tok = r.read_expr_token()?;
88    read_fixed_with_tag::<T, R>(r, tok, path, n)
89}
90
91/// [`read_fixed`] given an already-consumed expression token.
92pub fn read_fixed_with_tag<'de, T: NumericTarget, R: Reader<'de>>(
93    r: &mut WxfReader<R>,
94    tok: ExpressionEnum,
95    path: &str,
96    n: usize,
97) -> Result<Vec<T>, Error> {
98    let v = read_vec_with_tag::<T, R>(r, tok, path)?;
99    if v.len() != n {
100        return Err(err(
101            path,
102            "numeric array with matching element count",
103            format!("expected {} elements, got {}", n, v.len()),
104        ));
105    }
106    Ok(v)
107}
108
109fn err(path: &str, expected: &'static str, got: String) -> Error {
110    Error::Deserialize {
111        path: path.to_string(),
112        expected: expected,
113        got: got,
114    }
115}
116
117//==============================================================================
118// Per-target widening tables
119//==============================================================================
120
121/// Little-endian element reader. Yields one `$t` per `$n`-byte chunk.
122macro_rules! make_reader {
123    ($name:ident, $t:ty, $n:expr) => {
124        #[inline]
125        fn $name(b: &[u8]) -> impl Iterator<Item = $t> + '_ {
126            b.chunks_exact($n).map(|c| {
127                let arr: [u8; $n] = c.try_into().unwrap();
128                <$t>::from_le_bytes(arr)
129            })
130        }
131    };
132}
133
134#[inline]
135fn read_i8(b: &[u8]) -> impl Iterator<Item = i8> + '_ {
136    b.iter().map(|&x| x as i8)
137}
138#[inline]
139fn read_u8(b: &[u8]) -> impl Iterator<Item = u8> + '_ {
140    b.iter().copied()
141}
142make_reader!(read_i16, i16, 2);
143make_reader!(read_i32, i32, 4);
144make_reader!(read_i64, i64, 8);
145make_reader!(read_u16, u16, 2);
146make_reader!(read_u32, u32, 4);
147make_reader!(read_u64, u64, 8);
148make_reader!(read_f32, f32, 4);
149make_reader!(read_f64, f64, 8);
150
151#[inline]
152fn read_complex32(b: &[u8]) -> impl Iterator<Item = Complex32> + '_ {
153    b.chunks_exact(8).map(|c| Complex32 {
154        re: f32::from_le_bytes(c[..4].try_into().unwrap()),
155        im: f32::from_le_bytes(c[4..].try_into().unwrap()),
156    })
157}
158#[inline]
159fn read_complex64(b: &[u8]) -> impl Iterator<Item = Complex64> + '_ {
160    b.chunks_exact(16).map(|c| Complex64 {
161        re: f64::from_le_bytes(c[..8].try_into().unwrap()),
162        im: f64::from_le_bytes(c[8..].try_into().unwrap()),
163    })
164}
165
166fn reject(src: DT, target: DT) -> String {
167    format!(
168        "cannot widen {} → {} without truncation or precision loss",
169        src.name(),
170        target.name()
171    )
172}
173
174// Each impl_target! call names the target reader explicitly. The identity case
175// just calls collect() on it — no unsafe, no memcpy, same pattern as widening.
176macro_rules! impl_target {
177    ($t:ty, $target:ident, $target_reader:ident, { $($src:ident => $reader:ident),* $(,)? }) => {
178        impl NumericTarget for $t {
179            const TARGET: DT = DT::$target;
180            fn widen_from(src: DT, bytes: &[u8]) -> Result<Vec<Self>, String> {
181                match src {
182                    DT::$target => Ok($target_reader(bytes).collect()),
183                    $(DT::$src => Ok($reader(bytes).map(<$t>::from).collect()),)*
184                    other => Err(reject(other, DT::$target)),
185                }
186            }
187        }
188    };
189}
190
191impl_target!(i8, Integer8, read_i8, {});
192impl_target!(i16, Integer16,         read_i16, { Integer8 => read_i8, UnsignedInteger8 => read_u8 });
193impl_target!(i32, Integer32,         read_i32, { Integer8 => read_i8, Integer16 => read_i16, UnsignedInteger8 => read_u8, UnsignedInteger16 => read_u16 });
194impl_target!(i64, Integer64,         read_i64, { Integer8 => read_i8, Integer16 => read_i16, Integer32 => read_i32, UnsignedInteger8 => read_u8, UnsignedInteger16 => read_u16, UnsignedInteger32 => read_u32 });
195impl_target!(u8, UnsignedInteger8, read_u8, {});
196impl_target!(u16, UnsignedInteger16, read_u16, { UnsignedInteger8 => read_u8 });
197impl_target!(u32, UnsignedInteger32, read_u32, { UnsignedInteger8 => read_u8, UnsignedInteger16 => read_u16 });
198impl_target!(u64, UnsignedInteger64, read_u64, { UnsignedInteger8 => read_u8, UnsignedInteger16 => read_u16, UnsignedInteger32 => read_u32 });
199impl_target!(f32, Real32,            read_f32, { Integer8 => read_i8, Integer16 => read_i16, UnsignedInteger8 => read_u8, UnsignedInteger16 => read_u16 });
200impl_target!(f64, Real64,            read_f64, { Integer8 => read_i8, Integer16 => read_i16, Integer32 => read_i32, UnsignedInteger8 => read_u8, UnsignedInteger16 => read_u16, UnsignedInteger32 => read_u32, Real32 => read_f32 });
201impl_target!(Complex32, ComplexReal32, read_complex32, {});
202impl_target!(Complex64, ComplexReal64, read_complex64, {});