compactly 0.1.8

Compactly encode data types using adaptive arithmetic coding
Documentation
//! Everything lives behind the `v2` feature so that
//! `cargo check --no-default-features` (run by CI and the pre-commit
//! hook) still compiles this bin.
#[cfg(feature = "v2")]
mod imp {
    use compactly::v2::{decode, encode, Encode, EntropyCoder, EntropyDecoder};
    use std::net::{Ipv4Addr, Ipv6Addr};
    use std::time::Instant;

    fn report<T: Encode + PartialEq>(label: &str, values: &Vec<T>, raw_bytes: usize) {
        let t0 = Instant::now();
        let encoded = encode(values);
        let encode_ms = t0.elapsed().as_secs_f64() * 1000.0;

        let t1 = Instant::now();
        let decoded = decode::<Vec<T>>(&encoded).expect("decode failed");
        let decode_ms = t1.elapsed().as_secs_f64() * 1000.0;

        assert!(decoded == *values, "roundtrip mismatch");
        println!(
        "{label}: raw={raw_bytes}B encoded={}B ratio={:.2}x encode={encode_ms:.1}ms decode={decode_ms:.1}ms",
        encoded.len(),
        raw_bytes as f64 / encoded.len() as f64,
    );
    }

    #[derive(Default, Clone)]
    struct OctetCtx {
        is_zero: <bool as Encode>::Context,
        nonzero: <u8 as Encode>::Context,
    }
    impl OctetCtx {
        fn enc<E: EntropyCoder>(&mut self, byte: u8, w: &mut E) {
            let z = byte == 0;
            z.encode(w, &mut self.is_zero);
            if !z {
                byte.encode(w, &mut self.nonzero);
            }
        }
        fn dec<D: EntropyDecoder>(&mut self, r: &mut D) -> Result<u8, std::io::Error> {
            if bool::decode(r, &mut self.is_zero)? {
                Ok(0)
            } else {
                u8::decode(r, &mut self.nonzero)
            }
        }
    }

    fn encode_zsi<E: EntropyCoder>(byte: u8, w: &mut E, zctx: &mut <bool as Encode>::Context) {
        let z = byte == 0;
        z.encode(w, zctx);
        if !z {
            w.encode_incompressible_bytes(&[byte]);
        }
    }
    fn decode_zsi<D: EntropyDecoder>(
        r: &mut D,
        zctx: &mut <bool as Encode>::Context,
    ) -> Result<u8, std::io::Error> {
        if bool::decode(r, zctx)? {
            Ok(0)
        } else {
            let mut b = [0u8; 1];
            r.decode_incompressible_bytes(&mut b)?;
            Ok(b[0])
        }
    }

    // Primary2: octet 0 pure ByteCtx (never zero), octets 1–6 and 11–12 full
    // OctetCtx, octets 7–10 and 13–14 ZSI, octet 15 always incompressible.
    #[derive(Default, Clone)]
    struct Ipv6Primary2Ctx {
        oct_0: <u8 as Encode>::Context,
        oct_1_6: [OctetCtx; 6],
        oct_7_10_zsi: [<bool as Encode>::Context; 4],
        oct_11_12: [OctetCtx; 2],
        oct_13_14_zsi: [<bool as Encode>::Context; 2],
    }
    struct Ipv6Primary2([u8; 16]);
    impl PartialEq for Ipv6Primary2 {
        fn eq(&self, o: &Self) -> bool {
            self.0 == o.0
        }
    }
    impl Encode for Ipv6Primary2 {
        type Context = Ipv6Primary2Ctx;
        fn encode<E: EntropyCoder>(&self, w: &mut E, c: &mut Self::Context) {
            let o = &self.0;
            o[0].encode(w, &mut c.oct_0);
            for (i, ctx) in c.oct_1_6.iter_mut().enumerate() {
                ctx.enc(o[1 + i], w);
            }
            for (i, ctx) in c.oct_7_10_zsi.iter_mut().enumerate() {
                encode_zsi(o[7 + i], w, ctx);
            }
            for (i, ctx) in c.oct_11_12.iter_mut().enumerate() {
                ctx.enc(o[11 + i], w);
            }
            for (i, ctx) in c.oct_13_14_zsi.iter_mut().enumerate() {
                encode_zsi(o[13 + i], w, ctx);
            }
            w.encode_incompressible_bytes(&[o[15]]);
        }
        fn decode<D: EntropyDecoder>(
            r: &mut D,
            c: &mut Self::Context,
        ) -> Result<Self, std::io::Error> {
            let mut o = [0u8; 16];
            o[0] = u8::decode(r, &mut c.oct_0)?;
            for (i, ctx) in c.oct_1_6.iter_mut().enumerate() {
                o[1 + i] = ctx.dec(r)?;
            }
            for (i, ctx) in c.oct_7_10_zsi.iter_mut().enumerate() {
                o[7 + i] = decode_zsi(r, ctx)?;
            }
            for (i, ctx) in c.oct_11_12.iter_mut().enumerate() {
                o[11 + i] = ctx.dec(r)?;
            }
            for (i, ctx) in c.oct_13_14_zsi.iter_mut().enumerate() {
                o[13 + i] = decode_zsi(r, ctx)?;
            }
            r.decode_incompressible_bytes(&mut o[15..16])?;
            Ok(Ipv6Primary2(o))
        }
    }

