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    ComposedSub, DateTimeEncoding, PointDesc, ScaleMode, StorageType, StringPadding,
11    StringTermination, 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,
46        exponent,
47    } = p.value
48    {
49        if base == 0 {
50            return Err(DecodeError::ComposedBaseZero);
51        }
52        let mant = sub_int(regs, mantissa, p.byte_big, p.word_big);
53        let exp = sub_int(regs, exponent, p.byte_big, p.word_big);
54        let mut r = Rat::int(mant);
55        let b = Rat::int(base);
56        if exp >= 0 {
57            for _ in 0..exp.min(64) {
58                r = r.mul(b);
59            }
60        } else {
61            for _ in 0..(-exp).min(64) {
62                r = r.div(b);
63            }
64        }
65        return Ok(Value::F64(r.to_f64()));
66    }
67
68    // IEEE754 floats decode straight from the normalized byte stream.
69    if p.storage == StorageType::F32 {
70        let raw = assemble_u64(regs, p.byte_big, p.word_big) as u32;
71        return Ok(Value::F64(apply_float_scale(f32::from_bits(raw) as f64, p)));
72    }
73    if p.storage == StorageType::F64 {
74        let raw = assemble_u64(regs, p.byte_big, p.word_big);
75        return Ok(Value::F64(apply_float_scale(f64::from_bits(raw), p)));
76    }
77
78    // Integer-backed value.
79    let bits = p.storage.bits(regs.len());
80    let raw = assemble_u64(regs, p.byte_big, p.word_big) & mask_for(bits);
81
82    // §8.4 sentinel check on the masked raw integer.
83    for na in p.na {
84        if (na.raw as u64) & mask_for(bits) == raw {
85            return Ok(Value::Unavailable);
86        }
87    }
88
89    // §13.2 flags / §13 fields surface the raw window.
90    match p.value {
91        ValueKind::Flags(_) => return Ok(Value::Flags(raw)),
92        ValueKind::Fields(_) => return Ok(Value::Fields(raw)),
93        _ => {}
94    }
95
96    if p.storage == StorageType::Bcd {
97        return Ok(Value::I64(bcd_to_int(regs, p.byte_big, p.word_big)));
98    }
99
100    let signed = p.storage.signed();
101    let raw_int: i64 = if signed {
102        sign_extend(raw, bits)
103    } else {
104        raw as i64
105    };
106
107    match p.value {
108        ValueKind::Bool => Ok(Value::Bool(raw != 0)),
109        ValueKind::DateTime(enc) => Ok(Value::DateTime(match enc {
110            DateTimeEncoding::EpochSeconds | DateTimeEncoding::EpochMillis => raw as i64,
111        })),
112        ValueKind::Decimal => Ok(Value::F64(apply_scale(raw_int, signed, raw, p, ctx)?)),
113        ValueKind::Uint => Ok(Value::U64(raw)),
114        ValueKind::Int => Ok(Value::I64(raw_int)),
115        // Enum-backed and anything else integer-shaped: raw integer, signed
116        // if the storage is (parity with Go/TS default branch).
117        _ => Ok(if signed {
118            Value::I64(raw_int)
119        } else {
120            Value::U64(raw)
121        }),
122    }
123}
124
125/// Pre-scale integer view (exactness escape hatch, parity with TS
126/// `decodePointRaw`).
127pub fn decode_raw(p: &PointDesc<'_>, regs: &[u16]) -> Result<(u64, u32), DecodeError> {
128    if regs.len() < p.words() {
129        return Err(DecodeError::ShortRead);
130    }
131    let regs = &regs[..p.words()];
132    let bits = p.storage.bits(regs.len());
133    Ok((
134        assemble_u64(regs, p.byte_big, p.word_big) & mask_for(bits),
135        bits,
136    ))
137}
138
139/// Decode a string point into a caller-provided buffer (§15).
140pub fn decode_str<'b>(
141    p: &PointDesc<'_>,
142    regs: &[u16],
143    out: &'b mut [u8],
144) -> Result<&'b str, DecodeError> {
145    if regs.len() < p.words() {
146        return Err(DecodeError::ShortRead);
147    }
148    let regs = &regs[..p.words()];
149    let n = copy_bytes(regs, p.byte_big, p.word_big, out).ok_or(DecodeError::BufferTooSmall)?;
150    let (padding, termination) = match p.value {
151        ValueKind::Str {
152            padding,
153            termination,
154        } => (padding, termination),
155        _ => (StringPadding::None, StringTermination::FixedLength),
156    };
157    let mut end = n;
158    if termination == StringTermination::NullTerminated {
159        if let Some(i) = out[..n].iter().position(|&b| b == 0) {
160            end = i;
161        }
162    }
163    let pad = match padding {
164        StringPadding::Null => Some(0u8),
165        StringPadding::Space => Some(b' '),
166        StringPadding::None => None,
167    };
168    if let Some(c) = pad {
169        while end > 0 && out[end - 1] == c {
170            end -= 1;
171        }
172    }
173    core::str::from_utf8(&out[..end]).map_err(|_| DecodeError::InvalidUtf8)
174}
175
176/// Decode a BYTES_RAW point into a caller-provided buffer.
177pub fn decode_bytes<'b>(
178    p: &PointDesc<'_>,
179    regs: &[u16],
180    out: &'b mut [u8],
181) -> Result<&'b [u8], DecodeError> {
182    if regs.len() < p.words() {
183        return Err(DecodeError::ShortRead);
184    }
185    let regs = &regs[..p.words()];
186    let n = copy_bytes(regs, p.byte_big, p.word_big, out).ok_or(DecodeError::BufferTooSmall)?;
187    Ok(&out[..n])
188}
189
190/// §10 transform pipeline (static rational, scale_ref, selector cases).
191fn apply_scale(
192    raw_int: i64,
193    signed: bool,
194    raw_u: u64,
195    p: &PointDesc<'_>,
196    ctx: &Ctx<'_>,
197) -> Result<f64, DecodeError> {
198    let mut r = if signed {
199        Rat::int(raw_int)
200    } else {
201        Rat::from_u64(raw_u)
202    };
203
204    // §10.5: a matching selector case replaces the point's own transform;
205    // unresolved selector or unmatched case falls through (Go/TS parity).
206    if let Some(sel) = &p.selector {
207        if let Some(key) = ctx.get(sel.point_id) {
208            if let Some(c) = sel.cases.iter().find(|c| c.key == key) {
209                if let Some(s) = c.scale {
210                    if s.den != 0 {
211                        r = r.mul(Rat::new(s.num as i128, s.den as i128));
212                    }
213                }
214                if let Some(o) = c.offset {
215                    if o.den != 0 {
216                        r = r.add(Rat::new(o.num as i128, o.den as i128));
217                    }
218                }
219                return Ok(r.to_f64());
220            }
221        }
222    }
223
224    if let Some(sr) = &p.scale_ref {
225        let sf = ctx.get(sr.point_id).ok_or(DecodeError::UnresolvedRef)?;
226        match sr.mode {
227            ScaleMode::Pow10 => r = r.mul(Rat::pow10(sf)),
228            ScaleMode::Multiply => {
229                let den = if sr.denominator == 0 {
230                    1
231                } else {
232                    sr.denominator
233                };
234                r = r.mul(Rat::new(sf as i128, den as i128));
235            }
236        }
237    } else if let Some(s) = p.scale {
238        if s.den == 0 {
239            return Err(DecodeError::ZeroScaleDenominator);
240        }
241        r = r.mul(Rat::new(s.num as i128, s.den as i128));
242    }
243
244    if let Some(o) = p.offset_add {
245        if o.den != 0 {
246            r = r.add(Rat::new(o.num as i128, o.den as i128));
247        }
248    }
249    Ok(r.to_f64())
250}
251
252fn apply_float_scale(mut f: f64, p: &PointDesc<'_>) -> f64 {
253    if let Some(s) = p.scale {
254        if s.den != 0 {
255            f = f * s.num as f64 / s.den as f64;
256        }
257    }
258    if let Some(o) = p.offset_add {
259        if o.den != 0 {
260            f += o.num as f64 / o.den as f64;
261        }
262    }
263    f
264}
265
266/// Integer from a composed sub-mapping (§14). A bit window (bit_length > 0)
267/// selects [bit_offset, bit_offset+bit_length) of the assembled sub-window
268/// and sign-extends from bit_length — the §14.2 embedded decade exponent,
269/// where mantissa and exponent share a word (Iskra T5/T6, Eaton PXM).
270fn sub_int(regs: &[u16], s: ComposedSub, byte_big: bool, word_big: bool) -> i64 {
271    let idx = s.offset as usize;
272    let n = if s.words == 0 { 1 } else { s.words as usize };
273    if idx + n > regs.len() {
274        return 0;
275    }
276    let mut raw = assemble_u64(&regs[idx..idx + n], byte_big, word_big);
277    let mut bits = s.width_bits as u32;
278    if s.bit_length > 0 {
279        raw = (raw >> s.bit_offset) & mask_for(s.bit_length as u32);
280        bits = s.bit_length as u32;
281    }
282    if s.signed {
283        sign_extend(raw & mask_for(bits), bits)
284    } else {
285        (raw & mask_for(bits)) as i64
286    }
287}
288
289fn bcd_to_int(regs: &[u16], byte_big: bool, word_big: bool) -> i64 {
290    let mut v: i64 = 0;
291    for i in 0..regs.len() * 2 {
292        let b = byte_at(regs, i, byte_big, word_big);
293        v = v * 100 + ((b >> 4) as i64) * 10 + (b & 0x0f) as i64;
294    }
295    v
296}