beamr 0.15.3

A Rust runtime with the BEAM's execution model, targeting Gleam
Documentation
//! `LitT` literal-table encoder.
//!
//! ETF-encodes each loader [`Literal`] and packs the results into the `LitT`
//! chunk body: a 4-byte literal count, then per literal a 4-byte size and the
//! version-prefixed external term. The body is emitted UNCOMPRESSED behind a
//! zero u32 size prefix — the form `decode::chunks::decode_literal_chunk`
//! reads directly and the form the committed erlc/gleam fixture corpus itself
//! uses (tear-ruled candidate C, ENC-001). The emitted bytes are a pure
//! function of the literal table: no compressor, no ambient default, no
//! configuration surface. The decode side still inflates legacy compressed
//! chunks; nothing here emits them.
//!
//! The type domain here is the loader's `Literal`, not a runtime `Term`, so the
//! encoder walks `Literal` directly. Tag encodings mirror `etf::tags`, staying
//! within the 16 tags `decode::etf` accepts on read.

use crate::etf::tags;
use crate::loader::decode::Literal;

use super::compact::AtomEncoder;
use super::container::EncodeError;

/// Encodes the module's literal table into a `LitT` chunk body. Returns `None`
/// when there are no literals (the chunk is then omitted entirely).
pub(crate) fn encode_literal_chunk(
    literals: &[Literal],
    atoms: &AtomEncoder<'_>,
) -> Result<Option<Vec<u8>>, EncodeError> {
    if literals.is_empty() {
        return Ok(None);
    }

    let mut payload = Vec::new();
    payload.extend_from_slice(&u32_len(literals.len())?.to_be_bytes());
    for literal in literals {
        let mut term = vec![tags::VERSION];
        encode_literal(&mut term, literal, atoms)?;
        payload.extend_from_slice(&u32_len(term.len())?.to_be_bytes());
        payload.extend_from_slice(&term);
    }

    // Tear-ruled candidate C (ENC-001): a zero u32 size prefix marks the body
    // as uncompressed and the raw payload follows verbatim — the exact form
    // `decode::chunks::decode_literal_chunk` reads directly. The container
    // frames chunk lengths as u32, so refuse a table too wide to round-trip
    // rather than truncating silently.
    u32_len(payload.len())?;
    let mut chunk = Vec::with_capacity(4 + payload.len());
    chunk.extend_from_slice(&0_u32.to_be_bytes());
    chunk.extend_from_slice(&payload);
    Ok(Some(chunk))
}

/// Encodes one literal as an external term (without the leading version byte).
fn encode_literal(
    out: &mut Vec<u8>,
    literal: &Literal,
    atoms: &AtomEncoder<'_>,
) -> Result<(), EncodeError> {
    match literal {
        Literal::Integer(value) => encode_integer(out, *value),
        Literal::Float(value) => {
            out.push(tags::NEW_FLOAT_EXT);
            out.extend_from_slice(&value.to_bits().to_be_bytes());
        }
        Literal::BigInteger(bytes) => encode_big_integer(out, bytes)?,
        Literal::Atom(atom) => encode_atom_name(out, atoms.resolve(*atom)?)?,
        Literal::Binary(bytes) => {
            out.push(tags::BINARY_EXT);
            out.extend_from_slice(&u32_len(bytes.len())?.to_be_bytes());
            out.extend_from_slice(bytes);
        }
        Literal::Tuple(elements) => {
            if let Ok(arity) = u8::try_from(elements.len()) {
                out.push(tags::SMALL_TUPLE_EXT);
                out.push(arity);
            } else {
                out.push(tags::LARGE_TUPLE_EXT);
                out.extend_from_slice(&u32_len(elements.len())?.to_be_bytes());
            }
            for element in elements {
                encode_literal(out, element, atoms)?;
            }
        }
        Literal::Nil => out.push(tags::NIL_EXT),
        Literal::List(elements, tail) => {
            out.push(tags::LIST_EXT);
            out.extend_from_slice(&u32_len(elements.len())?.to_be_bytes());
            for element in elements {
                encode_literal(out, element, atoms)?;
            }
            encode_literal(out, tail, atoms)?;
        }
        Literal::Map(pairs) => {
            out.push(tags::MAP_EXT);
            out.extend_from_slice(&u32_len(pairs.len())?.to_be_bytes());
            for (key, value) in pairs {
                encode_literal(out, key, atoms)?;
                encode_literal(out, value, atoms)?;
            }
        }
        Literal::String(bytes) => {
            let length = u16::try_from(bytes.len()).map_err(|_| EncodeError::ValueOutOfRange)?;
            out.push(tags::STRING_EXT);
            out.extend_from_slice(&length.to_be_bytes());
            out.extend_from_slice(bytes);
        }
        Literal::ExportFun {
            module,
            function,
            arity,
        } => {
            out.push(tags::EXPORT_EXT);
            encode_atom_name(out, atoms.resolve(*module)?)?;
            encode_atom_name(out, atoms.resolve(*function)?)?;
            encode_integer(out, i64::from(*arity));
        }
    }
    Ok(())
}

