SagaSaga
Guide

BitStrings

BitStrings are the BEAM's native binary data type. Saga provides syntax for constructing and pattern matching on binary data, following Erlang's bit syntax with a few simplifications.

Construction

Build a bitstring with << >>:

let bs = <<72, 101, 108, 108, 111>>   # "Hello" as bytes
let empty = <<>>

By default, each element is an 8-bit unsigned integer (a byte). For other sizes and types, add segment specifiers after a colon:

# Explicit sizes
<<1:8, 256:16/big>>          # 1 byte + 2 bytes big-endian

# Strings expand to their UTF-8 bytes
<<1:8, 256:16/big, "hello">>

# Endianness
<<value:32/big>>             # big-endian (default)
<<value:32/little>>          # little-endian

# Float segments
<<value:64/float>>           # 64-bit IEEE 754

# UTF-8 codepoints
<<char/utf8>>

# Binary (variable-length) segments
<<header:4/binary, rest/binary>>

Concatenation

BitStrings support the <> operator:

let a = <<1, 2, 3>>
let b = <<4, 5, 6>>
let combined = a <> b   # <<1, 2, 3, 4, 5, 6>>

Pattern Matching

BitString patterns use the same << >> syntax in case expressions. This is where binary data handling gets powerful:

case packet {
  <<tag:8, rest/binary>> -> process tag rest
  <<>> -> dbg "empty"
  _ -> dbg "no match"
}

Segment specifiers in patterns

Patterns support the same specifiers as construction:

case data {
  <<tag:8, len:16/big, payload:len/binary>> -> {
    dbg $"tag: {tag}, length: {len}"
    dbg (debug payload)
  }
  _ -> dbg "no match"
}

Notice that len is bound by an earlier segment and used as the size of payload. This variable-sized segment matching is one of the BEAM's most powerful features for parsing binary protocols.

Parsing a binary protocol

Here is a more complete example parsing a simple packet format with a tag byte, a 16-bit length, and a payload:

import Std.BitString

type Packet =
  | Data BitString
  | Ping
  | Unknown Int

fun parse_packet : BitString -> Packet
parse_packet bs = case bs {
  <<1:8, len:16/big, payload:len/binary>> -> Data payload
  <<2:8>> -> Ping
  <<tag:8, _/binary>> -> Unknown tag
  _ -> Unknown 0
}

Stdlib Operations

Std.BitString provides functions for working with bitstrings programmatically:

import Std.BitString

let bs = BitString.from_list [1, 2, 3]
BitString.size bs                    # 3
BitString.to_list bs                 # [1, 2, 3]
BitString.at 0 bs                    # Just 1
BitString.slice 1 2 bs               # <<2, 3>>
BitString.is_empty <<>>              # True

# Integer encoding
BitString.encode_int 4 256           # <<0, 0, 1, 0>> (big-endian)
BitString.decode_int <<0, 0, 1, 0>>  # 256

# Little-endian variants
BitString.encode_int_little 4 256    # <<0, 1, 0, 0>>
BitString.decode_int_little <<0, 1, 0, 0>>  # 256

# String conversion
BitString.from_string "hello"        # <<104, 101, 108, 108, 111>>
BitString.to_string <<104, 101, 108, 108, 111>>  # Ok "hello"