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)
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.