QUIC
QUIC is an excellent wirespec showcase. Its variable-length integer encoding (VarInt) exercises computed types and bits[N] matches. Its frame format exercises nearly every wirespec field kind: optional fields, derived fields, arrays, and bytes[remaining]. Together, varint.wspec and frames.wspec cover most of the language.
VarInt
QUIC uses a 2-bit prefix to encode integers of 6, 14, 30, or 62 bits. A VarInt occupies 1, 2, 4, or 8 bytes on the wire depending on the magnitude of the value.
module quic.varint
@strict
type VarInt = {
prefix: bits[2],
value: match prefix {
0b00 => bits[6],
0b01 => bits[14],
0b10 => bits[30],
0b11 => bits[62],
},
}Features demonstrated:
| Feature | Where |
|---|---|
| Computed type | type VarInt = { ... } with braces |
bits[N] sub-byte field | prefix: bits[2] |
match on a field | match prefix { 0b00 => ... } |
| Binary literals | 0b00, 0b01, 0b10, 0b11 |
@strict annotation | Non-canonical encodings rejected at parse time |
How it works. The prefix field occupies the top 2 bits of the first byte. The match dispatches on its value to determine how many bits follow. 0b00 means 6 more bits in the same byte (total 1 byte). 0b11 means 62 more bits across 7 more bytes (total 8 bytes).
@strict and canonical encoding. RFC 9000 requires that VarInt values use the shortest possible encoding. The @strict annotation enforces this: if the parser reads a value that could have been encoded in fewer bytes, it returns WIRESPEC_ERR_NONCANONICAL instead of silently accepting it. Without @strict, non-canonical encodings are accepted.
Compile:
wirespec compile examples/quic/varint.wspec -o build/Generated C API:
wirespec_result_t quic_varint_var_int_parse(
const uint8_t *buf, size_t len,
quic_varint_var_int_t *out, size_t *consumed);
wirespec_result_t quic_varint_var_int_serialize(
const quic_varint_var_int_t *in,
uint8_t *buf, size_t cap, size_t *written);The generated struct holds both the raw prefix and value fields. The serializer reconstructs the prefix from the value magnitude automatically.
RFC 9000 Appendix A test vectors. The wirespec test suite for VarInt includes all vectors from RFC 9000 Appendix A, covering each encoding width and boundary values.
QUIC Frames
QUIC frames use a VarInt tag to dispatch to over a dozen frame types. Each frame type has its own field layout, and many use optional fields, derived fields, and length-delimited byte arrays.
module quic.frames
@endian big
import quic.varint.VarInt
const MAX_CID_LENGTH: u8 = 20
packet LengthPrefixedCid {
length: u8,
value: bytes[length],
require length <= MAX_CID_LENGTH,
}
frame QuicFrame = match frame_type: VarInt {
0x00 => Padding {},
0x01 => Ping {},
0x02..=0x03 => Ack {
largest_ack: VarInt,
ack_delay: VarInt,
ack_range_count: VarInt,
first_ack_range: VarInt,
ack_ranges: [AckRange; ack_range_count],
ecn_counts: if frame_type == 0x03 { EcnCounts },
},
0x06 => Crypto {
offset: VarInt,
length: VarInt,
data: bytes[length: length],
},
0x08..=0x0f => Stream {
stream_id: VarInt,
offset_raw: if frame_type & 0x04 { VarInt },
length_raw: if frame_type & 0x02 { VarInt },
data: bytes[length_or_remaining: length_raw],
let offset: u64 = offset_raw ?? 0,
let fin: bool = (frame_type & 0x01) != 0,
},
0x18 => NewConnectionId {
sequence: VarInt,
retire_prior: VarInt,
cid_length: u8,
cid: bytes[cid_length],
reset_token: bytes[16],
},
0x1c..=0x1d => ConnectionClose {
error_code: VarInt,
offending_frame_type: if frame_type == 0x1c { VarInt },
reason_length: VarInt,
reason_phrase: bytes[reason_length: reason_length],
},
0x1e => HandshakeDone {},
0x30..=0x31 => Datagram {
length: if frame_type & 0x01 { VarInt },
data: bytes[length_or_remaining: length],
},
_ => Unknown { data: bytes[remaining] },
}
packet AckRange { gap: VarInt, ack_range: VarInt }
packet EcnCounts { ect0: VarInt, ect1: VarInt, ecn_ce: VarInt }Features demonstrated:
| Feature | Where |
|---|---|
frame tagged union | frame QuicFrame = match frame_type: VarInt { ... } |
| Import | import quic.varint.VarInt |
const | const MAX_CID_LENGTH: u8 = 20 |
| Pattern ranges | 0x02..=0x03, 0x08..=0x0f, 0x1c..=0x1d, 0x30..=0x31 |
| Empty frames | Padding {}, Ping {}, HandshakeDone {} |
| Arrays | [AckRange; ack_range_count] |
| Optional fields | if COND { T } |
| Bitwise condition | if frame_type & 0x04 { VarInt } |
bytes[length: EXPR] | data: bytes[length: length] |
bytes[length_or_remaining: EXPR] | data: bytes[length_or_remaining: length_raw] |
bytes[remaining] | Unknown { data: bytes[remaining] } |
| Fixed-length bytes | reset_token: bytes[16] |
Derived fields (let) | let offset: u64 = offset_raw ?? 0 |
Coalesce operator (??) | offset_raw ?? 0 |
require + const | require length <= MAX_CID_LENGTH |
| Wildcard branch | _ => Unknown { ... } |
Pattern Ranges
A single frame branch can handle a range of tag values using ..= (inclusive):
0x02..=0x03 => Ack { ... }This matches both 0x02 (ACK without ECN) and 0x03 (ACK with ECN). Inside the branch, frame_type still holds the exact value, so the ECN field can be conditioned on frame_type == 0x03.
Optional Fields
Optional fields use if COND { T }. The condition is evaluated at parse time. If true, the field is parsed; otherwise it is absent.
ecn_counts: if frame_type == 0x03 { EcnCounts },In generated C, optional fields appear as a pair:
bool has_ecn_counts;
quic_frames_ecn_counts_t ecn_counts;Bitwise conditions work the same way:
offset_raw: if frame_type & 0x04 { VarInt },The O bit (0x04) in the Stream frame type indicates whether an offset is present. If the bit is clear, has_offset_raw is false and the field is skipped.
bytes[length_or_remaining: EXPR]
Stream and Datagram frames use this special bytes spec. The expression is an Option[VarInt] — it is present when the L bit (0x02) is set, absent otherwise.
data: bytes[length_or_remaining: length_raw],- If
length_rawis present: read exactly that many bytes. - If
length_rawis absent (null): consume all remaining bytes in the current scope.
This cleanly models the RFC 9000 rule: a Stream frame with the L bit clear extends to the end of the QUIC packet.
Derived Fields
let fields are computed from wire fields but are not present on the wire themselves:
let offset: u64 = offset_raw ?? 0,
let fin: bool = (frame_type & 0x01) != 0,The ?? coalesce operator unwraps an optional: offset_raw ?? 0 yields offset_raw if it is present, or 0 if it is absent. This lets you use a clean u64 everywhere downstream instead of checking has_offset_raw.
let fields appear in the generated C struct alongside wire fields:
uint64_t offset; /* derived */
bool fin; /* derived */Multi-Module Compilation
frames.wspec imports VarInt from varint.wspec. Compile both together:
wirespec compile examples/quic/frames.wspec \
-I examples/ \
-o build/The -I examples/ flag tells the resolver where to find quic/varint.wspec. The compiler performs topological sorting and emits quic_varint.h before quic_frames.h so the include order is correct.
Alternatively, compile all files in the examples/quic/ directory at once:
wirespec compile --recursive examples/quic/ -o build/Generated C API:
wirespec_result_t quic_frames_quic_frame_parse(
const uint8_t *buf, size_t len,
quic_frames_quic_frame_t *out, size_t *consumed);
wirespec_result_t quic_frames_quic_frame_serialize(
const quic_frames_quic_frame_t *in,
uint8_t *buf, size_t cap, size_t *written);The generated union discriminates on frame_type:
typedef struct {
quic_varint_var_int_t frame_type;
union {
quic_frames_ack_t ack;
quic_frames_crypto_t crypto;
quic_frames_stream_t stream;
quic_frames_datagram_t datagram;
quic_frames_unknown_t unknown;
/* ... */
};
} quic_frames_quic_frame_t;What Next
- Classic Protocols — UDP, TCP, Ethernet, IPv4 basics
- BLE — little-endian, type aliases, enums, ATT protocol
- MQTT — continuation-bit VarInt, TLV capsule, expression-based dispatch
- TLS 1.3 — enum tags,
u24, fill-within arrays