fn encode_integer(out: &mut Vec<u8>, value: i64) {
    if let Ok(byte) = u8::try_from(value) {
        out.push(tags::SMALL_INTEGER_EXT);
        out.push(byte);
    } else if let Ok(narrow) = i32::try_from(value) {
        out.push(tags::INTEGER_EXT);
        out.extend_from_slice(&narrow.to_be_bytes());
    } else {
        let negative = value.is_negative();
        let magnitude = value.unsigned_abs().to_le_bytes();
        let trimmed = trim_trailing_zeros(&magnitude);
        // A value wider than i32 fits within 8 magnitude bytes, so SMALL_BIG
        // (u8 length) always suffices here.
        out.push(tags::SMALL_BIG_EXT);
        out.push(trimmed.len() as u8);
        out.push(u8::from(negative));
        out.extend_from_slice(trimmed);
    }
}

/// Encodes a loader `BigInteger` (a sign byte followed by little-endian
/// magnitude bytes) as `SMALL_BIG_EXT` / `LARGE_BIG_EXT`. The magnitude bytes
/// are written verbatim — untrimmed — so the decoder reconstructs the identical
/// literal, including any values whose 8-byte magnitude exceeds `i64`.
fn encode_big_integer(out: &mut Vec<u8>, bytes: &[u8]) -> Result<(), EncodeError> {
    let (sign, magnitude) = bytes
        .split_first()
        .ok_or(EncodeError::MalformedBigInteger)?;
    if *sign > 1 {
        return Err(EncodeError::MalformedBigInteger);
    }
    if let Ok(length) = u8::try_from(magnitude.len()) {
        out.push(tags::SMALL_BIG_EXT);
        out.push(length);
    } else {
        out.push(tags::LARGE_BIG_EXT);
        out.extend_from_slice(&u32_len(magnitude.len())?.to_be_bytes());
    }
    out.push(*sign);
    out.extend_from_slice(magnitude);
    Ok(())
}

fn encode_atom_name(out: &mut Vec<u8>, name: &str) -> Result<(), EncodeError> {
    let bytes = name.as_bytes();
    if let Ok(length) = u8::try_from(bytes.len()) {
        out.push(tags::SMALL_ATOM_UTF8_EXT);
        out.push(length);
    } else {
        let length = u16::try_from(bytes.len()).map_err(|_| EncodeError::ValueOutOfRange)?;
        out.push(tags::ATOM_UTF8_EXT);
        out.extend_from_slice(&length.to_be_bytes());
    }
    out.extend_from_slice(bytes);
    Ok(())
}

fn trim_trailing_zeros(bytes: &[u8]) -> &[u8] {
    let end = bytes
        .iter()
        .rposition(|byte| *byte != 0)
        .map_or(1, |index| index + 1);
    &bytes[..end]
}

