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
(bytes? x)is true for byte-aligned values -- the JVM-compatible subset.(bitstring? x)is true for any MINO_BYTES, byte-aligned or bit-aligned.
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:
:size N- bit count. Default 8 for:int/:uint, 64 for:float,(* 8 (count v))for:bytes.:type T- one of:int,:uint,:float(32 or 64), or:bytes.:endian E-:big(default) or:little. Little-endian requires:sizeto be a multiple of 8 (matching Erlang).:signed? B- read-side modifier; affectsbits-geton this segment.
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])) ; => trueRandom-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:
mino_bytes(S, src, n)- copynbytes fromsrc(NULL = zero-fill).mino_bytes_from_array(S, src, n)- signed-byte-pointer peer.mino_is_bytes(v),mino_is_bitstring(v)- tag-aware predicates.mino_bytes_len(v),mino_bytes_bit_len(v)- byte and total-bit counts.mino_bytes_data(v)- pointer into the GC-managed buffer.mino_bytes_get(v, i)- read a single byte as 0..255 unsigned int.
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.