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)

An opaque binary parser that, when run, consumes input and produces a value of type a or fails with a BittyError.

pub opaque type Parser(a)

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)
pub fn within_bytes_partial(
  byte_len: Int,
  run inner: Parser(a),
) -> Parser(#(a, BitArray))

Like within_bytes, but the inner parser may stop early. Returns the parsed value and any unconsumed bytes from the window as a tuple. Requires byte alignment.

Search Document