Skip to main content

logicaffeine_data/
wire.rs

1//! Shared wire-codec core — the exact byte format of the peer/transport codec
2//! (`logicaffeine_compile::concurrency::marshal`), factored out so that TWO value models
3//! encode and decode through ONE definition:
4//!
5//!   * the interpreter's `RuntimeValue` (host side), and
6//!   * AOT-generated Rust `enum`/`struct` types (native side).
7//!
8//! Because both go through this module, the wire bytes are byte-identical by construction —
9//! not a parallel re-implementation that can drift. This is what lets a compile-once native
10//! partial evaluator receive a program as data over the *real* fast codec, at native speed.
11//!
12//! The tag bytes and field layout MUST stay in lockstep with `marshal.rs`; the
13//! `wire_bytes_match_peer_codec` test in `logicaffeine_compile` proves it.
14
15// ── Tag bytes (subset used by algebraic ASTs; identical values to marshal.rs) ──────────────
16/// `Nothing` / unit.
17pub const T_NOTHING: u8 = 0;
18/// Boolean `false`.
19pub const T_FALSE: u8 = 1;
20/// Boolean `true`.
21pub const T_TRUE: u8 = 2;
22/// A signed integer, zig-zag + LEB128 varint.
23pub const T_INT: u8 = 3;
24/// An IEEE-754 double, 8 little-endian bytes.
25pub const T_FLOAT: u8 = 4;
26/// A UTF-8 string: varint byte-length, then the bytes.
27pub const T_TEXT: u8 = 6;
28/// A heterogeneous list: varint element count, then each element (tagged).
29pub const T_LIST: u8 = 13;
30/// An inductive/enum value: `type_name` string, `constructor` string, varint arg count, args.
31pub const T_INDUCTIVE: u8 = 18;
32
33// ── Primitive stream helpers (byte-for-byte identical to marshal.rs) ───────────────────────
34
35/// LEB128 unsigned varint.
36#[inline]
37pub fn write_uvarint(mut x: u64, out: &mut Vec<u8>) {
38    while x >= 0x80 {
39        out.push((x as u8) | 0x80);
40        x >>= 7;
41    }
42    out.push(x as u8);
43}
44
45/// Inverse of [`write_uvarint`]; `None` on truncation or overlong (>64-bit) input.
46#[inline]
47pub fn read_uvarint(buf: &[u8], pos: &mut usize) -> Option<u64> {
48    let mut result = 0u64;
49    let mut shift = 0u32;
50    loop {
51        let b = *buf.get(*pos)?;
52        *pos += 1;
53        if shift >= 64 {
54            return None;
55        }
56        result |= u64::from(b & 0x7f) << shift;
57        if b & 0x80 == 0 {
58            return Some(result);
59        }
60        shift += 7;
61    }
62}
63
64/// Map a signed integer onto an unsigned one so small magnitudes stay short.
65#[inline]
66pub fn zigzag(x: i64) -> u64 {
67    ((x << 1) ^ (x >> 63)) as u64
68}
69
70/// Inverse of [`zigzag`].
71#[inline]
72pub fn unzigzag(x: u64) -> i64 {
73    ((x >> 1) as i64) ^ -((x & 1) as i64)
74}
75
76/// A length-prefixed UTF-8 string, UNtagged (used for inductive `type_name`/`constructor`).
77#[inline]
78pub fn write_str(s: &str, out: &mut Vec<u8>) {
79    write_uvarint(s.len() as u64, out);
80    out.extend_from_slice(s.as_bytes());
81}
82
83/// Inverse of [`write_str`]; `None` on truncation or invalid UTF-8.
84#[inline]
85pub fn read_str(buf: &[u8], pos: &mut usize) -> Option<String> {
86    let n = read_uvarint(buf, pos)? as usize;
87    let bytes = buf.get(*pos..pos.checked_add(n)?)?;
88    *pos += n;
89    String::from_utf8(bytes.to_vec()).ok()
90}
91
92/// Read the tag byte at `pos` and advance; `None` if it is not `expected`.
93#[inline]
94pub fn expect_tag(buf: &[u8], pos: &mut usize, expected: u8) -> Option<()> {
95    let t = *buf.get(*pos)?;
96    if t != expected {
97        return None;
98    }
99    *pos += 1;
100    Some(())
101}
102
103// ── Inductive header (the seam codegen emits into) ─────────────────────────────────────────
104
105/// Write the header of an inductive value: `T_INDUCTIVE`, its `type_name`, its `constructor`,
106/// and the argument count. The args themselves follow, each written via [`WireEncode`].
107#[inline]
108pub fn write_inductive_header(out: &mut Vec<u8>, type_name: &str, constructor: &str, nargs: u64) {
109    out.push(T_INDUCTIVE);
110    write_str(type_name, out);
111    write_str(constructor, out);
112    write_uvarint(nargs, out);
113}
114
115/// Read an inductive header, returning `(type_name, constructor, nargs)`. The caller dispatches
116/// on `constructor` and reads `nargs` arguments via [`WireDecode`].
117#[inline]
118pub fn read_inductive_header(buf: &[u8], pos: &mut usize) -> Option<(String, String, u64)> {
119    expect_tag(buf, pos, T_INDUCTIVE)?;
120    let type_name = read_str(buf, pos)?;
121    let constructor = read_str(buf, pos)?;
122    let nargs = read_uvarint(buf, pos)?;
123    Some((type_name, constructor, nargs))
124}
125
126// ── The two traits (codegen emits impls for generated types; RuntimeValue impls them host-side) ─
127
128/// A value that serializes into the shared wire format.
129pub trait WireEncode {
130    /// Append `self`'s tagged wire bytes to `out`.
131    fn wire_encode(&self, out: &mut Vec<u8>);
132}
133
134/// A value that reconstructs from the shared wire format.
135pub trait WireDecode: Sized {
136    /// Read one value starting at `*pos`, advancing `pos`. `None` on any malformed input.
137    fn wire_decode(buf: &[u8], pos: &mut usize) -> Option<Self>;
138}
139
140impl WireEncode for i64 {
141    fn wire_encode(&self, out: &mut Vec<u8>) {
142        out.push(T_INT);
143        write_uvarint(zigzag(*self), out);
144    }
145}
146impl WireDecode for i64 {
147    fn wire_decode(buf: &[u8], pos: &mut usize) -> Option<Self> {
148        expect_tag(buf, pos, T_INT)?;
149        Some(unzigzag(read_uvarint(buf, pos)?))
150    }
151}
152
153impl WireEncode for bool {
154    fn wire_encode(&self, out: &mut Vec<u8>) {
155        out.push(if *self { T_TRUE } else { T_FALSE });
156    }
157}
158impl WireDecode for bool {
159    fn wire_decode(buf: &[u8], pos: &mut usize) -> Option<Self> {
160        let t = *buf.get(*pos)?;
161        *pos += 1;
162        match t {
163            T_TRUE => Some(true),
164            T_FALSE => Some(false),
165            _ => None,
166        }
167    }
168}
169
170impl WireEncode for f64 {
171    fn wire_encode(&self, out: &mut Vec<u8>) {
172        out.push(T_FLOAT);
173        out.extend_from_slice(&self.to_le_bytes());
174    }
175}
176impl WireDecode for f64 {
177    fn wire_decode(buf: &[u8], pos: &mut usize) -> Option<Self> {
178        expect_tag(buf, pos, T_FLOAT)?;
179        let b: [u8; 8] = buf.get(*pos..pos.checked_add(8)?)?.try_into().ok()?;
180        *pos += 8;
181        Some(f64::from_le_bytes(b))
182    }
183}
184
185impl WireEncode for String {
186    fn wire_encode(&self, out: &mut Vec<u8>) {
187        out.push(T_TEXT);
188        write_str(self, out);
189    }
190}
191impl WireDecode for String {
192    fn wire_decode(buf: &[u8], pos: &mut usize) -> Option<Self> {
193        expect_tag(buf, pos, T_TEXT)?;
194        read_str(buf, pos)
195    }
196}
197
198impl<T: WireEncode + ?Sized> WireEncode for Box<T> {
199    fn wire_encode(&self, out: &mut Vec<u8>) {
200        (**self).wire_encode(out);
201    }
202}
203impl<T: WireDecode> WireDecode for Box<T> {
204    fn wire_decode(buf: &[u8], pos: &mut usize) -> Option<Self> {
205        Some(Box::new(T::wire_decode(buf, pos)?))
206    }
207}
208
209impl<T: WireEncode> WireEncode for Vec<T> {
210    fn wire_encode(&self, out: &mut Vec<u8>) {
211        out.push(T_LIST);
212        write_uvarint(self.len() as u64, out);
213        for e in self {
214            e.wire_encode(out);
215        }
216    }
217}
218impl<T: WireDecode> WireDecode for Vec<T> {
219    fn wire_decode(buf: &[u8], pos: &mut usize) -> Option<Self> {
220        expect_tag(buf, pos, T_LIST)?;
221        let n = read_uvarint(buf, pos)? as usize;
222        let mut xs = Vec::with_capacity(n.min(1024));
223        for _ in 0..n {
224            xs.push(T::wire_decode(buf, pos)?);
225        }
226        Some(xs)
227    }
228}
229
230// A `Seq of T` field codegens as `LogosSeq<T>` (the interpreter/AOT sequence type), so the
231// generated `wire_encode`/`wire_decode` need it too — same `T_LIST` bytes as `Vec<T>`.
232impl<T: WireEncode> WireEncode for crate::types::LogosSeq<T> {
233    fn wire_encode(&self, out: &mut Vec<u8>) {
234        let inner = self.0.borrow();
235        out.push(T_LIST);
236        write_uvarint(inner.len() as u64, out);
237        for e in inner.iter() {
238            e.wire_encode(out);
239        }
240    }
241}
242impl<T: WireDecode> WireDecode for crate::types::LogosSeq<T> {
243    fn wire_decode(buf: &[u8], pos: &mut usize) -> Option<Self> {
244        expect_tag(buf, pos, T_LIST)?;
245        let n = read_uvarint(buf, pos)? as usize;
246        let mut v = Vec::with_capacity(n.min(1024));
247        for _ in 0..n {
248            v.push(T::wire_decode(buf, pos)?);
249        }
250        Some(crate::types::LogosSeq::from_vec(v))
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    // A mirror of what codegen emits for an algebraic type: an enum with scalar, string,
259    // bool, float, boxed-recursive, and list fields. If THIS round-trips, the emitted
260    // impls round-trip.
261    #[derive(Debug, Clone, PartialEq)]
262    enum Tree {
263        Leaf(i64),
264        Str(String),
265        Node { tag: String, flag: bool, ratio: f64, kids: Vec<Tree>, boxed: Box<Tree> },
266        Empty,
267    }
268
269    impl WireEncode for Tree {
270        fn wire_encode(&self, out: &mut Vec<u8>) {
271            match self {
272                Tree::Leaf(v) => {
273                    write_inductive_header(out, "Tree", "Leaf", 1);
274                    v.wire_encode(out);
275                }
276                Tree::Str(s) => {
277                    write_inductive_header(out, "Tree", "Str", 1);
278                    s.wire_encode(out);
279                }
280                Tree::Node { tag, flag, ratio, kids, boxed } => {
281                    write_inductive_header(out, "Tree", "Node", 5);
282                    tag.wire_encode(out);
283                    flag.wire_encode(out);
284                    ratio.wire_encode(out);
285                    kids.wire_encode(out);
286                    boxed.wire_encode(out);
287                }
288                Tree::Empty => write_inductive_header(out, "Tree", "Empty", 0),
289            }
290        }
291    }
292    impl WireDecode for Tree {
293        fn wire_decode(buf: &[u8], pos: &mut usize) -> Option<Self> {
294            let (ty, ctor, _n) = read_inductive_header(buf, pos)?;
295            assert_eq!(ty, "Tree");
296            Some(match ctor.as_str() {
297                "Leaf" => Tree::Leaf(i64::wire_decode(buf, pos)?),
298                "Str" => Tree::Str(String::wire_decode(buf, pos)?),
299                "Node" => Tree::Node {
300                    tag: String::wire_decode(buf, pos)?,
301                    flag: bool::wire_decode(buf, pos)?,
302                    ratio: f64::wire_decode(buf, pos)?,
303                    kids: Vec::<Tree>::wire_decode(buf, pos)?,
304                    boxed: Box::<Tree>::wire_decode(buf, pos)?,
305                },
306                "Empty" => Tree::Empty,
307                _ => return None,
308            })
309        }
310    }
311
312    fn roundtrip(t: &Tree) -> Tree {
313        let mut out = Vec::new();
314        t.wire_encode(&mut out);
315        let mut pos = 0usize;
316        let back = Tree::wire_decode(&out, &mut pos).expect("decode");
317        assert_eq!(pos, out.len(), "decoder must consume every byte");
318        back
319    }
320
321    #[test]
322    fn varint_roundtrips_across_the_range() {
323        for x in [0u64, 1, 127, 128, 16383, 16384, u32::MAX as u64, u64::MAX] {
324            let mut b = Vec::new();
325            write_uvarint(x, &mut b);
326            let mut p = 0;
327            assert_eq!(read_uvarint(&b, &mut p), Some(x));
328            assert_eq!(p, b.len());
329        }
330    }
331
332    #[test]
333    fn zigzag_roundtrips_including_extremes() {
334        for x in [0i64, 1, -1, 42, -42, i64::MAX, i64::MIN] {
335            assert_eq!(unzigzag(zigzag(x)), x);
336        }
337    }
338
339    #[test]
340    fn scalar_leaves_roundtrip() {
341        for v in [0i64, 1, -1, 42, -99999, i64::MAX, i64::MIN] {
342            assert_eq!(roundtrip(&Tree::Leaf(v)), Tree::Leaf(v));
343        }
344    }
345
346    #[test]
347    fn strings_roundtrip_including_empty_and_unicode() {
348        for s in ["", "x", "hello world", "héllo, 世界! + x_1", "\n\t\"quoted\""] {
349            assert_eq!(roundtrip(&Tree::Str(s.to_string())), Tree::Str(s.to_string()));
350        }
351    }
352
353    #[test]
354    fn nested_node_with_every_field_kind_roundtrips() {
355        let t = Tree::Node {
356            tag: "root".into(),
357            flag: true,
358            ratio: 3.5,
359            kids: vec![
360                Tree::Leaf(1),
361                Tree::Str("two".into()),
362                Tree::Node {
363                    tag: "inner".into(),
364                    flag: false,
365                    ratio: -0.25,
366                    kids: vec![],
367                    boxed: Box::new(Tree::Empty),
368                },
369            ],
370            boxed: Box::new(Tree::Leaf(-7)),
371        };
372        assert_eq!(roundtrip(&t), t);
373    }
374
375    #[test]
376    fn empty_list_and_nullary_variant_roundtrip() {
377        assert_eq!(roundtrip(&Tree::Empty), Tree::Empty);
378        let t = Tree::Node { tag: "".into(), flag: false, ratio: 0.0, kids: vec![], boxed: Box::new(Tree::Empty) };
379        assert_eq!(roundtrip(&t), t);
380    }
381
382    #[test]
383    fn deep_recursion_roundtrips() {
384        let mut t = Tree::Leaf(0);
385        for i in 1..500 {
386            t = Tree::Node { tag: format!("n{i}"), flag: i % 2 == 0, ratio: i as f64, kids: vec![Tree::Leaf(i)], boxed: Box::new(t) };
387        }
388        assert_eq!(roundtrip(&t), t);
389    }
390
391    #[test]
392    fn logos_seq_roundtrips_and_matches_vec_bytes() {
393        use crate::types::LogosSeq;
394        let elems = vec![1i64, -2, 3, 0, i64::MAX];
395        let seq = LogosSeq::from_vec(elems.clone());
396
397        let mut seq_bytes = Vec::new();
398        seq.wire_encode(&mut seq_bytes);
399        let mut vec_bytes = Vec::new();
400        elems.wire_encode(&mut vec_bytes);
401        assert_eq!(seq_bytes, vec_bytes, "LogosSeq and Vec must share the T_LIST byte format");
402
403        let mut pos = 0usize;
404        let back = LogosSeq::<i64>::wire_decode(&seq_bytes, &mut pos).expect("decode");
405        assert_eq!(pos, seq_bytes.len());
406        assert_eq!(back.to_vec(), elems);
407
408        // empty
409        let empty = LogosSeq::<i64>::from_vec(vec![]);
410        let mut b = Vec::new();
411        empty.wire_encode(&mut b);
412        let mut p = 0usize;
413        assert_eq!(LogosSeq::<i64>::wire_decode(&b, &mut p).unwrap().to_vec(), Vec::<i64>::new());
414    }
415
416    #[test]
417    fn truncated_input_returns_none_never_panics() {
418        let mut out = Vec::new();
419        Tree::Leaf(123456).wire_encode(&mut out);
420        for cut in 0..out.len() {
421            let mut pos = 0usize;
422            let _ = Tree::wire_decode(&out[..cut], &mut pos); // must not panic
423        }
424    }
425}