    // Batched: all zero flags first, then adaptive bytes, then all incompressible
    // bytes in one call. Same logical encoding as Primary2, different order.
    //
    // Context layout:
    //   zero[i]  ↔ octet i+1  (i in 0..14)
    //   nz[0]    ↔ octet 0    (always non-zero)
    //   nz[1+i]  ↔ octet 1+i  (i in 0..6, adaptive non-zero)
    //   nz[7+i]  ↔ octet 11+i (i in 0..2, adaptive non-zero)
    //
    // ZSI positions in zero[]: indices 6–9 (octets 7–10) and 12–13 (octets 13–14).
    #[derive(Default, Clone)]
    struct Ipv6BatchCtx {
        zero: [<bool as Encode>::Context; 14], // octets 1–14
        nz: [<u8 as Encode>::Context; 9],      // octets 0, 1–6, 11–12
    }
    struct Ipv6Batched([u8; 16]);
    impl PartialEq for Ipv6Batched {
        fn eq(&self, o: &Self) -> bool {
            self.0 == o.0
        }
    }
    impl Encode for Ipv6Batched {
        type Context = Ipv6BatchCtx;
        fn encode<E: EntropyCoder>(&self, w: &mut E, c: &mut Self::Context) {
            let o = self.0;
            // Phase 1: all zero flags for octets 1–14
            let z: [bool; 14] = std::array::from_fn(|i| o[i + 1] == 0);
            for (zf, ctx) in z.iter().zip(c.zero.iter_mut()) {
                zf.encode(w, ctx);
            }
            // Phase 2: adaptive bytes (octet 0 always; 1–6 and 11–12 if non-zero)
            o[0].encode(w, &mut c.nz[0]);
            for i in 0..6 {
                if !z[i] {
                    o[1 + i].encode(w, &mut c.nz[1 + i]);
                }
            }
            for i in 0..2 {
                if !z[10 + i] {
                    o[11 + i].encode(w, &mut c.nz[7 + i]);
                }
            }
            // Phase 3: all incompressible bytes in one batch
            let mut buf = [0u8; 7];
            let mut n = 0;
            for i in 0..4 {
                if !z[6 + i] {
                    buf[n] = o[7 + i];
                    n += 1;
                }
            }
            for i in 0..2 {
                if !z[12 + i] {
                    buf[n] = o[13 + i];
                    n += 1;
                }
            }
            buf[n] = o[15];
            n += 1;
            w.encode_incompressible_bytes(&buf[..n]);
        }
        fn decode<D: EntropyDecoder>(
            r: &mut D,
            c: &mut Self::Context,
        ) -> Result<Self, std::io::Error> {
            // Phase 1: all zero flags for octets 1–14
            let mut z = [false; 14];
            for (zf, ctx) in z.iter_mut().zip(c.zero.iter_mut()) {
                *zf = bool::decode(r, ctx)?;
            }
            // Phase 2: adaptive bytes
            let mut o = [0u8; 16];
            o[0] = u8::decode(r, &mut c.nz[0])?;
            for i in 0..6 {
                if !z[i] {
                    o[1 + i] = u8::decode(r, &mut c.nz[1 + i])?;
                }
            }
            for i in 0..2 {
                if !z[10 + i] {
                    o[11 + i] = u8::decode(r, &mut c.nz[7 + i])?;
                }
            }
            // Phase 3: batch incompressible read
            let n = z[6..10].iter().filter(|&&z| !z).count()
                + z[12..14].iter().filter(|&&z| !z).count()
                + 1;
            let mut buf = [0u8; 7];
            r.decode_incompressible_bytes(&mut buf[..n])?;
            let mut idx = 0;
            for i in 0..4 {
                if !z[6 + i] {
                    o[7 + i] = buf[idx];
                    idx += 1;
                }
            }
            for i in 0..2 {
                if !z[12 + i] {
                    o[13 + i] = buf[idx];
                    idx += 1;
                }
            }
            o[15] = buf[idx];
            Ok(Ipv6Batched(o))
        }
    }

    pub(crate) fn run() {
        let ipv4s: Vec<Ipv4Addr> = std::fs::read_to_string("ipv4.txt")
            .expect("ipv4.txt")
            .lines()
            .filter(|l| !l.is_empty())
            .map(|l| l.trim().parse().expect("valid IPv4"))
            .collect();

        let ipv6s: Vec<Ipv6Addr> = std::fs::read_to_string("ipv6.txt")
            .expect("ipv6.txt")
            .lines()
            .filter(|l| !l.is_empty())
            .map(|l| l.trim().parse().expect("valid IPv6"))
            .collect();

        let raw4 = ipv4s.len() * 4;
        let raw6 = ipv6s.len() * 16;

        println!("── IPv4 ({} addresses) ──", ipv4s.len());
        report("  library Ipv4Addr          ", &ipv4s, raw4);

        println!("── IPv6 ({} addresses) ──", ipv6s.len());
        let v6_p2: Vec<Ipv6Primary2> = ipv6s.iter().map(|a| Ipv6Primary2(a.octets())).collect();
        report("  Primary2 (interleaved)    ", &v6_p2, raw6);
        let v6_b: Vec<Ipv6Batched> = ipv6s.iter().map(|a| Ipv6Batched(a.octets())).collect();
        report("  Batched  (flags+incomp)   ", &v6_b, raw6);
    }
}

#[cfg(feature = "v2")]
fn main() {
    imp::run()
}

#[cfg(not(feature = "v2"))]
fn main() {
    eprintln!("bench-net requires the v2 feature");
}