fn u32_len(value: usize) -> Result<u32, EncodeError> {
    u32::try_from(value).map_err(|_| EncodeError::ValueOutOfRange)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::atom::{Atom, AtomTable};
    use crate::loader::decode::budget::DecodeBudget;
    use crate::loader::decode::decode_literal_chunk;

    /// One representative literal table covering every [`Literal`] variant and
    /// every width boundary the encoder branches on. Golden-walled below —
    /// keep in lockstep with `GOLDEN_LITT_CHUNK` (see the regeneration note).
    fn representative_table(table: &AtomTable) -> (Vec<Atom>, Vec<Literal>) {
        let ok = table.intern("ok");
        let wide = table.intern(&"x".repeat(256));
        let lists = table.intern("lists");
        let map_fun = table.intern("map");
        let atoms = vec![ok, wide, lists, map_fun];

        // 2^63 as sign byte + 8-byte little-endian magnitude: word-sized but
        // beyond `i64`, so it survives decode as a `BigInteger`.
        let mut two_pow_63 = vec![0_u8; 9];
        two_pow_63[8] = 0x80;
        // 9-byte magnitude (beyond any `i64`) whose trailing zero byte the
        // encoder writes verbatim — the untrimmed-magnitude path.
        let untrimmed = vec![0_u8, 1, 2, 3, 4, 5, 6, 7, 8, 0];
        // 256-byte magnitude: wider than a u8 length, so `LARGE_BIG_EXT`.
        let mut large_magnitude = vec![1_u8]; // negative sign
        large_magnitude.extend(0..=255_u8);

        let literals = vec![
            // encode_integer: SMALL_INTEGER_EXT (fits u8) at both bounds.
            Literal::Integer(0),
            Literal::Integer(255),
            // encode_integer: INTEGER_EXT (fits i32, not u8) at the bounds.
            Literal::Integer(-1),
            Literal::Integer(256),
            Literal::Integer(i64::from(i32::MIN)),
            Literal::Integer(i64::from(i32::MAX)),
            // encode_integer: SMALL_BIG_EXT (beyond i32), both signs.
            Literal::Integer(i64::from(i32::MAX) + 1),
            Literal::Integer(i64::MIN),
            Literal::Float(-1234.5),
            // encode_big_integer: SMALL_BIG_EXT, 8-byte magnitude over i64.
            Literal::BigInteger(two_pow_63),
            // encode_big_integer: SMALL_BIG_EXT, untrimmed trailing zero.
            Literal::BigInteger(untrimmed),
            // encode_big_integer: LARGE_BIG_EXT (magnitude wider than u8).
            Literal::BigInteger(large_magnitude),
            // encode_atom_name: SMALL_ATOM_UTF8_EXT and ATOM_UTF8_EXT.
            Literal::Atom(ok),
            Literal::Atom(wide),
            Literal::Binary(vec![0xDE, 0xAD, 0xBE, 0xEF]),
            // SMALL_TUPLE_EXT, then LARGE_TUPLE_EXT (arity above u8).
            Literal::Tuple(vec![Literal::Atom(ok), Literal::Integer(7)]),
            Literal::Tuple(vec![Literal::Nil; 256]),
            Literal::Nil,
            // LIST_EXT with a non-nil tail (improper list).
            Literal::List(
                vec![Literal::Integer(1), Literal::Integer(2)],
                Box::new(Literal::Integer(3)),
            ),
            Literal::Map(vec![
                (Literal::Atom(ok), Literal::Integer(1)),
                (Literal::String(b"k".to_vec()), Literal::Nil),
            ]),
            Literal::String(b"golden".to_vec()),
            Literal::ExportFun {
                module: lists,
                function: map_fun,
                arity: 2,
            },
        ];
        (atoms, literals)
    }

    /// Golden bytes for `representative_table` in the tear-ruled candidate C
    /// zero-prefix uncompressed `LitT` form (ENC-001).
    ///
    /// REGENERATION (re-derive, never hand-edit): only a deliberate, reviewed
    /// change to the emitted form may move these bytes. To re-derive, print a
    /// fresh encode of `representative_table` — e.g. temporarily add
    /// `println!("{:?}", chunk)` in `litt_encode_matches_golden_bytes` and run
    /// `cargo test -p beamr --features encode --lib litt_encode -- --nocapture`
    /// — paste the bytes here, and put the byte-level diff in front of review.
    /// An unexplained failure of this wall is an encoder-determinism
    /// regression, never a fixture to update.
    const GOLDEN_LITT_CHUNK: &[u8] = &[
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x03, 0x83, 0x61, 0x00,
        0x00, 0x00, 0x00, 0x03, 0x83, 0x61, 0xff, 0x00, 0x00, 0x00, 0x06, 0x83, 0x62, 0xff, 0xff,
        0xff, 0xff, 0x00, 0x00, 0x00, 0x06, 0x83, 0x62, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
        0x06, 0x83, 0x62, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x83, 0x62, 0x7f, 0xff,
        0xff, 0xff, 0x00, 0x00, 0x00, 0x08, 0x83, 0x6e, 0x04, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00,
        0x00, 0x00, 0x0c, 0x83, 0x6e, 0x08, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
        0x00, 0x00, 0x00, 0x0a, 0x83, 0x46, 0xc0, 0x93, 0x4a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x0c, 0x83, 0x6e, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
        0x00, 0x00, 0x00, 0x0d, 0x83, 0x6e, 0x09, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
        0x08, 0x00, 0x00, 0x00, 0x01, 0x07, 0x83, 0x6f, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01,
        0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10,
        0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
        0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e,
        0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d,
        0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c,
        0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b,
        0x5c, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a,
        0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79,
        0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88,
        0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,
        0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6,
        0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5,
        0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4,
        0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf, 0xd0, 0xd1, 0xd2, 0xd3,
        0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf, 0xe0, 0xe1, 0xe2,
        0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, 0xf0, 0xf1,
        0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff, 0x00,
        0x00, 0x00, 0x05, 0x83, 0x77, 0x02, 0x6f, 0x6b, 0x00, 0x00, 0x01, 0x04, 0x83, 0x76, 0x01,
        0x00, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78,
        0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78,
        0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78,
        0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78,
        0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78,
        0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78,
        0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78,
        0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78,
        0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78,
        0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78,
        0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78,
        0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78,
        0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78,
        0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78,
        0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78,
        0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78,
        0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78,
        0x78, 0x78, 0x00, 0x00, 0x00, 0x0a, 0x83, 0x6d, 0x00, 0x00, 0x00, 0x04, 0xde, 0xad, 0xbe,
        0xef, 0x00, 0x00, 0x00, 0x09, 0x83, 0x68, 0x02, 0x77, 0x02, 0x6f, 0x6b, 0x61, 0x07, 0x00,
        0x00, 0x01, 0x06, 0x83, 0x69, 0x00, 0x00, 0x01, 0x00, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a,
        0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a,
        0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a,
        0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a,
        0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a,
        0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a,
        0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a,
        0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a,
        0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a,
        0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a,
        0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a,
        0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a,
        0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a,
        0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a,
        0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a,
        0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a,
        0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a,
        0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x00, 0x00, 0x00, 0x02, 0x83,
        0x6a, 0x00, 0x00, 0x00, 0x0c, 0x83, 0x6c, 0x00, 0x00, 0x00, 0x02, 0x61, 0x01, 0x61, 0x02,
        0x61, 0x03, 0x00, 0x00, 0x00, 0x11, 0x83, 0x74, 0x00, 0x00, 0x00, 0x02, 0x77, 0x02, 0x6f,
        0x6b, 0x61, 0x01, 0x6b, 0x00, 0x01, 0x6b, 0x6a, 0x00, 0x00, 0x00, 0x0a, 0x83, 0x6b, 0x00,
        0x06, 0x67, 0x6f, 0x6c, 0x64, 0x65, 0x6e, 0x00, 0x00, 0x00, 0x10, 0x83, 0x71, 0x77, 0x05,
        0x6c, 0x69, 0x73, 0x74, 0x73, 0x77, 0x03, 0x6d, 0x61, 0x70, 0x61, 0x02,
    ];

    #[test]
    fn litt_encode_matches_golden_bytes() {
        let table = AtomTable::with_common_atoms();
        let (atoms, literals) = representative_table(&table);
        let encoder = AtomEncoder::new(&atoms, &table);
        let first = encode_literal_chunk(&literals, &encoder)
            .expect("representative table encodes")
            .expect("non-empty table emits a chunk");
        let second = encode_literal_chunk(&literals, &encoder)
            .expect("representative table encodes")
            .expect("non-empty table emits a chunk");
        // Determinism by construction: same table, same bytes, every time.
        assert_eq!(first, second, "repeated encodes must be byte-identical");
        // The zero u32 size prefix marks the uncompressed form.
        assert_eq!(first[..4], [0_u8; 4], "chunk must carry the zero prefix");
        assert_eq!(
            first.len(),
            GOLDEN_LITT_CHUNK.len(),
            "emitted LitT length diverged from the golden bytes"
        );
        if let Some(index) = first
            .iter()
            .zip(GOLDEN_LITT_CHUNK)
            .position(|(a, b)| a != b)
        {
            panic!(
                "emitted LitT bytes diverge from golden at byte {index}: \
                 {:#04x} != {:#04x}",
                first[index], GOLDEN_LITT_CHUNK[index]
            );
        }
    }

    #[test]
    fn litt_golden_bytes_round_trip() {
        let table = AtomTable::with_common_atoms();
        let (_atoms, literals) = representative_table(&table);
        // Generous budget: the golden chunk is ~1 KiB with a few hundred nodes.
        let mut budget = DecodeBudget::new(64, 1 << 16, 1 << 20, 64);
        let decoded = decode_literal_chunk(GOLDEN_LITT_CHUNK, &table, &mut budget)
            .expect("golden bytes decode as an uncompressed LitT chunk");
        assert_eq!(
            decoded, literals,
            "golden bytes must decode back to the source literal table"
        );
    }
}