bitty

A binary and string parser combinator library for Gleam that tries to be zero-copy when it can be.
You probably don’t want this library. Gleam already has excellent parsing capabilities with
features like bit arrays and pattern matching. For strings the excellent splitter
library can be very useful.
Installation
gleam add bitty
Quick Example
Parsing a simple TLV (tag-length-value) structure
Using Gleam’s use syntax for monadic bind:
import bitty as p
import bitty/bytes as b
import bitty/num
pub type Tlv {
Tlv(tag: Int, length: Int, value: BitArray)
}
pub fn tlv() -> p.Parser(Tlv) {
use tag <- p.then(num.u8())
use len <- p.then(num.u8())
use value <- p.then(b.take(len))
p.success(Tlv(tag, len, value))
}
value is a zero-copy slice.
- No hidden allocation.
- Fully portable.
Core Concepts
Parser
pub opaque type Parser(a)
Parsers operate over BitArray and track:
- Byte offset
- Bit offset
- Commit state for backtracking control
Running a parser
pub fn run(parser: Parser(a), on input: BitArray)
-> Result(a, BittyError)
pub fn run_partial(parser: Parser(a), on input: BitArray)
-> Result(#(a, BitArray), BittyError)
run requires full consumption.
run_partial returns the zero-copy remainder.
Length-Bounded Parsing
Critical for TLV-style formats:
pub fn within_bytes(byte_len: Int, run inner: Parser(a))
-> Parser(a)
Parse exactly byte_len bytes as a sub-stream.
Used for:
- ASN.1 SEQUENCE
- PNG chunks
- Length-prefixed frames
- Nested protocols
Numeric Decoding
import bitty/num
num.u8()
num.u16(num.BigEndian)
num.u32(num.LittleEndian)
num.u64(num.BigEndian)
All integer parsers return Int. On the JavaScript target, 64-bit values
above 2^53 - 1 (9,007,199,254,740,991) may lose precision due to IEEE 754
double-precision floating-point limitations.
Bit-Level Parsing
import bitty/bits
use flag <- p.then(bits.bit())
use value <- p.then(bits.uint(5))
use _ <- p.then(p.align())
p.success(#(flag, value))
Byte-aligned parsing is the fast path.
Bit parsing is explicit and opt-in.
String Parsing
import bitty as p
import bitty/string as s
pub fn request_line() -> p.Parser(#(String, String, String)) {
use method <- p.then(s.alpha1())
use _ <- p.then(s.literal(" "))
use path <- p.then(s.take_while1(fn(c) { c != " " }))
use _ <- p.then(s.literal(" "))
use version <- p.then(s.not_line_ending())
p.success(#(method, path, version))
}
Error Reporting
Structured error type:
pub type BittyError {
BittyError(
at: Location,
expected: List(String),
context: List(String),
message: Option(String),
)
}
Add readable labels:
p.label(parser, named: "ASN.1 length")
p.context(parser, in: "TLS handshake")
You get precise error locations and meaningful context stacks.