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)

Deprecated: Use bitty/bits.align instead

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 capture(parser: Parser(a)) -> Parser(BitArray)

Run a parser and return the raw bytes it consumed, discarding the parsed value. Requires byte alignment at both start and end.

let inner = {
  use _ <- bitty.then(num.u8())
  use _ <- bitty.then(num.u8())
  bitty.success(Nil)
}
let assert Ok(raw) = bitty.run(bitty.capture(inner), on: <<0xCA, 0xFE>>)
assert raw == <<0xCA, 0xFE>>
pub fn cond(
  when condition: Bool,
  run parser: Parser(a),
) -> Parser(option.Option(a))

Conditionally run a parser. If condition is True, run the parser and wrap the result in Some. If False, return None without consuming input.

let parser = bitty.cond(when: True, run: num.u8())
let assert Ok(Some(42)) = bitty.run(parser, on: <<42>>)
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 delimited(
  open: Parser(a),
  parser: Parser(b),
  close: Parser(c),
) -> Parser(b)

Run open, parser, then close, returning only the parser’s value.

let parser = bitty.delimited(
  bytes.tag(<<0x28>>),
  num.u8(),
  bytes.tag(<<0x29>>),
)
let assert Ok(value) = bitty.run(parser, on: <<0x28, 42, 0x29>>)
assert value == 42
pub fn end() -> Parser(Nil)

Succeed only if all input has been consumed. Fails with "end of input" expected if bytes remain.

let assert Ok(Nil) = bitty.run(bitty.end(), on: <<>>)
pub fn fail(message: String) -> Parser(a)

Create a parser that always fails with the given message without consuming any input.

pub fn fold(
  parser: Parser(a),
  from initial: b,
  with f: fn(b, a) -> b,
) -> Parser(b)

Like many but accumulates results with a function instead of building a list. Runs the parser zero or more times.

let parser = bitty.fold(num.u8(), from: 0, with: fn(acc, x) { acc + x })
let assert Ok(6) = bitty.run(parser, on: <<1, 2, 3>>)
pub fn fold1(
  parser: Parser(a),
  from initial: b,
  with f: fn(b, a) -> b,
) -> Parser(b)

Like fold but requires at least one successful match.

let parser = bitty.fold1(num.u8(), from: 0, with: fn(acc, x) { acc + x })
let assert Ok(6) = bitty.run(parser, on: <<1, 2, 3>>)
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 length_repeat(
  count: Parser(Int),
  run parser: Parser(a),
) -> Parser(List(a))

Parse a count, then run a parser that many times. Dynamic version of repeat.

let parser = bitty.length_repeat(num.u8(), run: num.u8())
let assert Ok(values) = bitty.run(parser, on: <<3, 10, 20, 30>>)
assert values == [10, 20, 30]
pub fn length_take(count: Parser(Int)) -> Parser(BitArray)

Parse a byte count, then take that many bytes as a BitArray. Dynamic version of bytes.take.

let parser = bitty.length_take(num.u8())
let assert Ok(data) = bitty.run(parser, on: <<3, 0xAA, 0xBB, 0xCC>>)
assert data == <<0xAA, 0xBB, 0xCC>>
pub fn length_within(
  count: Parser(Int),
  run parser: Parser(a),
) -> Parser(a)

Parse a byte count, then run a parser within a window of that many bytes. Dynamic version of within_bytes.

let parser = bitty.length_within(num.u8(), run: bytes.rest())
let assert Ok(data) = bitty.run(parser, on: <<3, 0xAA, 0xBB, 0xCC>>)
assert data == <<0xAA, 0xBB, 0xCC>>
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.

let assert Ok(values) =
  bitty.run(bitty.many1(num.u8()), on: <<1, 2, 3>>)
