Skip to main content

moddef_core/codec/
encode.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Point encoder (spec §10 inverse, §11.4/§11.5), allocation-free. Port of
4//! go/codec/encode.go / moddef-ts encode.ts: composed values and packed
5//! field windows stay read-oriented.
6
7use crate::codec::bytes::{mask_for, words_from_bytes, words_from_u64};
8use crate::codec::decode::Ctx;
9use crate::codec::rat::Rat;
10use crate::desc::{PointDesc, ScaleMode, StorageType, ValueKind};
11use crate::error::{ConstraintKind, EncodeError};
12use crate::value::Value;
13
14/// Encode a value into `out` registers (must be exactly `p.words()` long).
15pub fn encode(
16    p: &PointDesc<'_>,
17    v: &Value,
18    ctx: &Ctx<'_>,
19    out: &mut [u16],
20) -> Result<(), EncodeError> {
21    if out.len() < p.words() {
22        return Err(EncodeError::BufferTooSmall);
23    }
24    let out = &mut out[..p.words()];
25
26    match p.storage {
27        StorageType::Composed => return Err(EncodeError::Unsupported),
28        StorageType::F32 => {
29            let f = v.as_f64().ok_or(EncodeError::WrongValueType)? as f32;
30            words_from_u64(f.to_bits() as u64, p.byte_big, p.word_big, out);
31            return Ok(());
32        }
33        StorageType::F64 => {
34            let f = v.as_f64().ok_or(EncodeError::WrongValueType)?;
35            words_from_u64(f.to_bits(), p.byte_big, p.word_big, out);
36            return Ok(());
37        }
38        StorageType::StringAscii | StorageType::StringUtf8 | StorageType::BytesRaw => {
39            return Err(EncodeError::WrongValueType); // use encode_str / encode_bytes
40        }
41        _ => {}
42    }
43
44    let bits = p.storage.bits(out.len());
45    let raw: u64 = match (&p.value, v) {
46        (ValueKind::Flags(_), Value::Flags(mask)) => *mask & mask_for(bits),
47        (ValueKind::Fields(_), _) => return Err(EncodeError::Unsupported),
48        (ValueKind::Bool, v) => (v.as_i64().ok_or(EncodeError::WrongValueType)? != 0) as u64,
49        (ValueKind::DateTime(_), Value::DateTime(t)) => *t as u64 & mask_for(bits),
50        (ValueKind::Decimal, v) => {
51            let val = match v {
52                Value::F64(f) => Rat::from_f64(*f),
53                Value::I64(i) => Rat::int(*i),
54                Value::U64(u) => Rat::from_u64(*u),
55                _ => return Err(EncodeError::WrongValueType),
56            };
57            (encode_scaled(val, p, ctx)? as u64) & mask_for(bits)
58        }
59        (_, v) => {
60            let i = v.as_i64().ok_or(EncodeError::WrongValueType)?;
61            (i as u64) & mask_for(bits)
62        }
63    };
64
65    let raw = if p.storage == StorageType::Bcd {
66        int_to_bcd(raw as i64)
67    } else {
68        raw
69    };
70    words_from_u64(raw, p.byte_big, p.word_big, out);
71    Ok(())
72}
73
74/// Encode a string point (fixed window, §15); pads with zero bytes.
75pub fn encode_str(p: &PointDesc<'_>, s: &str, out: &mut [u16]) -> Result<(), EncodeError> {
76    if out.len() < p.words() {
77        return Err(EncodeError::BufferTooSmall);
78    }
79    words_from_bytes(s.as_bytes(), p.byte_big, p.word_big, &mut out[..p.words()]);
80    Ok(())
81}
82
83/// Encode raw bytes (BYTES_RAW).
84pub fn encode_bytes(p: &PointDesc<'_>, b: &[u8], out: &mut [u16]) -> Result<(), EncodeError> {
85    if out.len() < p.words() {
86        return Err(EncodeError::BufferTooSmall);
87    }
88    words_from_bytes(b, p.byte_big, p.word_big, &mut out[..p.words()]);
89    Ok(())
90}
91
92/// Inverse transform pipeline: raw = (value - offset) / scale.
93fn encode_scaled(mut val: Rat, p: &PointDesc<'_>, ctx: &Ctx<'_>) -> Result<i64, EncodeError> {
94    if let Some(o) = p.offset_add {
95        if o.den != 0 {
96            val = val.sub(Rat::new(o.num as i128, o.den as i128));
97        }
98    }
99    if let Some(sr) = &p.scale_ref {
100        let sf = ctx.get(sr.point_id).ok_or(EncodeError::UnresolvedRef)?;
101        match sr.mode {
102            ScaleMode::Pow10 => val = val.div(Rat::pow10(sf)),
103            ScaleMode::Multiply => {
104                let den = if sr.denominator == 0 {
105                    1
106                } else {
107                    sr.denominator
108                };
109                val = val.div(Rat::new(sf as i128, den as i128));
110            }
111        }
112    } else if let Some(s) = p.scale {
113        if s.den != 0 {
114            val = val.div(Rat::new(s.num as i128, s.den as i128));
115        }
116    }
117    Ok(val.round())
118}
119
120/// §11.4 write constraint validation in engineering units.
121pub fn validate_write(p: &PointDesc<'_>, v: &Value) -> Result<(), ConstraintKind> {
122    let Some(w) = &p.write else { return Ok(()) };
123    let num = v.as_f64();
124
125    if !w.allowed.is_empty() {
126        let iv = num.map(|f| libm_round(f) as i64);
127        match iv {
128            Some(iv) if w.allowed.contains(&iv) => return Ok(()),
129            _ => return Err(ConstraintKind::AllowedValues),
130        }
131    }
132    let Some(num) = num else { return Ok(()) };
133    if let Some(min) = w.min {
134        if min.den != 0 && num < min.num as f64 / min.den as f64 {
135            return Err(ConstraintKind::Min);
136        }
137    }
138    if let Some(max) = w.max {
139        if max.den != 0 && num > max.num as f64 / max.den as f64 {
140            return Err(ConstraintKind::Max);
141        }
142    }
143    if let Some(step) = w.step {
144        if step.den != 0 && step.num != 0 {
145            let s = step.num as f64 / step.den as f64;
146            let base = w
147                .min
148                .filter(|m| m.den != 0)
149                .map(|m| m.num as f64 / m.den as f64)
150                .unwrap_or(0.0);
151            let k = (num - base) / s;
152            if (k - libm_round(k)).abs() > 1e-9 {
153                return Err(ConstraintKind::Step);
154            }
155        }
156    }
157    Ok(())
158}
159
160fn int_to_bcd(mut v: i64) -> u64 {
161    let mut raw: u64 = 0;
162    let mut shift = 0;
163    while v > 0 {
164        raw |= ((v % 10) as u64) << shift;
165        shift += 4;
166        v /= 10;
167    }
168    raw
169}
170
171/// f64::round is not available in core on all channels historically; a tiny
172/// half-away-from-zero rounding that works in no_std.
173#[inline]
174fn libm_round(f: f64) -> f64 {
175    if f >= 0.0 {
176        (f + 0.5) as i64 as f64
177    } else {
178        (f - 0.5) as i64 as f64
179    }
180}