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"