Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Encoder codecs

Every standard node of OpenZL v0.3.0 has an encoder in compressor::core::codecs, behind the Cargo feature of its codec. This page is the encoder-side counterpart of the decoder catalogue: it maps registry node IDs to wire codec IDs, lists their ports, and records the mechanics and deviations of each family. The engine that runs them is described in compression graphs; the graphs that wire them together in standard graphs.

Module map

graph TD
    REG["registry::nodes: STANDARD_NODES, 85 rows"] -->|"encoder: &dyn Encode"| ENC
    subgraph ENC ["compressor::core::codecs"]
        ZN["zstd_node + core::zstd"]
        CV[conversion]
        NUM["numeric/: delta, zigzag, divide_by, range_pack, bitsplit, float_deconstruct, sparse_num, merge_sorted, sentinel, partition"]
        STR["structure/: transpose, split, split_by_struct, dispatch, concat, dedup, interleave, prefix, tokenize, parse_int"]
        PK["bitpack, bitunpack, flatpack, constant, quantize, mux_lengths"]
        EN["entropy, pivco_huffman"]
        LZ["lz4/, lz/, field_lz/, lz_common/"]
        DIS["DISABLED / LEGACY"]
    end
    ZN --> MZ[mzstd]
    EN --> SE["shared::entropy + shared::stats"]
    PK --> SB[shared::bitpack_kernels]
    NUM --> SN[shared::numeric]
    LZ --> SS[shared::stats::count]

Each encoder is a FnEncoder static wrapping fn(&mut NodeCx, &[Stream]) -> Result<(), NodeError>. A registry row selects if cfg!(feature = "x") { &codecs::x::ENCODER } else { &codecs::DISABLED }; families with several features (numeric, structure) alias the single nodes to DISABLED inside their module. DISABLED fails the node with NodeError::Disabled, LEGACY (deprecated nodes 76 to 85, never reachable at v21 and newer) with NodeError::Legacy. There is no “not ported” encoder any more: every standard node runs.

The key invariants every encoder keeps:

  • The output bytes equal the reference’s for the same input, parameters and format version (tests/encode_*.rs compare node by node).
  • A node fails exactly where the C encoder fails, because permissive mode, trial compression and the store-on-expansion fallback depend on it.
  • At most one codec header per run (NodeCx::send_header); outputs are created with emit, emit_with_metadata, emit_strings, reference or reference_strings, and integer output metadata with set_output_metadata.
  • Local parameters follow ZL_Encoder_getLocalParam: NodeCx::int, ints, copy_param, ref_param (reference parameters carry Arc-shared Rust values instead of C pointers). Parser-function and callback parameters are not supported and fail the node.

Node table

Generated from registry/nodes.rs (node IDs are the registry’s, not the wire codec IDs). “Versions” is the format-version range the node exists in.

