bitty
Zero-copy binary parser combinators for Gleam, targeting both BEAM and
JavaScript. Build parsers by composing primitives from bitty/bytes,
bitty/bits, and bitty/num with the combinators in this module.
Parsers are designed for Gleam’s use syntax.
Types
A parse error with the position it occurred at, what was expected,
context labels from enclosing context calls, and an optional message
from fail.
pub type BittyError {
BittyError(
at: Location,
expected: List(String),
context: List(String),
message: option.Option(String),
)
}
Constructors
-
BittyError( at: Location, expected: List(String), context: List(String), message: option.Option(String), )
A position in the input, expressed as a byte offset and a bit offset within that byte (0–7).
pub type Location {
Location(byte: Int, bit: Int)
}
Constructors
-
Location(byte: Int, bit: Int)
Values
pub fn align() -> Parser(Nil)
Skip remaining bits in the current byte to reach the next byte boundary. If already aligned, this is a no-op. Use after bit-level parsing to resume byte-aligned operations.
pub fn attempt(parser: Parser(a)) -> Parser(a)
Wrap a parser to allow backtracking on failure. If the inner parser
fails after consuming input, attempt resets the consumed flag so
that one_of can try the next alternative.
let parser = bitty.one_of([
bitty.attempt(bytes.tag(<<0xCA, 0xFE>>)),
bytes.tag(<<0xCA, 0x11>>),
])
let assert Ok(Nil) = bitty.run(parser, on: <<0xCA, 0x11>>)
pub fn context(parser: Parser(a), in name: String) -> Parser(a)
Push name onto the error context stack on failure, creating a
breadcrumb trail (e.g. ["TLS record", "handshake", "certificate"]).
let parser = num.u8() |> bitty.context(in: "header")
let assert Error(e) = bitty.run(parser, on: <<>>)
assert e.context == ["header"]
pub fn cut(parser: Parser(a)) -> Parser(a)
Commit to the current parse path on success. After cut, a later
failure will not backtrack past this point, producing better error
messages. Typically used after matching a tag or discriminator.
use _ <- bitty.then(bitty.cut(bytes.tag(<<0x01>>)))
use value <- bitty.then(num.u8())
bitty.success(value)
pub fn end() -> Parser(Nil)
Succeed only if all input has been consumed. Fails with
"end of input" expected if bytes remain.
pub fn fail(message: String) -> Parser(a)
Create a parser that always fails with the given message without consuming any input.
pub fn from_result(result: Result(a, e)) -> Parser(a)
Convert a Result(a, e) into a parser: Ok(value) succeeds with the
value, Error(e) fails with the inspected error as the message.
Useful for lifting fallible conversions (e.g. bit_array.to_string)
into a parser pipeline with use syntax.
pub fn label(parser: Parser(a), named name: String) -> Parser(a)
Replace the expected list in a parse error with name.
Useful for giving user-friendly names to complex parsers.
let parser = num.u8() |> bitty.label(named: "message type")
let assert Error(e) = bitty.run(parser, on: <<>>)
assert e.expected == ["message type"]
pub fn location() -> Parser(Location)
Return the current Location in the input without consuming anything.
pub fn many(parser: Parser(a)) -> Parser(List(a))
Repeat a parser zero or more times, collecting results into a list. The inner parser must consume input on each iteration; a non-consuming parser causes an immediate error to prevent infinite loops. Stops when the parser fails without consuming input.
let assert Ok(values) =
bitty.run(bitty.many(num.u8()), on: <<1, 2, 3>>)
assert values == [1, 2, 3]
pub fn many1(parser: Parser(a)) -> Parser(List(a))
Like many, but requires at least one successful match.
pub fn map(parser: Parser(a), with f: fn(a) -> b) -> Parser(b)
Transform the result of a parser by applying f to the parsed value.
pub fn one_of(parsers: List(Parser(a))) -> Parser(a)
Try each parser in order, returning the first success. A parser that
consumes input before failing will not backtrack — wrap alternatives
in attempt if backtracking is needed. Errors from non-consuming
failures at the same position are merged.
let parser = bitty.one_of([
bitty.attempt(bytes.tag(<<0x01>>) |> bitty.map(fn(_) { "one" })),
bytes.tag(<<0x02>>) |> bitty.map(fn(_) { "two" }),
])
let assert Ok("two") = bitty.run(parser, on: <<0x02>>)
pub fn optional(parser: Parser(a)) -> Parser(option.Option(a))
Try a parser, returning Some(value) on success or None if it fails
without consuming input. A consuming failure still propagates.
pub fn repeat(
parser: Parser(a),
times count: Int,
) -> Parser(List(a))
Run a parser exactly count times, collecting results into a list.
pub fn run(
parser: Parser(a),
on input: BitArray,
) -> Result(a, BittyError)
Run a parser on the given input, requiring it to consume all bytes.
Returns Error if parsing fails or if unconsumed input remains.
let assert Ok(byte) = bitty.run(num.u8(), on: <<0xFF>>)
assert byte == 255
pub fn run_partial(
parser: Parser(a),
on input: BitArray,
) -> Result(#(a, BitArray), BittyError)
Run a parser on the given input, returning the parsed value and any unconsumed bytes. Does not require all input to be consumed.
let assert Ok(#(byte, rest)) =
bitty.run_partial(num.u8(), on: <<0xAA, 0xBB>>)
assert byte == 0xAA
assert rest == <<0xBB>>
pub fn run_with_location(
parser: Parser(a),
on input: BitArray,
) -> Result(#(a, Location, BitArray), BittyError)
Like run_partial, but also returns the Location where the parser
stopped. Useful for incremental or streaming parsing.
pub fn success(value: a) -> Parser(a)
Create a parser that always succeeds with the given value without consuming any input.
pub fn then(
parser: Parser(a),
next: fn(a) -> Parser(b),
) -> Parser(b)
Sequence two parsers: run parser, then pass its result to next to
get the second parser. Designed for Gleam’s use syntax:
use length <- bitty.then(num.u8())
use data <- bitty.then(bytes.take(length))
bitty.success(data)
pub fn within_bytes(
byte_len: Int,
run inner: Parser(a),
) -> Parser(a)
Run inner on a zero-copy window of exactly byte_len bytes.
The inner parser must consume the entire window or the parse fails.
Requires byte alignment.
use length <- bitty.then(num.u8())
use value <- bitty.then(bitty.within_bytes(length, run: bytes.rest()))
bitty.success(value)