Skip to main content

moddef_core/codec/
decode.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Point decoder (spec §8–§15), allocation-free. Port of go/codec/decode.go
4//! and moddef-ts decode.ts (incl. §10.5 selector cases with transform
5//! fallback), operating on [`PointDesc`].
6
7use crate::codec::bytes::{assemble_u64, byte_at, copy_bytes, mask_for, sign_extend};
8use crate::codec::rat::Rat;
9use crate::desc::{
10    DateTimeEncoding, PointDesc, ScaleMode, StorageType, StringPadding, StringTermination,
11    ValueKind,
12};
13use crate::error::DecodeError;
14use crate::value::Value;
15
16/// Cross-point context: integer values of scale_ref / selector_ref targets
17/// (spec §10.4/§10.5). A slice keeps it heap-free; lookups are O(n) over a
18/// handful of refs.
19#[derive(Clone, Copy, Debug, Default)]
20pub struct Ctx<'a> {
21    pub refs: &'a [(&'a str, i64)],
22}
23
24impl<'a> Ctx<'a> {
25    pub const EMPTY: Ctx<'static> = Ctx { refs: &[] };
26
27    pub fn get(&self, id: &str) -> Option<i64> {
28        self.refs.iter().find(|(k, _)| *k == id).map(|(_, v)| *v)
29    }
30}
31
32/// Decode a point's registers into a heap-free [`Value`].
33///
34/// Strings/bytes are not handled here — use [`decode_str`] / [`decode_bytes`]
35/// with a caller buffer (no_std) or the `alloc` facade.
36pub fn decode(p: &PointDesc<'_>, regs: &[u16], ctx: &Ctx<'_>) -> Result<Value, DecodeError> {
37    if regs.len() < p.words() {
38        return Err(DecodeError::ShortRead);
39    }
40    let regs = &regs[..p.words()];
41
42    // §14 composed mantissa/exponent over the window.
43    if let ValueKind::Composed {
44        base,
45        mantissa_offset,
46        mantissa_words,
47        exponent_offset,
48        exponent_words,
49    } = p.value
50    {
51        if base == 0 {
52            return Err(DecodeError::ComposedBaseZero);
53        }
54        let mant = sub_int(
55            regs,
56            mantissa_offset,
57            mantissa_words,
58            p.byte_big,
59            p.word_big,
60        );
61        let exp = sub_int(
62            regs,
63            exponent_offset,
64            exponent_words,
65            p.byte_big,
66            p.word_big,
67        );
68        let mut r = Rat::int(mant);
69        let b = Rat::int(base);
70        if exp >= 0 {
71            for _ in 0..exp.min(64) {
72                r = r.mul(b);
73            }
74        } else {
75            for _ in 0..(-exp).min(64) {
76                r = r.div(b);
77            }
78        }
79        return Ok(Value::F64(r.to_f64()));
80    }
81
82    // IEEE754 floats decode straight from the normalized byte stream.
83    if p.storage == StorageType::F32 {
84        let raw = assemble_u64(regs, p.byte_big, p.word_big) as u32;
85        return Ok(Value::F64(apply_float_scale(f32::from_bits(raw) as f64, p)));
86    }
87    if p.storage == StorageType::F64 {
88        let raw = assemble_u64(regs, p.byte_big, p.word_big);
89        return Ok(Value::F64(apply_float_scale(f64::from_bits(raw), p)));
90    }
91
92    // Integer-backed value.
93    let bits = p.storage.bits(regs.len());
94    let raw = assemble_u64(regs, p.byte_big, p.word_big) & mask_for(bits);
95
96    // §8.4 sentinel check on the masked raw integer.
97    for na in p.na {
98        if (na.raw as u64) & mask_for(bits) == raw {
99            return Ok(Value::Unavailable);
100        }
101    }
102
103    // §13.2 flags / §13 fields surface the raw window.
104    match p.value {
105        ValueKind::Flags(_) => return Ok(Value::Flags(raw)),
106        ValueKind::Fields(_) => return Ok(Value::Fields(raw)),
107        _ => {}
108    }
109
110    if p.storage == StorageType::Bcd {
111        return Ok(Value::I64(bcd_to_int(regs, p.byte_big, p.word_big)));
112    }
113
114    let signed = p.storage.signed();
115    let raw_int: i64 = if signed {
116        sign_extend(raw, bits)
117    } else {
118        raw as i64
119    };
120
121    match p.value {
122        ValueKind::Bool => Ok(Value::Bool(raw != 0)),
123        ValueKind::DateTime(enc) => Ok(Value::DateTime(match enc {
124            DateTimeEncoding::EpochSeconds | DateTimeEncoding::EpochMillis => raw as i64,
125        })),
126        ValueKind::Decimal => Ok(Value::F64(apply_scale(raw_int, signed, raw, p, ctx)?)),
127        ValueKind::Uint => Ok(Value::U64(raw)),
128        ValueKind::Int => Ok(Value::I64(raw_int)),
129        // Enum-backed and anything else integer-shaped: raw integer, signed
130        // if the storage is (parity with Go/TS default branch).
131        _ => Ok(if signed {
132            Value::I64(raw_int)
133        } else {
134            Value::U64(raw)
135        }),
136    }
137}
138
139/// Pre-scale integer view (exactness escape hatch, parity with TS
140/// `decodePointRaw`).
141pub fn decode_raw(p: &PointDesc<'_>, regs: &[u16]) -> Result<(u64, u32), DecodeError> {
142    if regs.len() < p.words() {
143        return Err(DecodeError::ShortRead);
144    }
145    let regs = &regs[..p.words()];
146    let bits = p.storage.bits(regs.len());
147    Ok((
148        assemble_u64(regs, p.byte_big, p.word_big) & mask_for(bits),
149        bits,
150    ))
151}
152
153/// Decode a string point into a caller-provided buffer (§15).
154pub fn decode_str<'b>(
155    p: &PointDesc<'_>,
156    regs: &[u16],
157    out: &'b mut [u8],
158) -> Result<&'b str, DecodeError> {
159    if regs.len() < p.words() {
160        return Err(DecodeError::ShortRead);
161    }
162    let regs = &regs[..p.words()];
163    let n = copy_bytes(regs, p.byte_big, p.word_big, out).ok_or(DecodeError::BufferTooSmall)?;
164    let (padding, termination) = match p.value {
165        ValueKind::Str {
166            padding,
167            termination,
168        } => (padding, termination),
169        _ => (StringPadding::None, StringTermination::FixedLength),
170    };
171    let mut end = n;
172    if termination == StringTermination::NullTerminated {
173        if let Some(i) = out[..n].iter().position(|&b| b == 0) {
174            end = i;
175        }
176    }
177    let pad = match padding {
178        StringPadding::Null => Some(0u8),
179        StringPadding::Space => Some(b' '),
180        StringPadding::None => None,
181    };
182    if let Some(c) = pad {
183        while end > 0 && out[end - 1] == c {
184            end -= 1;
185        }
186    }
187    core::str::from_utf8(&out[..end]).map_err(|_| DecodeError::InvalidUtf8)
188}
189
190/// Decode a BYTES_RAW point into a caller-provided buffer.
191pub fn decode_bytes<'b>(
192    p: &PointDesc<'_>,
193    regs: &[u16],
194    out: &'b mut [u8],
195) -> Result<&'b [u8], DecodeError> {
196    if regs.len() < p.words() {
197        return Err(DecodeError::ShortRead);
198    }
199    let regs = &regs[..p.words()];
200    let n = copy_bytes(regs, p.byte_big, p.word_big, out).ok_or(DecodeError::BufferTooSmall)?;
201    Ok(&out[..n])
202}
203
204/// §10 transform pipeline (static rational, scale_ref, selector cases).
205fn apply_scale(
206    raw_int: i64,
207    signed: bool,
208    raw_u: u64,
209    p: &PointDesc<'_>,
210    ctx: &Ctx<'_>,
211) -> Result<f64, DecodeError> {
212    let mut r = if signed {
213        Rat::int(raw_int)
214    } else {
215        Rat::from_u64(raw_u)
216    };
217
218    // §10.5: a matching selector case replaces the point's own transform;
219    // unresolved selector or unmatched case falls through (Go/TS parity).
220    if let Some(sel) = &p.selector {
221        if let Some(key) = ctx.get(sel.point_id) {
222            if let Some(c) = sel.cases.iter().find(|c| c.key == key) {
223                if let Some(s) = c.scale {
224                    if s.den != 0 {
225                        r = r.mul(Rat::new(s.num as i128, s.den as i128));
226                    }
227                }
228                if let Some(o) = c.offset {
229                    if o.den != 0 {
230                        r = r.add(Rat::new(o.num as i128, o.den as i128));
231                    }
232                }
233                return Ok(r.to_f64());
234            }
235        }
236    }
237
238    if let Some(sr) = &p.scale_ref {
239        let sf = ctx.get(sr.point_id).ok_or(DecodeError::UnresolvedRef)?;
240        match sr.mode {
241            ScaleMode::Pow10 => r = r.mul(Rat::pow10(sf)),
242            ScaleMode::Multiply => {
243                let den = if sr.denominator == 0 {
244                    1
245                } else {
246                    sr.denominator
247                };
248                r = r.mul(Rat::new(sf as i128, den as i128));
249            }
250        }
251    } else if let Some(s) = p.scale {
252        if s.den == 0 {
253            return Err(DecodeError::ZeroScaleDenominator);
254        }
255        r = r.mul(Rat::new(s.num as i128, s.den as i128));
256    }
257
258    if let Some(o) = p.offset_add {
259        if o.den != 0 {
260            r = r.add(Rat::new(o.num as i128, o.den as i128));
261        }
262    }
263    Ok(r.to_f64())
264}
265
266fn apply_float_scale(mut f: f64, p: &PointDesc<'_>) -> f64 {
267    if let Some(s) = p.scale {
268        if s.den != 0 {
269            f = f * s.num as f64 / s.den as f64;
270        }
271    }
272    if let Some(o) = p.offset_add {
273        if o.den != 0 {
274            f += o.num as f64 / o.den as f64;
275        }
276    }
277    f
278}
279
280/// Signed integer from a sub-window (composed mantissa/exponent, §14).
281fn sub_int(regs: &[u16], offset: u16, words: u8, byte_big: bool, word_big: bool) -> i64 {
282    let idx = offset as usize;
283    let n = if words == 0 { 1 } else { words as usize };
284    if idx + n > regs.len() {
285        return 0;
286    }
287    let slice = &regs[idx..idx + n];
288    let raw = assemble_u64(slice, byte_big, word_big);
289    sign_extend(raw & mask_for((n * 16) as u32), (n * 16) as u32)
290}
291
292fn bcd_to_int(regs: &[u16], byte_big: bool, word_big: bool) -> i64 {
293    let mut v: i64 = 0;
294    for i in 0..regs.len() * 2 {
295        let b = byte_at(regs, i, byte_big, word_big);
296        v = v * 100 + ((b >> 4) as i64) * 10 + (b & 0x0f) as i64;
297    }
298    v
299}