NodeNameCodecVersionsInputsOutputsFeature
2delta_int13-27NumericNumericdelta
3transpose_split411-27Structvariable Serialtranspose
4zigzag33-27NumericNumericzigzag
5dispatchN_byTag429-27SerialNumeric, Numeric; variable Serialdispatch
6float32_deconstruct334-27NumericStruct, Serialfloat-deconstruct
7bfloat16_deconstruct335-27NumericStruct, Serialfloat-deconstruct
8float16_deconstruct335-27NumericStruct, Serialfloat-deconstruct
9field_lz243-27StructStruct, Struct, Numeric, Numeric, Numericfield-lz
10convert_struct_to_serial63-27StructSerialconversion
11convert_num_to_struct_le83-27NumericStructconversion
12convert_num_to_serial_le103-27NumericSerialconversion
13convert_serial_to_struct53-27SerialStructconversion
14convert_serial_to_struct253-27SerialStructconversion
15convert_serial_to_struct453-27SerialStructconversion
16convert_serial_to_struct853-27SerialStructconversion
17convert_struct_to_num_le73-27StructNumericconversion
18convert_struct_to_num_be1321-27StructNumericconversion
19convert_serial_to_num893-27SerialNumericconversion
20convert_serial_to_num_le1693-27SerialNumericconversion
21convert_serial_to_num_le3293-27SerialNumericconversion
22convert_serial_to_num_le6493-27SerialNumericconversion
23convert_serial_to_num_be161421-27SerialNumericconversion
24convert_serial_to_num_be321421-27SerialNumericconversion
25convert_serial_to_num_be641421-27SerialNumericconversion
26separate_string_components1210-27StringSerial, Numericconversion
27bitunpack346-27SerialNumericbitunpack
28range_pack358-27NumericNumericrange-pack
29merge_sorted439-27NumericNumeric, Numericmerge-sorted
30prefix4611-27StringString, Numericprefix
31divide_by5316-27NumericNumericdivide-by
32dispatch_string5416-27StringNumeric; variable Stringdispatch
33concat_serial5516-27Serial (variable count)Numeric, Serialconcat
34concat_num5717-27Numeric (variable count)Numeric, Numericconcat
35concat_struct5817-27Struct (variable count)Numeric, Structconcat
36concat_string5918-27String (variable count)Numeric, Stringconcat
37dedup_num5616-27Numeric (variable count)Numericdedup
38parse_int6019-27StringNumericparse-int
39interleave_string6120-27String (variable count)Stringinterleave
40tokenize_struct368-27StructStruct, Numerictokenize
41tokenize_numeric378-27NumericNumeric, Numerictokenize
42tokenize_string3811-27StringString, Numerictokenize
43quantize_offsets253-27NumericNumeric, Serialquantize
44quantize_lengths263-27NumericNumeric, Serialquantize
45bitsplit_top86324-27Numericvariable Numericbitsplit
46bitsplit_fp6324-27Numericvariable Numericbitsplit
47bitsplit_bf166324-27Numericvariable Numericbitsplit
48partition6424-27NumericNumeric, Serialpartition
49split_byrange4824-27Numericvariable Numericsplit
50sentinel_byte1824-27NumericNumeric, Numericsentinel
51sentinel_num1824-27NumericNumeric, Numericsentinel
52lz1924-27SerialSerial, Numeric, Numeric, Numericlz
53mux_lengths6524-27Numeric, NumericSerial, Numericmux-lengths
54sparse_num6626-27NumericNumeric, Numericsparse-num
55sparse_num_auto6626-27NumericNumeric, Numericsparse-num
57set_string_lens1110-27SerialStringconversion
58fse_v24915-27SerialNumeric, Serialentropy
59huffman_v25015-27SerialNumeric, Serialentropy
60huffman_struct_v25115-27StructNumeric, Serialentropy
61fse_ncount5215-27NumericSerialentropy
62zstd223-27SerialSerialzstd
63bitpack_serial273-27SerialSerialbitpack
64bitpack_int283-27NumericSerialbitpack
65flatpack293-27SerialSerial, Serialflatpack
66splitN409-27Serialvariable Serialsplit
67splitN_struct4715-27Structvariable Structsplit
68splitN_num4815-27Numericvariable Numericsplit
69split_by_struct419-27Serialvariable Structsplit-by-struct
70constant_serial4411-27SerialSerialconstant
71constant_fixed4511-27StructStructconstant
72tokenize_sorted378-27NumericNumeric, Numerictokenize
73tokenize_string_sorted3811-27StringString, Numerictokenize
74dedup_num_trusted5616-27Numeric (variable count)Numericdedup
75bit_split6324-27Numericvariable Numericbitsplit
76rolz_deprecated203-12SerialSeriallegacy
77fast_lz_deprecated213-12SerialSeriallegacy
78fse_deprecated153-14SerialSeriallegacy
79huffman_deprecated163-14SerialSeriallegacy
80huffman_fixed_deprecated173-14StructSeriallegacy
81zstd_fixed_deprecated233-10StructSeriallegacy
82transpose_deprecated23-10StructStructlegacy
83transpose_split2_deprecated303-10StructSerial, Seriallegacy
84transpose_split4_deprecated313-10StructSerial, Serial, Serial, Seriallegacy
85transpose_split8_deprecated323-10StructSerial, Serial, Serial, Serial, Serial, Serial, Serial, Seriallegacy
86lz46223-27SerialSeriallz4
87pivco_huffman6727-27SerialNumeric, Serialpivco-huffman

