Bytes and Bit Syntax

mino has a dedicated immutable binary-data value type and a destructure-shaped surface for packing and unpacking bit fields. The design borrows from Erlang's bit syntax, which has been a first-class data type on BEAM for two decades. JVM Clojure does not ship anything equivalent; mino's embedded focus makes binary protocols, sensor packets, hardware register layouts, and similar bit-precise workloads a target use case.

The bytes value type

(byte-array N) returns a MINO_BYTES value of N zero-initialised bytes; (byte-array coll) packs each element of the collection as an unsigned byte (range -128..255). The value is immutable; aset on it throws eval/state MST005. The persistent-value model does not allow in-place writes.

(byte-array 4)
;=> #bytes "00000000"

(byte-array [0x41 0x42 0x43])
;=> #bytes "414243"

(byte-array (range 5))
;=> #bytes "0001020304"

The literal form #bytes "HEX..." round-trips through reader and printer. Whitespace between hex pairs is tolerated by the reader.

Predicates

Bit-aligned values appear once the bit-syntax surface (below) packs a total bit length that is not a multiple of 8. Their print form carries a trailing /N suffix, e.g. #bytes "08/5" is a five-bit value.

Sequence integration

Every standard seq abstraction works:

(first (byte-array [9 8 7]))    ; => 9
(rest (byte-array [9 8 7]))     ; => (8 7)
(nth (byte-array [9 8 7]) 1)    ; => 8
(count (byte-array [1 2 3]))    ; => 3
(reduce + (byte-array [1 2 3])) ; => 6
(map inc (byte-array [1 2 3]))  ; => (2 3 4)
(take 5 (filter odd?
                (byte-array (range 50))))
; => (1 3 5 7 9)
(into [] (byte-array [10 20 30]))
; => [10 20 30]

Internally, (seq bytes) returns a chunked-cons spine of 32-element chunks, matching how vector seq works. Pipelines like (map ...) over (filter ...) over bytes propagate chunkedness end-to-end without per-element allocation overhead.

Constructing bit-aligned values

bits takes any number of [value & options] segments and packs them in order:

(bits [0x12  :size 8]
      [0x34  :size 8])
;=> #bytes "1234"

(bits [0x1234 :size 16])
;=> #bytes "1234"

(bits [0x1234 :size 16 :endian :little])
;=> #bytes "3412"

(bits [3.14 :size 32 :type :float])
;=> #bytes "4048f5c3"

Supported options:

When the total bit length is not a multiple of 8, the result is a bit-aligned bitstring:

(bits [1 :size 5])
;=> #bytes "08/5"
(bytes? (bits [1 :size 5]))     ; => false
(bitstring? (bits [1 :size 5])) ; => true

Random-access reads

bits-get reads a bit field at an arbitrary offset:

(let [bs (bits [0xABCD :size 16])]
  [(bits-get bs :offset 0 :size 16)
   (bits-get bs :offset 0 :size 4)
   (bits-get bs :offset 12 :size 4)
   (bits-get bs :offset 0 :size 8 :signed? true)])
;=> [43981 10 13 -85]

For :type :bytes, bits-get returns a fresh bytes value covering the requested range -- a zero-copy-semantics slice.

subbits is the dedicated slice over a half-open bit range:

(subbits (bits [0xFF :size 8] [0x00 :size 8]) 4 12)
;=> #bytes "f0"

Destructuring with let-bits

let-bits is the destructure-shape over bits-get. Each segment is a [symbol & options] vector; the macro emits the running-offset chain automatically. The final :type :bytes segment without an explicit :size binds the bit-aligned remainder.

(let-bits [packet
           [version  :size 4]
           [ihl      :size 4]
           [dscp     :size 6]
           [ecn      :size 2]
           [total    :size 16]
           [id       :size 16]
           [flags    :size 3]
           [frag-off :size 13]
           [ttl      :size 8]
           [proto    :size 8]
           [checksum :size 16]
           [src      :size 32 :type :bytes]
           [dst      :size 32 :type :bytes]]
  ...)

That's the canonical IPv4 header decoder. The full worked example is in mino-examples under use-cases/packet_parsing.cpp.

Why this is in mino

Embedded use cases routinely deal with binary protocols that JVM Clojure makes awkward: every ByteBuffer.get* call is a separate ceremony, and sub-byte fields require manual shift / mask arithmetic. Erlang's bit syntax solved this in 2001 and remains its most distinctive surface feature; mino borrows the shape because the embedded-runtime niche has the same needs.

mino's surface is not a drop-in clone -- Clojure's destructure idioms shape the API names (let-bits mirrors the rest of the let family) -- but the semantics line up: type tag, size, endianness, and sign all work the way an Erlang programmer expects.

Chess engines are another classic bitboard use case; the use-cases/chess_bitboard.cpp example in mino-examples shows how to use bits / bits-get plus mino's bitwise primitives to represent piece positions as 64-bit bitboards and compute knight attacks.

Embedder C-API

Host code can construct and inspect bytes values without going through the script surface:

The buffer is GC-managed; the pointer is stable for the value's lifetime. Cross-state mino_clone deep-copies the bytes so each mino_state owns its own storage.