assert values == [1, 2, 3]
pub fn many_until(
  parser: Parser(a),
  until terminator: Parser(b),
) -> Parser(#(List(a), b))

Repeat a parser until a terminator succeeds, returning the collected values and the terminator’s result as a tuple. The terminator is tried first each iteration; on failure the item parser runs. The item parser must consume input on each iteration to prevent infinite loops.

let parser = bitty.many_until(num.u8(), until: bytes.tag(<<0x00>>))
let assert Ok(#(values, Nil)) =
  bitty.run(parser, on: <<1, 2, 3, 0x00>>)
assert values == [1, 2, 3]
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 not(parser: Parser(a)) -> Parser(Nil)

Negative lookahead: succeeds with Nil if the child parser fails, fails if the child parser succeeds. Never consumes input.

let parser = {
  use _ <- bitty.then(bitty.not(bytes.tag(<<0x00>>)))
  num.u8()
}
let assert Ok(0x42) = bitty.run(parser, on: <<0x42>>)
let assert Error(_) = bitty.run(parser, on: <<0x00>>)
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.

let assert Ok(value) =
  bitty.run(bitty.optional(num.u8()), on: <<42>>)
assert value == Some(42)
pub fn pair(
  first: Parser(a),
  second: Parser(b),
) -> Parser(#(a, b))

Run two parsers in sequence and return their results as a tuple.

let parser = bitty.pair(num.u8(), num.u8())
let assert Ok(value) = bitty.run(parser, on: <<1, 2>>)
assert value == #(1, 2)
pub fn preceded(
  prefix: Parser(a),
  parser: Parser(b),
) -> Parser(b)

Run prefix then parser, discarding the prefix result and returning only the parser’s value.

use value <- bitty.then(bitty.preceded(bytes.tag(<<0x00>>), num.u8()))
bitty.success(value)
pub fn repeat(
  parser: Parser(a),
  times count: Int,
) -> Parser(List(a))

Run a parser exactly count times, collecting results into a list.

let assert Ok(values) =
  bitty.run(bitty.repeat(num.u8(), times: 2), on: <<1, 2, 3>>)
assert values == [1, 2]
pub fn replace(parser: Parser(a), with value: b) -> Parser(b)

Replace the result of a parser with a fixed value, discarding the original result. Useful with tag patterns.

let parser = bytes.tag(<<0x01>>) |> bitty.replace(with: "one")
let assert Ok("one") = bitty.run(parser, on: <<0x01>>)
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 input. Does not require all input to be consumed. The returned BitArray may not be byte-aligned when bit-level parsers are used.

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. The returned BitArray may not be byte-aligned when bit-level parsers are used.

pub fn separated(
  parser: Parser(a),
  by separator: Parser(b),
) -> Parser(List(a))

Parse zero or more occurrences of parser separated by separator. The separator parser’s result is discarded. Returns a list of the parsed values. Succeeds with an empty list if the first item fails without consuming input.

A trailing separator (one not followed by a valid item) is left unconsumed. Compose with end() if you need to ensure all input is consumed.

let parser = bitty.separated(num.u8(), by: bytes.tag(<<0x2C>>))
let assert Ok(values) = bitty.run(parser, on: <<1, 0x2C, 2, 0x2C, 3>>)
assert values == [1, 2, 3]
pub fn separated1(
  parser: Parser(a),
  by separator: Parser(b),
) -> Parser(List(a))

Like separated, but requires at least one item.

A trailing separator (one not followed by a valid item) is left unconsumed. Compose with end() if you need to ensure all input is consumed.

let parser = bitty.separated1(num.u8(), by: bytes.tag(<<0x2C>>))
let assert Ok(values) = bitty.run(parser, on: <<1, 0x2C, 2, 0x2C, 3>>)
assert values == [1, 2, 3]
pub fn separated_pair(
  first: Parser(a),
  by separator: Parser(b),
  then second: Parser(c),
) -> Parser(#(a, c))

Run two parsers separated by a third, discarding the separator’s result.

let parser = bitty.separated_pair(
  num.u8(),
  by: bytes.tag(<<0x2C>>),
  then: num.u8(),
)
let assert Ok(value) = bitty.run(parser, on: <<1, 0x2C, 2>>)
assert value == #(1, 2)
pub fn success(value: a) -> Parser(a)

Create a parser that always succeeds with the given value without consuming any input.

pub fn terminated(
  parser: Parser(a),
  suffix: Parser(b),
) -> Parser(a)

Run parser then suffix, discarding the suffix result and returning only the parser’s value.

use value <- bitty.then(bitty.terminated(num.u8(), bytes.tag(<<0x00>>)))
bitty.success(value)
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 verify(
  parser: Parser(a),
  with predicate: fn(a) -> Bool,
) -> Parser(a)

Run a parser and then check the result against a predicate. If the predicate returns False, the parse fails with "verify" expected.

let parser = num.u8() |> bitty.verify(with: fn(x) { x > 0 })
let assert Ok(42) = bitty.run(parser, on: <<42>>)
let assert Error(_) = bitty.run(parser, on: <<0>>)
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 input from the window as a tuple. Requires byte alignment. The returned BitArray may not be byte-aligned when bit-level parsers are used.

Search Document