Node 56 does not exist in the reference table. Nodes 72 and 73 (tokenize_*_sorted) share the tokenize encoders with a default local parameter that sorts the alphabet. bit_split (75) and bitsplit_top8/fp/bf16 (45 to 47) share one encoder with different splits. Numeric, struct and string conversion nodes 10 to 26 are the implicit conversions of the engine (conversion feature).

Mechanics by family

Zstandard (node 62, zstd)

  • Output stream is varint(element width) followed by the magicless Zstandard frame: mzstd writes FrameFormat::Magicless natively, so no staging or magic overwrite exists. The default-graph fast path (ZstdNode::encode) pushes [0x01] then compress_into.
  • Parameters are applied in the reference’s order: the global level, then literalCompressionMode = Disabled when the decompression level is 1, then the node’s local integer parameters (raw ZSTD_c_* IDs) through CParameter::try_from, check_memory_cap(3) and with_parameter. format (10) and contentSizeFlag (200) are refused; window, hash, chain and long-distance hash logs stop three below their maximum.
  • Block splitting (encode_elements: 2 to 8 elements of at least 1024 bytes, one flushed block per element with a pledged size) is ported and tested against the enc_zstd C oracle, but is unreachable from node 62 at v21 and newer (the registered node takes serial input only; the struct variant is v3 to 10).
  • A node with a dictionary compresses through refCDict equivalent (compress_with_dictionary_into); the block-splitting route ignores it, as C does. See dictionaries.

Conversions (conversion)

Nodes 10 to 26 and set_string_lens (57): reinterpret between serial, struct, numeric (little and big endian) and string without copying (reference), separate_string_components splits a string into serial content and numeric lengths. These are also the engine’s implicit conversions.

Numeric (numeric/)

CodecFormat summary
delta_intheader carries the first element (an empty input has 0 elements); deltas wrap at the width
zigzag(v << 1) ^ (v >> (w-1))
divide_byvarint divisor (divide_by plain / and %, no SIMD)
range_packheader: destination width and, when needed, the little-endian minimum
quantize_offsets/lengthscodes (numeric w1) plus extra bits (serial); offsets base 1<<c, lengths c < 16 literal else 1<<(c-12)
bitSplitheader: output width plus field widths, 512-element blocks, variable numeric outputs
float_deconstructheader 0/1/2 for f32/bf16/f16; sign and fraction as struct, exponent as serial
sparse_num, sparse_num_autodistances w1/2/4 and the dominant symbol in the header; auto chooses the dominant value
merge_sortedvarint per run, at most 64 runs; the engine’s graph rejects wider or longer inputs to the backup
sentinel (byte and num)exceptional values behind a sentinel; the sentinel and divisor indices travel in copy parameters
partitionheader flags (log2 width, preset, pow2 sizes) plus varint or bit-packed sizes, 1 to 256 partitions; value is base + offset; generic kernel only

Shared helpers live in shared::numeric (feature-gated by the codecs that call them; unused NUMOP_* functions are #[cfg(test)]).

Structure (structure/)

NodeNotes
tokenize (struct, numeric, string, and the *_sorted rows)alphabet plus index stream; the registry default local parameter (0, 1) selects the sort; custom tokenizer and parser functions fail the node
splitN, splitN_struct, splitN_num, split_byrangesegment sizes arrive as the reference parameter 323 (Arc<Vec<usize>>); a last size of 0 takes the rest; split_byrange reads parameter 324
split_by_structfield sizes as a reference parameter
dispatchN_byTag, dispatch_stringtags, sizes and one stream per tag, with metadata 83; dispatch_string writes the index stream first
concat_*, dedup_num (+ trusted), interleave_string, prefix, parse_intmulti-input nodes take a variable input count; parse_int accepts pre-parsed integers as a reference parameter
transpose_splitthe only transpose still written (v11 and newer)

Public helpers GraphContext::run_split_node, run_dispatch_node and run_dispatch_string_node wrap the reference-parameter forms for user graphs.

Packing

  • bitpack_serial / bitpack_int (63, 64): nbBits = 1 + highbit(max); header byte log2(width) << 6 | (nbBits - 1) plus a padding byte; values LSB first. nbBits comes from bitpack_kernels::nb_bits, an OR reduction over 64-byte fearless_simd lanes (the highest set bit of the OR is that of the maximum), dispatched once on cx.simd and folded onto the element’s byte positions. Fields of up to 16 bits are packed eight at a time: eight fields are exactly nbBits bytes, composed in a u128 and stored whole into a pre-sized buffer (encode_groups); wider fields use the one-field encode_scalar.
  • bitunpack (27): width from nbBits; header [nbBits] plus trailing bits from v7.
  • flatpack (65): sorted distinct alphabet and the indices plus a terminator; eight indices (exactly bits bytes) are composed in a u64 per store, the < 8 tail and the terminator bit are written separately.
  • constant (70, 71): one element and a varint count.
  • mux_lengths (53): multiplexes literal and match lengths; header split | bias << 4, the bias taken from the LZ metadata.
  • Kernels live in shared::bitpack_kernels (encode, decode, encode_bound, and writer::BitWriter, the port of ZS_BitCStreamFF).

Entropy and LZ

See entropy (fse_v2, fse_ncount, huffman_v2, huffman_struct_v2, pivco_huffman) and LZ (lz4, lz, field_lz, quantize, mux_lengths).

Deviations from the reference

  • divide_by and partition use the portable kernels; merge_sorted scans k-way; SIMD variants of the reference are not ported (outputs are identical).
  • Encoder-side transpose 2 and transpose_split2/4/8 are not ported: the encoder’s maximum useful version for them is 10, below the supported range.
  • Custom tokenizers and parser-function parameters are unsupported.
  • tokenize (struct and numeric) finds first occurrences with an open-addressing TokenMap (linear probing, load factor 1/2) and records each element’s id while scanning, so sorting only remaps ids to ranks; strings still use a BTreeMap.
  • transpose_split runs shared::byte_transpose::split: for widths 2, 4, 8 the elements are deinterleaved sixteen at a time with log2(W) rounds of unzip_low/high on u8x16, dispatched on cx.simd; the tail and other widths are scalar.

lz4 (HC chain walk)

The graph level 6 maps to lz4hc hash chains (32 attempts, <PATTERN = false, SWAP = false>). The hot path is Hc::insert_and_get_wider_match, which is #[inline(always)] so each call site (find_best_match, the two insert_and_get_wider_match calls of the lazy parser, the optimal parser) gets its own copy: the lookback is a constant at the first-search site, and the call needs no frame or Match return through memory (the C compiler inlines it the same way). Reads in the chain loop are single-comparison checked: peek16 tests at <= len - 2 once (a wrapped, negative position fails it and reads as -2), count bounds both 8-byte reads with limit <= len, p_match < p_in and p_in + 8 <= limit, and count_back slices [ip - 8, ip) and [matched - 8, matched) after matched <= ip <= len. Anything outside those cases falls to the unchanged count_long / count_back_long loops.

flowchart LR
    A[find_best_match / lazy parser] --> B[insert_and_get_wider_match inlined]
    B --> C[LZ4HC_Insert: hash and chain every position before ip]
    C --> D{chain walk, up to 32 attempts}
    D -->|peek16 and read32 match| E[extend: count_back + count]
    D -->|no| F[next = index - chain delta]
    E --> F
    F --> D
    D -->|done| G[Match len, off, back]

Known gaps

  • No SIMD in any encoder kernel except the XXH3 checksum; the hotpath anchors cover only the chunk writer, the zstd node and XXH3, so the new codecs have not been profiled.
  • LZ4 has only the fresh-stream, not-limited entry points (no streaming or limitedOutput modes).
  • Some shared helpers (shared::bitpack_kernels::writer, shared::stats::count and data_stats, parts of shared::estimate) carry expect(dead_code) gates for feature sets that do not use them.
  • The C prefix encoder crashes on an all-empty string input, so the oracle tests cannot cover that shape.